asic_rs/miners/backends/epic/v1/
mod.rs

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        // Create ChipData for each active board
376        for board in &mut hashboards {
377            board.expected_chips = self.device_info.hardware.chips;
378            // No need to add ChipData if we know the board is not active
379            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        //Capabilities Board Serial Numbers
396        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                // Since we only have an array with no index, it will only correspond to working boards, so search for first working board
403                // without serial and insert there
404                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; // Only assign to the first board without a serial number
410                    }
411                }
412            }
413        };
414
415        // Summary Data
416        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                            // Hashrate
425                            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                            // ExpectedHashrate
438                            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                            //Frequency
456                            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                            //Voltage
461                            if let Some(v) = board.get("Input Voltage").and_then(|v| v.as_f64()) {
462                                hashboard.voltage = Some(Voltage::from_volts(v));
463                            };
464                            //Board Temp
465                            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        //Temp Data
474        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                            // Outlet Temperature
483                            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        //Chip Temps
508        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        //Chip Voltages
535        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        //Chip Frequencies
562        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        //Chip Hashrate
589        //There should always be a hashrate, and if there is a hashrate its also working
590        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                        // Hashrate
635                        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                    // Extract the number from the key (e.g. "Fans Speed 3" -> 3)
675                    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                    // Get Stats
788                    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}