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

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        // Create ChipData for each active board
349        for board in &mut hashboards {
350            board.expected_chips = self.device_info.hardware.chips;
351            // No need to add ChipData if we know the board is not active
352            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        //Capabilities Board Serial Numbers
369        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                // Since we only have an array with no index, it will only correspond to working boards, so search for first working board
376                // without serial and insert there
377                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; // Only assign to the first board without a serial number
383                    }
384                }
385            }
386        };
387
388        // Summary Data
389        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                            // Hashrate
398                            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                            // ExpectedHashrate
411                            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                            //Frequency
429                            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                            //Voltage
434                            if let Some(v) = board.get("Input Voltage").and_then(|v| v.as_f64()) {
435                                hashboard.voltage = Some(Voltage::from_volts(v));
436                            };
437                            //Board Temp
438                            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        //Temp Data
447        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                            // Outlet Temperature
456                            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        //Chip Temps
481        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        //Chip Voltages
508        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        //Chip Frequencies
535        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        //Chip Hashrate
562        //There should always be a hashrate, and if there is a hashrate its also working
563        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                        // Hashrate
608                        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                    // Extract the number from the key (e.g. "Fans Speed 3" -> 3)
648                    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                    // Get Stats
761                    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}