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