1use anyhow;
2use async_trait::async_trait;
3use macaddr::MacAddr;
4use measurements::{AngularVelocity, Frequency, Power, Temperature, Voltage};
5use reqwest::Method;
6use serde_json::{Value, json};
7use std::collections::HashMap;
8use std::net::IpAddr;
9use std::str::FromStr;
10use std::time::Duration;
11
12use crate::data::board::{BoardData, ChipData};
13use crate::data::device::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
14use crate::data::device::{MinerControlBoard, MinerMake};
15use crate::data::fan::FanData;
16use crate::data::hashrate::{HashRate, HashRateUnit};
17use crate::data::pool::{PoolData, PoolURL};
18use crate::miners::backends::traits::*;
19use crate::miners::commands::MinerCommand;
20use crate::miners::data::{
21 DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer,
22};
23
24use web::PowerPlayWebAPI;
25
26mod web;
27
28#[derive(Debug)]
29pub struct PowerPlayV1 {
30 ip: IpAddr,
31 web: PowerPlayWebAPI,
32 device_info: DeviceInfo,
33}
34
35impl PowerPlayV1 {
36 pub fn new(ip: IpAddr, model: MinerModel) -> Self {
37 PowerPlayV1 {
38 ip,
39 web: PowerPlayWebAPI::new(ip, 4028),
40 device_info: DeviceInfo::new(
41 MinerMake::from(model),
42 model,
43 MinerFirmware::EPic,
44 HashAlgorithm::SHA256,
45 ),
46 }
47 }
48}
49
50#[async_trait]
51impl APIClient for PowerPlayV1 {
52 async fn get_api_result(&self, command: &MinerCommand) -> anyhow::Result<Value> {
53 match command {
54 MinerCommand::WebAPI { .. } => self.web.get_api_result(command).await,
55 _ => Err(anyhow::anyhow!(
56 "Unsupported command type for ePIC PowerPlay API"
57 )),
58 }
59 }
60}
61
62impl GetDataLocations for PowerPlayV1 {
63 fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
64 const WEB_SUMMARY: MinerCommand = MinerCommand::WebAPI {
65 command: "summary",
66 parameters: None,
67 };
68 const WEB_NETWORK: MinerCommand = MinerCommand::WebAPI {
69 command: "network",
70 parameters: None,
71 };
72 const WEB_CAPABILITIES: MinerCommand = MinerCommand::WebAPI {
73 command: "capabilities",
74 parameters: None,
75 };
76 const WEB_CHIP_TEMPS: MinerCommand = MinerCommand::WebAPI {
77 command: "temps/chip",
78 parameters: None,
79 };
80 const WEB_CHIP_VOLTAGES: MinerCommand = MinerCommand::WebAPI {
81 command: "voltages",
82 parameters: None,
83 };
84 const WEB_CHIP_HASHRATES: MinerCommand = MinerCommand::WebAPI {
85 command: "hashrate",
86 parameters: None,
87 };
88 const WEB_CHIP_CLOCKS: MinerCommand = MinerCommand::WebAPI {
89 command: "clocks",
90 parameters: None,
91 };
92 const WEB_TEMPS: MinerCommand = MinerCommand::WebAPI {
93 command: "temps",
94 parameters: None,
95 };
96
97 match data_field {
98 DataField::Mac => vec![(
99 WEB_NETWORK,
100 DataExtractor {
101 func: get_by_pointer,
102 key: Some(""),
103 tag: None,
104 },
105 )],
106 DataField::Hostname => vec![(
107 WEB_SUMMARY,
108 DataExtractor {
109 func: get_by_pointer,
110 key: Some("/Hostname"),
111 tag: None,
112 },
113 )],
114 DataField::Uptime => vec![(
115 WEB_SUMMARY,
116 DataExtractor {
117 func: get_by_pointer,
118 key: Some("/Session/Uptime"),
119 tag: None,
120 },
121 )],
122 DataField::Wattage => vec![(
123 WEB_SUMMARY,
124 DataExtractor {
125 func: get_by_pointer,
126 key: Some("/Power Supply Stats/Input Power"),
127 tag: None,
128 },
129 )],
130 DataField::Fans => vec![(
131 WEB_SUMMARY,
132 DataExtractor {
133 func: get_by_pointer,
134 key: Some("/Fans Rpm"),
135 tag: None,
136 },
137 )],
138 DataField::Hashboards => vec![
139 (
140 WEB_TEMPS,
141 DataExtractor {
142 func: get_by_pointer,
143 key: Some(""),
144 tag: Some("Board Temps"),
145 },
146 ),
147 (
148 WEB_SUMMARY,
149 DataExtractor {
150 func: get_by_pointer,
151 key: Some(""),
152 tag: Some("Summary"),
153 },
154 ),
155 (
156 WEB_CHIP_TEMPS,
157 DataExtractor {
158 func: get_by_pointer,
159 key: Some(""),
160 tag: Some("Chip Temps"),
161 },
162 ),
163 (
164 WEB_CHIP_VOLTAGES,
165 DataExtractor {
166 func: get_by_pointer,
167 key: Some(""),
168 tag: Some("Chip Voltages"),
169 },
170 ),
171 (
172 WEB_CHIP_HASHRATES,
173 DataExtractor {
174 func: get_by_pointer,
175 key: Some(""),
176 tag: Some("Chip Hashrates"),
177 },
178 ),
179 (
180 WEB_CHIP_CLOCKS,
181 DataExtractor {
182 func: get_by_pointer,
183 key: Some(""),
184 tag: Some("Chip Clocks"),
185 },
186 ),
187 (
188 WEB_CAPABILITIES,
189 DataExtractor {
190 func: get_by_pointer,
191 key: Some(""),
192 tag: Some("Capabilities"),
193 },
194 ),
195 ],
196 DataField::Pools => vec![(
197 WEB_SUMMARY,
198 DataExtractor {
199 func: get_by_pointer,
200 key: Some(""),
201 tag: None,
202 },
203 )],
204 DataField::IsMining => vec![(
205 WEB_SUMMARY,
206 DataExtractor {
207 func: get_by_pointer,
208 key: Some("/Status/Operating State"),
209 tag: None,
210 },
211 )],
212 DataField::LightFlashing => vec![(
213 WEB_SUMMARY,
214 DataExtractor {
215 func: get_by_pointer,
216 key: Some("/Misc/Locate Miner State"),
217 tag: None,
218 },
219 )],
220 DataField::ControlBoardVersion => vec![(
221 WEB_CAPABILITIES,
222 DataExtractor {
223 func: get_by_pointer,
224 key: Some("/Control Board Version/cpuHardware"),
225 tag: None,
226 },
227 )],
228 DataField::SerialNumber => vec![(
229 WEB_CAPABILITIES,
230 DataExtractor {
231 func: get_by_pointer,
232 key: Some("/Control Board Version/cpuSerial"),
233 tag: None,
234 },
235 )],
236 DataField::ExpectedHashrate => vec![(
237 WEB_CAPABILITIES,
238 DataExtractor {
239 func: get_by_pointer,
240 key: Some("/Default Hashrate"),
241 tag: None,
242 },
243 )],
244 DataField::FirmwareVersion => vec![(
245 WEB_SUMMARY,
246 DataExtractor {
247 func: get_by_pointer,
248 key: Some("/Software"),
249 tag: None,
250 },
251 )],
252 DataField::Hashrate => vec![(
253 WEB_SUMMARY,
254 DataExtractor {
255 func: get_by_pointer,
256 key: Some("/HBs"),
257 tag: None,
258 },
259 )],
260 _ => vec![],
261 }
262 }
263}
264
265impl GetIP for PowerPlayV1 {
266 fn get_ip(&self) -> IpAddr {
267 self.ip
268 }
269}
270
271impl GetDeviceInfo for PowerPlayV1 {
272 fn get_device_info(&self) -> DeviceInfo {
273 self.device_info
274 }
275}
276
277impl CollectData for PowerPlayV1 {
278 fn get_collector(&self) -> DataCollector<'_> {
279 DataCollector::new(self)
280 }
281}
282
283impl GetMAC for PowerPlayV1 {
284 fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
285 match serde_json::from_value::<HashMap<String, Value>>(data.get(&DataField::Mac)?.clone())
286 .ok()
287 .and_then(|inner| inner.get("dhcp").or_else(|| inner.get("static")).cloned())
288 .and_then(|obj| {
289 obj.get("mac_address")
290 .and_then(|v| v.as_str())
291 .map(String::from)
292 }) {
293 Some(mac_str) => MacAddr::from_str(&mac_str).ok(),
294 None => None,
295 }
296 }
297}
298
299impl GetSerialNumber for PowerPlayV1 {
300 fn parse_serial_number(&self, data: &HashMap<DataField, Value>) -> Option<String> {
301 data.extract::<String>(DataField::SerialNumber)
302 }
303}
304
305impl GetHostname for PowerPlayV1 {
306 fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
307 data.extract::<String>(DataField::Hostname)
308 }
309}
310
311impl GetApiVersion for PowerPlayV1 {
312 fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
313 data.extract::<String>(DataField::ApiVersion)
314 }
315}
316
317impl GetFirmwareVersion for PowerPlayV1 {
318 fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
319 data.extract::<String>(DataField::FirmwareVersion)
320 }
321}
322
323impl GetControlBoardVersion for PowerPlayV1 {
324 fn parse_control_board_version(
325 &self,
326 data: &HashMap<DataField, Value>,
327 ) -> Option<MinerControlBoard> {
328 let cb_type = data.extract::<String>(DataField::ControlBoardVersion)?;
329 match cb_type.as_str() {
330 s if s.to_uppercase().contains("AMLOGIC") => Some(MinerControlBoard::AMLogic),
331 _ => Some(MinerControlBoard::EPicUMC),
332 }
333 }
334}
335
336impl GetHashboards for PowerPlayV1 {
337 fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
338 let mut hashboards: Vec<BoardData> = Vec::new();
339 for _ in 0..self.device_info.hardware.boards.unwrap_or_default() {
340 hashboards.push(BoardData {
341 position: 0,
342 hashrate: None,
343 expected_hashrate: None,
344 board_temperature: None,
345 intake_temperature: None,
346 outlet_temperature: None,
347 expected_chips: None,
348 working_chips: None,
349 serial_number: None,
350 chips: vec![],
351 voltage: None,
352 frequency: None,
353 tuned: None,
354 active: None,
355 });
356 }
357
358 data.get(&DataField::Hashboards)
359 .and_then(|v| v.pointer("/Summary/HBStatus"))
360 .and_then(|v| {
361 v.as_array().map(|boards| {
362 boards.iter().for_each(|board| {
363 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
364 && let Some(hashboard) = hashboards.get_mut(idx as usize)
365 {
366 hashboard.position = idx as u8;
367 if let Some(v) = board.get("Enabled").and_then(|v| v.as_bool()) {
368 hashboard.active = Some(v);
369 }
370 }
371 })
372 })
373 });
374
375 for board in &mut hashboards {
377 board.expected_chips = self.device_info.hardware.chips;
378 if board.active.unwrap_or(false) {
380 board.chips = vec![
381 ChipData {
382 position: 0,
383 hashrate: None,
384 temperature: None,
385 voltage: None,
386 frequency: None,
387 tuned: None,
388 working: None,
389 };
390 self.device_info.hardware.chips.unwrap_or_default() as usize
391 ];
392 }
393 }
394
395 if let Some(serial_numbers) = data
397 .get(&DataField::Hashboards)
398 .and_then(|v| v.pointer("/Capabilities/Board Serial Numbers"))
399 .and_then(|v| v.as_array())
400 {
401 for serial in serial_numbers {
402 for hb in hashboards.iter_mut() {
405 if hb.serial_number.is_none() && hb.active.unwrap_or(false) {
406 if let Some(serial_str) = serial.as_str() {
407 hb.serial_number = Some(serial_str.to_string());
408 }
409 break; }
411 }
412 }
413 };
414
415 data.get(&DataField::Hashboards)
417 .and_then(|v| v.pointer("/Summary/HBs"))
418 .and_then(|v| {
419 v.as_array().map(|boards| {
420 boards.iter().for_each(|board| {
421 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
422 && let Some(hashboard) = hashboards.get_mut(idx as usize)
423 {
424 if let Some(h) = board
426 .get("Hashrate")
427 .and_then(|v| v.as_array())
428 .and_then(|v| v.first().and_then(|f| f.as_f64()))
429 {
430 hashboard.hashrate = Some(HashRate {
431 value: h,
432 unit: HashRateUnit::MegaHash,
433 algo: String::from("SHA256"),
434 })
435 };
436
437 if let Some(h) = board
439 .get("Hashrate")
440 .and_then(|v| v.as_array())
441 .and_then(|v| {
442 Some((
443 v.first().and_then(|f| f.as_f64())?,
444 v.get(1).and_then(|f| f.as_f64())?,
445 ))
446 })
447 {
448 hashboard.expected_hashrate = Some(HashRate {
449 value: h.0 / h.1,
450 unit: HashRateUnit::MegaHash,
451 algo: String::from("SHA256"),
452 })
453 };
454
455 if let Some(f) = board.get("Core Clock Avg").and_then(|v| v.as_f64()) {
457 hashboard.frequency = Some(Frequency::from_megahertz(f))
458 };
459
460 if let Some(v) = board.get("Input Voltage").and_then(|v| v.as_f64()) {
462 hashboard.voltage = Some(Voltage::from_volts(v));
463 };
464 if let Some(v) = board.get("Temperature").and_then(|v| v.as_f64()) {
466 hashboard.board_temperature = Some(Temperature::from_celsius(v));
467 };
468 };
469 })
470 })
471 });
472
473 data.get(&DataField::Hashboards)
475 .and_then(|v| v.pointer("/Board Temps"))
476 .and_then(|v| {
477 v.as_array().map(|boards| {
478 boards.iter().for_each(|board| {
479 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
480 && let Some(hashboard) = hashboards.get_mut(idx as usize)
481 {
482 if let Some(h) = board.get("Data").and_then(|v| {
484 v.as_array().and_then(|arr| {
485 arr.iter()
486 .filter_map(|v| v.as_f64())
487 .max_by(|a, b| a.partial_cmp(b).unwrap())
488 })
489 }) {
490 hashboard.outlet_temperature = Some(Temperature::from_celsius(h));
491 };
492
493 if let Some(h) = board.get("Data").and_then(|v| {
494 v.as_array().and_then(|arr| {
495 arr.iter()
496 .filter_map(|v| v.as_f64())
497 .min_by(|a, b| a.partial_cmp(b).unwrap())
498 })
499 }) {
500 hashboard.intake_temperature = Some(Temperature::from_celsius(h));
501 };
502 };
503 })
504 })
505 });
506
507 data.get(&DataField::Hashboards)
509 .and_then(|v| v.pointer("/Chip Temps"))
510 .and_then(|v| {
511 v.as_array().map(|boards| {
512 boards.iter().for_each(|board| {
513 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
514 && let Some(hashboard) = hashboards.get_mut(idx as usize)
515 && let Some(t) =
516 board.get("Data").and_then(|v| v.as_array()).map(|arr| {
517 arr.iter()
518 .filter_map(|v| v.as_f64())
519 .map(Temperature::from_celsius)
520 .collect::<Vec<Temperature>>()
521 })
522 {
523 for (chip_no, temp) in t.iter().enumerate() {
524 if let Some(chip_data) = hashboard.chips.get_mut(chip_no) {
525 chip_data.position = chip_no as u16;
526 chip_data.temperature = Some(*temp);
527 }
528 }
529 };
530 })
531 })
532 });
533
534 data.get(&DataField::Hashboards)
536 .and_then(|v| v.pointer("/Chip Voltages"))
537 .and_then(|v| {
538 v.as_array().map(|boards| {
539 boards.iter().for_each(|board| {
540 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
541 && let Some(hashboard) = hashboards.get_mut(idx as usize)
542 && let Some(t) =
543 board.get("Data").and_then(|v| v.as_array()).map(|arr| {
544 arr.iter()
545 .filter_map(|v| v.as_f64())
546 .map(Voltage::from_millivolts)
547 .collect::<Vec<Voltage>>()
548 })
549 {
550 for (chip_no, voltage) in t.iter().enumerate() {
551 if let Some(chip_data) = hashboard.chips.get_mut(chip_no) {
552 chip_data.position = chip_no as u16;
553 chip_data.voltage = Some(*voltage);
554 }
555 }
556 };
557 })
558 })
559 });
560
561 data.get(&DataField::Hashboards)
563 .and_then(|v| v.pointer("/Chip Clocks"))
564 .and_then(|v| {
565 v.as_array().map(|boards| {
566 boards.iter().for_each(|board| {
567 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
568 && let Some(hashboard) = hashboards.get_mut(idx as usize)
569 && let Some(t) =
570 board.get("Data").and_then(|v| v.as_array()).map(|arr| {
571 arr.iter()
572 .filter_map(|v| v.as_f64())
573 .map(Frequency::from_megahertz)
574 .collect::<Vec<Frequency>>()
575 })
576 {
577 for (chip_no, freq) in t.iter().enumerate() {
578 if let Some(chip_data) = hashboard.chips.get_mut(chip_no) {
579 chip_data.position = chip_no as u16;
580 chip_data.frequency = Some(*freq);
581 }
582 }
583 };
584 })
585 })
586 });
587
588 data.get(&DataField::Hashboards)
591 .and_then(|v| v.pointer("/Chip Hashrates"))
592 .and_then(|v| {
593 v.as_array().map(|boards| {
594 boards.iter().for_each(|board| {
595 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
596 && let Some(hashboard) = hashboards.get_mut(idx as usize)
597 && let Some(t) =
598 board.get("Data").and_then(|v| v.as_array()).map(|arr| {
599 arr.iter()
600 .filter_map(|inner| inner.as_array())
601 .filter_map(|inner| inner.first().and_then(|v| v.as_f64()))
602 .map(|hr| HashRate {
603 value: hr,
604 unit: HashRateUnit::MegaHash,
605 algo: String::from("SHA256"),
606 })
607 .collect::<Vec<HashRate>>()
608 })
609 {
610 for (chip_no, hashrate) in t.iter().enumerate() {
611 if let Some(chip_data) = hashboard.chips.get_mut(chip_no) {
612 chip_data.position = chip_no as u16;
613 chip_data.working = Some(true);
614 chip_data.hashrate = Some(hashrate.clone());
615 }
616 }
617 };
618 })
619 })
620 });
621
622 hashboards
623 }
624}
625
626impl GetHashrate for PowerPlayV1 {
627 fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
628 let mut total_hashrate: f64 = 0.0;
629
630 data.get(&DataField::Hashrate).and_then(|v| {
631 v.as_array().map(|boards| {
632 boards.iter().for_each(|board| {
633 if let Some(_idx) = board.get("Index").and_then(|v| v.as_u64()) {
634 if let Some(h) = board
636 .get("Hashrate")
637 .and_then(|v| v.as_array())
638 .and_then(|v| v.first().and_then(|f| f.as_f64()))
639 {
640 total_hashrate += h;
641 };
642 }
643 })
644 })
645 });
646
647 Some(HashRate {
648 value: total_hashrate,
649 unit: HashRateUnit::MegaHash,
650 algo: String::from("SHA256"),
651 })
652 }
653}
654
655impl GetExpectedHashrate for PowerPlayV1 {
656 fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
657 data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| HashRate {
658 value: f,
659 unit: HashRateUnit::TeraHash,
660 algo: String::from("SHA256"),
661 })
662 }
663}
664
665impl GetFans for PowerPlayV1 {
666 fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
667 let mut fans: Vec<FanData> = Vec::new();
668
669 if let Some(fans_data) = data.get(&DataField::Fans)
670 && let Some(obj) = fans_data.as_object()
671 {
672 for (key, value) in obj {
673 if let Some(num) = value.as_f64() {
674 if let Some(pos_str) = key.strip_prefix("Fans Speed ")
676 && let Ok(pos) = pos_str.parse::<i16>()
677 {
678 fans.push(FanData {
679 position: pos,
680 rpm: Some(AngularVelocity::from_rpm(num)),
681 });
682 }
683 }
684 }
685 }
686
687 fans
688 }
689}
690
691impl GetPsuFans for PowerPlayV1 {}
692
693impl GetFluidTemperature for PowerPlayV1 {}
694
695impl GetWattage for PowerPlayV1 {
696 fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
697 data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
698 }
699}
700
701impl GetWattageLimit for PowerPlayV1 {}
702
703impl GetLightFlashing for PowerPlayV1 {
704 fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
705 data.extract::<bool>(DataField::LightFlashing)
706 }
707}
708
709impl GetMessages for PowerPlayV1 {}
710
711impl GetUptime for PowerPlayV1 {
712 fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
713 data.extract::<u64>(DataField::Uptime)
714 .map(Duration::from_secs)
715 }
716}
717
718impl GetIsMining for PowerPlayV1 {
719 fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
720 data.extract::<String>(DataField::IsMining)
721 .map(|state| state != "Idling")
722 .unwrap_or(false)
723 }
724}
725
726impl GetPools for PowerPlayV1 {
727 fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
728 let mut pools_vec: Vec<PoolData> = Vec::new();
729
730 if let Some(configs) = data
731 .get(&DataField::Pools)
732 .and_then(|v| v.pointer("/StratumConfigs"))
733 .and_then(|v| v.as_array())
734 {
735 for (idx, config) in configs.iter().enumerate() {
736 let url = config.get("pool").and_then(|v| v.as_str()).and_then(|s| {
737 if s.is_empty() {
738 None
739 } else {
740 Some(PoolURL::from(s.to_string()))
741 }
742 });
743 let user = config
744 .get("login")
745 .and_then(|v| v.as_str())
746 .map(String::from);
747 pools_vec.push(PoolData {
748 position: Some(idx as u16),
749 url,
750 accepted_shares: None,
751 rejected_shares: None,
752 active: Some(false),
753 alive: None,
754 user,
755 });
756 }
757 }
758
759 if let Some(stratum) = data
760 .get(&DataField::Pools)
761 .and_then(|v| v.pointer("/Stratum"))
762 .and_then(|v| v.as_object())
763 {
764 for pool in pools_vec.iter_mut() {
765 if pool.position
766 == stratum
767 .get("Config Id")
768 .and_then(|v| v.as_u64().map(|v| v as u16))
769 {
770 pool.active = Some(true);
771 pool.alive = stratum.get("IsPoolConnected").and_then(|v| v.as_bool());
772 pool.user = stratum
773 .get("Current User")
774 .and_then(|v| v.as_str())
775 .map(String::from);
776 pool.url = stratum
777 .get("Current Pool")
778 .and_then(|v| v.as_str())
779 .and_then(|s| {
780 if s.is_empty() {
781 None
782 } else {
783 Some(PoolURL::from(s.to_string()))
784 }
785 });
786
787 if let Some(session) = data
789 .get(&DataField::Pools)
790 .and_then(|v| v.pointer("/Session"))
791 .and_then(|v| v.as_object())
792 {
793 pool.accepted_shares = session.get("Accepted").and_then(|v| v.as_u64());
794 pool.rejected_shares = session.get("Rejected").and_then(|v| v.as_u64());
795 }
796 }
797 }
798 }
799
800 pools_vec
801 }
802}
803
804#[async_trait]
805impl SetFaultLight for PowerPlayV1 {
806 #[allow(unused_variables)]
807 async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
808 self.web
809 .send_command(
810 "identify",
811 false,
812 Some(json!({ "param": fault })),
813 Method::POST,
814 )
815 .await
816 .map(|v| v.get("result").and_then(Value::as_bool).unwrap_or(false))
817 }
818}
819
820#[async_trait]
821impl SetPowerLimit for PowerPlayV1 {
822 #[allow(unused_variables)]
823 async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
824 anyhow::bail!("Unsupported command");
825 }
826}
827
828#[async_trait]
829impl Restart for PowerPlayV1 {
830 async fn restart(&self) -> anyhow::Result<bool> {
831 self.web
832 .send_command("reboot", false, Some(json!({"param": "0"})), Method::POST)
833 .await
834 .map(|v| v.get("result").and_then(Value::as_bool).unwrap_or(false))
835 }
836}
837
838#[async_trait]
839impl Pause for PowerPlayV1 {
840 #[allow(unused_variables)]
841 async fn pause(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
842 self.web
843 .send_command("miner", false, Some(json!({"param": "Stop"})), Method::POST)
844 .await
845 .map(|v| v.get("result").and_then(Value::as_bool).unwrap_or(false))
846 }
847}
848
849#[async_trait]
850impl Resume for PowerPlayV1 {
851 #[allow(unused_variables)]
852 async fn resume(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
853 self.web
854 .send_command(
855 "miner",
856 false,
857 Some(json!({ "param": "Autostart" })),
858 Method::POST,
859 )
860 .await
861 .map(|v| v.get("result").and_then(Value::as_bool).unwrap_or(false))
862 }
863}
864
865#[cfg(test)]
866mod tests {
867 use super::*;
868 use crate::data::device::models::antminer::AntMinerModel::S19XP;
869 use crate::test::api::MockAPIClient;
870 use crate::test::json::epic::v1::*;
871 use anyhow;
872
873 #[tokio::test]
874 async fn parse_data_test_antminer_s19xp() -> anyhow::Result<()> {
875 let miner = PowerPlayV1::new(IpAddr::from([127, 0, 0, 1]), MinerModel::AntMiner(S19XP));
876
877 let mut results = HashMap::new();
878
879 let commands = vec![
880 ("summary", SUMMARY),
881 ("capabilities", CAPABILITIES),
882 ("temps", TEMPS),
883 ("network", NETWORK),
884 ("clocks", CHIP_CLOCKS),
885 ("temps/chip", CHIP_TEMPS),
886 ("voltages", CHIP_VOLTAGES),
887 ("hashrate", CHIP_HASHRATES),
888 ];
889
890 for (command, data) in commands {
891 let cmd: MinerCommand = MinerCommand::WebAPI {
892 command,
893 parameters: None,
894 };
895 results.insert(cmd, Value::from_str(data)?);
896 }
897
898 let mock_api = MockAPIClient::new(results);
899
900 let mut collector = DataCollector::new_with_client(&miner, &mock_api);
901 let data = collector.collect_all().await;
902
903 let miner_data = miner.parse_data(data);
904
905 assert_eq!(miner_data.uptime, Some(Duration::from_secs(23170)));
906 assert_eq!(miner_data.wattage, Some(Power::from_watts(2166.6174)));
907 assert_eq!(miner_data.hashboards.len(), 3);
908 assert_eq!(miner_data.hashboards[0].active, Some(false));
909 assert_eq!(miner_data.hashboards[1].chips.len(), 110);
910 assert_eq!(
911 miner_data.hashboards[1].chips[69].hashrate,
912 Some(HashRate {
913 value: 305937.8,
914 unit: HashRateUnit::MegaHash,
915 algo: String::from("SHA256"),
916 })
917 );
918 assert_eq!(
919 miner_data.hashboards[2].chips[72].hashrate,
920 Some(HashRate {
921 value: 487695.28,
922 unit: HashRateUnit::MegaHash,
923 algo: String::from("SHA256"),
924 })
925 );
926
927 Ok(())
928 }
929}