asic_rs/miners/backends/antminer/v2020/
mod.rs

1use crate::data::board::BoardData;
2use crate::data::device::{
3    DeviceInfo, HashAlgorithm, MinerControlBoard, MinerFirmware, MinerMake, MinerModel,
4};
5use crate::data::fan::FanData;
6use crate::data::hashrate::{HashRate, HashRateUnit};
7use crate::data::message::{MessageSeverity, MinerMessage};
8use crate::data::pool::{PoolData, PoolURL};
9use crate::miners::backends::traits::*;
10use crate::miners::commands::MinerCommand;
11use crate::miners::data::{
12    DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer,
13};
14use anyhow;
15use async_trait::async_trait;
16use macaddr::MacAddr;
17use measurements::{AngularVelocity, Frequency, Power, Temperature};
18use serde_json::{Value, json};
19use std::collections::HashMap;
20use std::fmt::Display;
21use std::net::IpAddr;
22use std::str::FromStr;
23use std::time::Duration;
24
25use rpc::AntMinerRPCAPI;
26use web::AntMinerWebAPI;
27
28mod rpc;
29mod web;
30
31#[derive(Debug)]
32pub struct AntMinerV2020 {
33    pub ip: IpAddr,
34    pub rpc: AntMinerRPCAPI,
35    pub web: AntMinerWebAPI,
36    pub device_info: DeviceInfo,
37}
38
39enum MinerMode {
40    Sleep,
41    Low,
42    Normal,
43    High,
44}
45
46impl Display for MinerMode {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        let str = match self {
49            MinerMode::Normal => "0".to_string(),
50            MinerMode::Sleep => "1".to_string(),
51            MinerMode::Low => "3".to_string(),
52            _ => "0".to_string(),
53        };
54        write!(f, "{}", str)
55    }
56}
57
58impl AntMinerV2020 {
59    pub fn new(ip: IpAddr, model: MinerModel) -> Self {
60        AntMinerV2020 {
61            ip,
62            rpc: AntMinerRPCAPI::new(ip),
63            web: AntMinerWebAPI::new(ip),
64            device_info: DeviceInfo::new(
65                MinerMake::AntMiner,
66                model,
67                MinerFirmware::Stock,
68                HashAlgorithm::SHA256,
69            ),
70        }
71    }
72
73    pub fn with_auth(
74        ip: IpAddr,
75        model: MinerModel,
76        firmware: MinerFirmware,
77        username: String,
78        password: String,
79    ) -> Self {
80        AntMinerV2020 {
81            ip,
82            rpc: AntMinerRPCAPI::new(ip),
83            web: AntMinerWebAPI::with_auth(ip, username, password),
84            device_info: DeviceInfo::new(
85                MinerMake::AntMiner,
86                model,
87                firmware,
88                HashAlgorithm::SHA256,
89            ),
90        }
91    }
92
93    fn parse_temp_string(temp_str: &str) -> Option<Temperature> {
94        let temps: Vec<f64> = temp_str
95            .split('-')
96            .filter_map(|s| s.parse().ok())
97            .filter(|&temp| temp > 0.0)
98            .collect();
99
100        if !temps.is_empty() {
101            let avg = temps.iter().sum::<f64>() / temps.len() as f64;
102            Some(Temperature::from_celsius(avg))
103        } else {
104            None
105        }
106    }
107
108    fn _calculate_average_temp_s21_hyd(chain: &Value) -> Option<Temperature> {
109        let mut temps = Vec::new();
110
111        if let Some(temp_pic) = chain.get("temp_pic").and_then(|v| v.as_array()) {
112            for i in 1..=3 {
113                if let Some(temp) = temp_pic.get(i).and_then(|v| v.as_f64())
114                    && temp != 0.0
115                {
116                    temps.push(temp);
117                }
118            }
119        }
120
121        if let Some(temp_pcb) = chain.get("temp_pcb").and_then(|v| v.as_array()) {
122            if let Some(temp) = temp_pcb.get(1).and_then(|v| v.as_f64())
123                && temp != 0.0
124            {
125                temps.push(temp);
126            }
127            if let Some(temp) = temp_pcb.get(3).and_then(|v| v.as_f64())
128                && temp != 0.0
129            {
130                temps.push(temp);
131            }
132        }
133
134        if !temps.is_empty() {
135            let avg = temps.iter().sum::<f64>() / temps.len() as f64;
136            Some(Temperature::from_celsius(avg))
137        } else {
138            None
139        }
140    }
141
142    fn _calculate_average_temp_pcb(chain: &Value) -> Option<Temperature> {
143        if let Some(temp_pcb) = chain.get("temp_pcb").and_then(|v| v.as_array()) {
144            let temps: Vec<f64> = temp_pcb
145                .iter()
146                .filter_map(|v| v.as_f64())
147                .filter(|&temp| temp != 0.0)
148                .collect();
149
150            if !temps.is_empty() {
151                let avg = temps.iter().sum::<f64>() / temps.len() as f64;
152                Some(Temperature::from_celsius(avg))
153            } else {
154                None
155            }
156        } else {
157            None
158        }
159    }
160
161    fn _calculate_average_temp_chip(chain: &Value) -> Option<Temperature> {
162        if let Some(temp_chip) = chain.get("temp_chip").and_then(|v| v.as_array()) {
163            let temps: Vec<f64> = temp_chip
164                .iter()
165                .filter_map(|v| v.as_f64())
166                .filter(|&temp| temp != 0.0)
167                .collect();
168
169            if !temps.is_empty() {
170                let avg = temps.iter().sum::<f64>() / temps.len() as f64;
171                Some(Temperature::from_celsius(avg))
172            } else {
173                None
174            }
175        } else {
176            None
177        }
178    }
179}
180
181#[async_trait]
182impl APIClient for AntMinerV2020 {
183    async fn get_api_result(&self, command: &MinerCommand) -> anyhow::Result<Value> {
184        match command {
185            MinerCommand::RPC { .. } => self.rpc.get_api_result(command).await,
186            MinerCommand::WebAPI { .. } => self.web.get_api_result(command).await,
187            _ => Err(anyhow::anyhow!("Unsupported command type for Antminer API")),
188        }
189    }
190}
191
192impl GetDataLocations for AntMinerV2020 {
193    fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
194        const RPC_VERSION: MinerCommand = MinerCommand::RPC {
195            command: "version",
196            parameters: None,
197        };
198
199        const RPC_STATS: MinerCommand = MinerCommand::RPC {
200            command: "stats",
201            parameters: None,
202        };
203
204        const RPC_SUMMARY: MinerCommand = MinerCommand::RPC {
205            command: "summary",
206            parameters: None,
207        };
208
209        const RPC_POOLS: MinerCommand = MinerCommand::RPC {
210            command: "pools",
211            parameters: None,
212        };
213
214        const WEB_SYSTEM_INFO: MinerCommand = MinerCommand::WebAPI {
215            command: "get_system_info",
216            parameters: None,
217        };
218
219        const WEB_BLINK_STATUS: MinerCommand = MinerCommand::WebAPI {
220            command: "get_blink_status",
221            parameters: None,
222        };
223
224        const WEB_MINER_CONF: MinerCommand = MinerCommand::WebAPI {
225            command: "get_miner_conf",
226            parameters: None,
227        };
228
229        const WEB_SUMMARY: MinerCommand = MinerCommand::WebAPI {
230            command: "summary",
231            parameters: None,
232        };
233
234        const WEB_MINER_TYPE: MinerCommand = MinerCommand::WebAPI {
235            command: "miner_type",
236            parameters: None,
237        };
238
239        match data_field {
240            DataField::Mac => vec![(
241                WEB_SYSTEM_INFO,
242                DataExtractor {
243                    func: get_by_pointer,
244                    key: Some("/macaddr"),
245                    tag: None,
246                },
247            )],
248            DataField::ApiVersion => vec![(
249                RPC_VERSION,
250                DataExtractor {
251                    func: get_by_pointer,
252                    key: Some("/VERSION/0/API"),
253                    tag: None,
254                },
255            )],
256            DataField::FirmwareVersion => vec![(
257                RPC_VERSION,
258                DataExtractor {
259                    func: get_by_pointer,
260                    key: Some("/VERSION/0/CompileTime"),
261                    tag: None,
262                },
263            )],
264            DataField::Hostname => vec![(
265                WEB_SYSTEM_INFO,
266                DataExtractor {
267                    func: get_by_pointer,
268                    key: Some("/hostname"),
269                    tag: None,
270                },
271            )],
272            DataField::ControlBoardVersion => vec![(
273                WEB_MINER_TYPE,
274                DataExtractor {
275                    func: get_by_pointer,
276                    key: Some("/subtype"),
277                    tag: None,
278                },
279            )],
280            DataField::Hashrate => vec![(
281                RPC_SUMMARY,
282                DataExtractor {
283                    func: get_by_pointer,
284                    key: Some("/SUMMARY/0/GHS 5s"),
285                    tag: None,
286                },
287            )],
288            DataField::ExpectedHashrate => vec![(
289                RPC_STATS,
290                DataExtractor {
291                    func: get_by_pointer,
292                    key: Some("/STATS/1/total_rateideal"),
293                    tag: None,
294                },
295            )],
296            DataField::Fans => vec![(
297                RPC_STATS,
298                DataExtractor {
299                    func: get_by_pointer,
300                    key: Some("/STATS/1"),
301                    tag: None,
302                },
303            )],
304            DataField::Hashboards => vec![(
305                RPC_STATS,
306                DataExtractor {
307                    func: get_by_pointer,
308                    key: Some("/STATS/1"),
309                    tag: None,
310                },
311            )],
312            DataField::LightFlashing => vec![(
313                WEB_BLINK_STATUS,
314                DataExtractor {
315                    func: get_by_pointer,
316                    key: Some("/blink"),
317                    tag: None,
318                },
319            )],
320            DataField::IsMining => vec![(
321                WEB_MINER_CONF,
322                DataExtractor {
323                    func: get_by_pointer,
324                    key: Some("/bitmain-work-mode"),
325                    tag: None,
326                },
327            )],
328            DataField::Uptime => vec![(
329                RPC_STATS,
330                DataExtractor {
331                    func: get_by_pointer,
332                    key: Some("/STATS/1/Elapsed"),
333                    tag: None,
334                },
335            )],
336            DataField::Pools => vec![(
337                RPC_POOLS,
338                DataExtractor {
339                    func: get_by_pointer,
340                    key: Some("/POOLS"),
341                    tag: None,
342                },
343            )],
344            DataField::Wattage => vec![(
345                RPC_STATS,
346                DataExtractor {
347                    func: get_by_pointer,
348                    key: Some("/STATS/1"),
349                    tag: None,
350                },
351            )],
352            DataField::SerialNumber => vec![
353                (
354                    WEB_SYSTEM_INFO,
355                    DataExtractor {
356                        func: get_by_pointer,
357                        key: Some("/serial_no"), // Cant find on 2022 firmware, does exist on 2025 firmware for XP
358                        tag: None,
359                    },
360                ),
361                (
362                    WEB_SYSTEM_INFO,
363                    DataExtractor {
364                        func: get_by_pointer,
365                        key: Some("/serinum"), // exist on 2025 firmware for s21
366                        tag: None,
367                    },
368                ),
369            ],
370            DataField::Messages => vec![(
371                WEB_SUMMARY,
372                DataExtractor {
373                    func: get_by_pointer,
374                    key: Some("/SUMMARY/0/status"),
375                    tag: None,
376                },
377            )],
378            _ => vec![],
379        }
380    }
381}
382
383impl GetIP for AntMinerV2020 {
384    fn get_ip(&self) -> IpAddr {
385        self.ip
386    }
387}
388
389impl GetDeviceInfo for AntMinerV2020 {
390    fn get_device_info(&self) -> DeviceInfo {
391        self.device_info
392    }
393}
394
395impl CollectData for AntMinerV2020 {
396    fn get_collector(&self) -> DataCollector<'_> {
397        DataCollector::new(self)
398    }
399}
400
401impl GetMAC for AntMinerV2020 {
402    fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
403        data.extract::<String>(DataField::Mac)
404            .and_then(|s| MacAddr::from_str(&s).ok())
405    }
406}
407
408impl GetHostname for AntMinerV2020 {
409    fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
410        data.extract::<String>(DataField::Hostname)
411    }
412}
413
414impl GetApiVersion for AntMinerV2020 {
415    fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
416        data.extract::<String>(DataField::ApiVersion)
417    }
418}
419
420impl GetFirmwareVersion for AntMinerV2020 {
421    fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
422        data.extract::<String>(DataField::FirmwareVersion)
423    }
424}
425
426impl GetHashboards for AntMinerV2020 {
427    fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
428        let mut hashboards: Vec<BoardData> = Vec::new();
429        let board_count = self.device_info.hardware.boards.unwrap_or(3);
430
431        for idx in 0..board_count {
432            hashboards.push(BoardData {
433                hashrate: None,
434                position: idx,
435                expected_hashrate: None,
436                board_temperature: None,
437                intake_temperature: None,
438                outlet_temperature: None,
439                expected_chips: self.device_info.hardware.chips,
440                working_chips: None,
441                serial_number: None,
442                chips: vec![],
443                voltage: None,
444                frequency: None,
445                tuned: Some(false),
446                active: Some(false),
447            });
448        }
449
450        if let Some(stats_data) = data.get(&DataField::Hashboards) {
451            for idx in 1..=board_count {
452                let board_idx = (idx - 1) as usize;
453                if board_idx >= hashboards.len() {
454                    break;
455                }
456
457                if let Some(hashrate) = stats_data
458                    .get(format!("chain_rate{}", idx))
459                    .and_then(|v| v.as_str())
460                    .and_then(|s| s.parse::<f64>().ok())
461                    .map(|f| {
462                        HashRate {
463                            value: f,
464                            unit: HashRateUnit::GigaHash,
465                            algo: String::from("SHA256"),
466                        }
467                        .as_unit(HashRateUnit::TeraHash)
468                    })
469                {
470                    hashboards[board_idx].hashrate = Some(hashrate);
471                }
472
473                if let Some(working_chips) = stats_data
474                    .get(format!("chain_acn{}", idx))
475                    .and_then(|v| v.as_u64())
476                    .map(|u| u as u16)
477                {
478                    hashboards[board_idx].working_chips = Some(working_chips);
479                }
480
481                if let Some(board_temp) = stats_data
482                    .get(format!("temp_pcb{}", idx))
483                    .and_then(|v| v.as_str())
484                    .and_then(Self::parse_temp_string)
485                {
486                    hashboards[board_idx].board_temperature = Some(board_temp);
487                }
488
489                if let Some(frequency) = stats_data
490                    .get(format!("freq{}", idx))
491                    .and_then(|v| v.as_u64())
492                    .map(|f| Frequency::from_megahertz(f as f64))
493                {
494                    hashboards[board_idx].frequency = Some(frequency);
495                }
496
497                let has_hashrate = hashboards[board_idx]
498                    .hashrate
499                    .as_ref()
500                    .map(|h| h.value > 0.0)
501                    .unwrap_or(false);
502                let has_chips = hashboards[board_idx]
503                    .working_chips
504                    .map(|chips| chips > 0)
505                    .unwrap_or(false);
506
507                hashboards[board_idx].active = Some(has_hashrate || has_chips);
508                hashboards[board_idx].tuned = Some(has_hashrate || has_chips);
509            }
510        }
511
512        hashboards
513    }
514}
515
516impl GetHashrate for AntMinerV2020 {
517    fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
518        data.extract_map::<f64, _>(DataField::Hashrate, |f| {
519            HashRate {
520                value: f,
521                unit: HashRateUnit::GigaHash,
522                algo: String::from("SHA256"),
523            }
524            .as_unit(HashRateUnit::TeraHash)
525        })
526    }
527}
528
529impl GetExpectedHashrate for AntMinerV2020 {
530    fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
531        data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| {
532            HashRate {
533                value: f,
534                unit: HashRateUnit::GigaHash,
535                algo: String::from("SHA256"),
536            }
537            .as_unit(HashRateUnit::TeraHash)
538        })
539    }
540}
541
542impl GetFans for AntMinerV2020 {
543    fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
544        let mut fans: Vec<FanData> = Vec::new();
545
546        if let Some(stats_data) = data.get(&DataField::Fans) {
547            for i in 1..=self.device_info.hardware.fans.unwrap_or(4) {
548                if let Some(fan_speed) =
549                    stats_data.get(format!("fan{}", i)).and_then(|v| v.as_f64())
550                    && fan_speed > 0.0
551                {
552                    fans.push(FanData {
553                        position: (i - 1) as i16,
554                        rpm: Some(AngularVelocity::from_rpm(fan_speed)),
555                    });
556                }
557            }
558        }
559
560        fans
561    }
562}
563
564impl GetLightFlashing for AntMinerV2020 {
565    fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
566        data.extract::<bool>(DataField::LightFlashing).or_else(|| {
567            data.extract::<String>(DataField::LightFlashing)
568                .map(|s| s.to_lowercase() == "true" || s == "1")
569        })
570    }
571}
572
573impl GetUptime for AntMinerV2020 {
574    fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
575        data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
576    }
577}
578
579impl GetIsMining for AntMinerV2020 {
580    fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
581        data.extract::<String>(DataField::IsMining)
582            .map(|status| {
583                let status_lower = status.to_lowercase();
584                status_lower != "stopped" && status_lower != "idle" && status_lower != "sleep"
585            })
586            .or_else(|| data.extract::<f64>(DataField::Hashrate).map(|hr| hr > 0.0))
587            .unwrap_or(false)
588    }
589}
590
591impl GetPools for AntMinerV2020 {
592    fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
593        let mut pools: Vec<PoolData> = Vec::new();
594
595        if let Some(pools_data) = data.get(&DataField::Pools)
596            && let Some(pools_array) = pools_data.as_array()
597        {
598            for (idx, pool_info) in pools_array.iter().enumerate() {
599                let url = pool_info
600                    .get("URL")
601                    .and_then(|v| v.as_str())
602                    .map(|s| PoolURL::from(s.to_string()));
603
604                let user = pool_info
605                    .get("User")
606                    .and_then(|v| v.as_str())
607                    .map(String::from);
608
609                let alive = pool_info
610                    .get("Status")
611                    .and_then(|v| v.as_str())
612                    .map(|s| s == "Alive");
613
614                let active = pool_info.get("Stratum Active").and_then(|v| v.as_bool());
615
616                let accepted_shares = pool_info.get("Accepted").and_then(|v| v.as_u64());
617
618                let rejected_shares = pool_info.get("Rejected").and_then(|v| v.as_u64());
619
620                pools.push(PoolData {
621                    position: Some(idx as u16),
622                    url,
623                    accepted_shares,
624                    rejected_shares,
625                    active,
626                    alive,
627                    user,
628                });
629            }
630        }
631
632        pools
633    }
634}
635
636impl GetSerialNumber for AntMinerV2020 {
637    fn parse_serial_number(&self, data: &HashMap<DataField, Value>) -> Option<String> {
638        data.extract::<String>(DataField::SerialNumber)
639    }
640}
641
642impl GetControlBoardVersion for AntMinerV2020 {
643    fn parse_control_board_version(
644        &self,
645        data: &HashMap<DataField, Value>,
646    ) -> Option<MinerControlBoard> {
647        data.extract::<String>(DataField::ControlBoardVersion)
648            .and_then(|s| MinerControlBoard::from_str(s.split("_").collect::<Vec<&str>>()[0]).ok())
649    }
650}
651
652impl GetWattage for AntMinerV2020 {
653    fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
654        if let Some(stats_data) = data.get(&DataField::Wattage) {
655            if let Some(chain_power) = stats_data.get("chain_power")
656                && let Some(power_str) = chain_power.as_str()
657            {
658                // Parse "3250 W" format
659                if let Some(watt_part) = power_str.split_whitespace().next()
660                    && let Ok(watts) = watt_part.parse::<f64>()
661                {
662                    return Some(Power::from_watts(watts));
663                }
664            }
665
666            if let Some(power) = stats_data
667                .get("power")
668                .or_else(|| stats_data.get("Power"))
669                .and_then(|v| v.as_f64())
670            {
671                return Some(Power::from_watts(power));
672            }
673        }
674        None
675    }
676}
677
678impl GetWattageLimit for AntMinerV2020 {}
679
680impl GetFluidTemperature for AntMinerV2020 {
681    fn parse_fluid_temperature(&self, data: &HashMap<DataField, Value>) -> Option<Temperature> {
682        // For S21+ Hyd models, use inlet/outlet temperature average
683        if self.device_info.model.to_string().contains("S21+ Hyd")
684            && let Some(hashboards_data) = data.get(&DataField::Hashboards)
685            && let Some(chains) = hashboards_data.as_array()
686        {
687            let mut temps = Vec::new();
688
689            for chain in chains {
690                if let Some(temp_pcb) = chain.get("temp_pcb").and_then(|v| v.as_array()) {
691                    // Inlet temp (index 0) and outlet temp (index 2)
692                    if let Some(inlet) = temp_pcb.first().and_then(|v| v.as_f64())
693                        && inlet != 0.0
694                    {
695                        temps.push(inlet);
696                    }
697                    if let Some(outlet) = temp_pcb.get(2).and_then(|v| v.as_f64())
698                        && outlet != 0.0
699                    {
700                        temps.push(outlet);
701                    }
702                }
703            }
704
705            if !temps.is_empty() {
706                let avg = temps.iter().sum::<f64>() / temps.len() as f64;
707                return Some(Temperature::from_celsius(avg));
708            }
709        }
710        None
711    }
712}
713
714impl GetPsuFans for AntMinerV2020 {}
715
716impl GetMessages for AntMinerV2020 {
717    fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
718        let mut messages = Vec::new();
719
720        if let Some(status_data) = data.get(&DataField::Messages)
721            && let Some(status_array) = status_data.as_array()
722        {
723            for (idx, item) in status_array.iter().enumerate() {
724                if let Some(status) = item.get("status").and_then(|v| v.as_str())
725                    && status != "s"
726                {
727                    // 's' means success/ok
728                    let message_text = item
729                        .get("msg")
730                        .and_then(|v| v.as_str())
731                        .unwrap_or("Unknown error")
732                        .to_string();
733
734                    let severity = match status.to_lowercase().as_str() {
735                        "e" => MessageSeverity::Error,
736                        "w" => MessageSeverity::Warning,
737                        _ => MessageSeverity::Info,
738                    };
739
740                    messages.push(MinerMessage::new(0, idx as u64, message_text, severity));
741                }
742            }
743        }
744
745        messages
746    }
747}
748
749#[async_trait]
750impl SetFaultLight for AntMinerV2020 {
751    #[allow(unused_variables)]
752    async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
753        Ok(self.web.blink(fault).await.is_ok())
754    }
755}
756
757#[async_trait]
758impl SetPowerLimit for AntMinerV2020 {
759    #[allow(unused_variables)]
760    async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
761        anyhow::bail!("Unsupported command");
762    }
763}
764
765#[async_trait]
766impl Restart for AntMinerV2020 {
767    async fn restart(&self) -> anyhow::Result<bool> {
768        Ok(self.web.reboot().await.is_ok())
769    }
770}
771
772#[async_trait]
773impl Pause for AntMinerV2020 {
774    #[allow(unused_variables)]
775    async fn pause(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
776        Ok(self
777            .web
778            .set_miner_conf(json!({"miner-mode": MinerMode::Sleep.to_string()}))
779            .await
780            .is_ok())
781    }
782}
783
784#[async_trait]
785impl Resume for AntMinerV2020 {
786    #[allow(unused_variables)]
787    async fn resume(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
788        Ok(self
789            .web
790            .set_miner_conf(json!({"miner-mode": MinerMode::Normal.to_string()}))
791            .await
792            .is_ok())
793    }
794}
795
796#[cfg(test)]
797mod tests {
798    use super::*;
799    use crate::data::device::models::antminer::AntMinerModel;
800    use crate::test::api::MockAPIClient;
801    use crate::test::json::bmminer::antminer_modern::{
802        AM_DEVS, AM_POOLS, AM_STATS, AM_SUMMARY, AM_VERSION,
803    };
804
805    #[tokio::test]
806    async fn test_antminer() {
807        let miner = AntMinerV2020::new(
808            IpAddr::from([127, 0, 0, 1]),
809            MinerModel::AntMiner(AntMinerModel::S19Pro),
810        );
811
812        let mut results = HashMap::new();
813
814        let stats_cmd = MinerCommand::RPC {
815            command: "stats",
816            parameters: None,
817        };
818
819        let version_cmd = MinerCommand::RPC {
820            command: "version",
821            parameters: None,
822        };
823
824        let summary_cmd = MinerCommand::RPC {
825            command: "summary",
826            parameters: None,
827        };
828
829        let devs_cmd = MinerCommand::RPC {
830            command: "devs",
831            parameters: None,
832        };
833
834        let pools_cmd = MinerCommand::RPC {
835            command: "pools",
836            parameters: None,
837        };
838
839        results.insert(stats_cmd, Value::from_str(AM_STATS).unwrap());
840        results.insert(version_cmd, Value::from_str(AM_VERSION).unwrap());
841        results.insert(summary_cmd, Value::from_str(AM_SUMMARY).unwrap());
842        results.insert(devs_cmd, Value::from_str(AM_DEVS).unwrap());
843        results.insert(pools_cmd, Value::from_str(AM_POOLS).unwrap());
844
845        let mock_api = MockAPIClient::new(results);
846
847        let mut collector = DataCollector::new_with_client(&miner, &mock_api);
848        let data = collector.collect_all().await;
849
850        let miner_data = miner.parse_data(data);
851
852        assert_eq!(miner_data.ip.to_string(), "127.0.0.1".to_owned());
853        assert_eq!(miner_data.hashboards.len(), 3);
854        assert_eq!(miner_data.light_flashing, None);
855        assert_eq!(miner_data.fans.len(), 4);
856        assert_eq!(
857            miner_data.expected_hashrate.unwrap(),
858            HashRate {
859                value: 110.0,
860                unit: HashRateUnit::TeraHash,
861                algo: "SHA256".to_string(),
862            }
863        );
864        assert_eq!(
865            miner_data.hashrate.unwrap(),
866            HashRate {
867                value: 110.56689,
868                unit: HashRateUnit::TeraHash,
869                algo: "SHA256".to_string(),
870            }
871        );
872    }
873}