asic_rs/miners/backends/whatsminer/v2/
mod.rs

1use crate::data::board::BoardData;
2use crate::data::device::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
3use crate::data::device::{MinerControlBoard, MinerMake};
4use crate::data::fan::FanData;
5use crate::data::hashrate::{HashRate, HashRateUnit};
6use crate::data::pool::{PoolData, PoolURL};
7use crate::miners::backends::traits::*;
8use crate::miners::commands::MinerCommand;
9use crate::miners::data::{
10    DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer,
11};
12use anyhow;
13use async_trait::async_trait;
14use chrono::{DateTime, NaiveDateTime, Utc};
15use macaddr::MacAddr;
16use measurements::{AngularVelocity, Frequency, Power, Temperature};
17use serde_json::{Value, json};
18use std::collections::HashMap;
19use std::net::IpAddr;
20use std::str::FromStr;
21use std::time::Duration;
22
23use crate::data::message::{MessageSeverity, MinerMessage};
24use rpc::WhatsMinerRPCAPI;
25
26mod rpc;
27
28#[derive(Debug)]
29pub struct WhatsMinerV2 {
30    pub ip: IpAddr,
31    pub rpc: WhatsMinerRPCAPI,
32    pub device_info: DeviceInfo,
33}
34
35impl WhatsMinerV2 {
36    pub fn new(ip: IpAddr, model: MinerModel) -> Self {
37        WhatsMinerV2 {
38            ip,
39            rpc: WhatsMinerRPCAPI::new(ip, None),
40            device_info: DeviceInfo::new(
41                MinerMake::WhatsMiner,
42                model,
43                MinerFirmware::Stock,
44                HashAlgorithm::SHA256,
45            ),
46        }
47    }
48}
49
50#[async_trait]
51impl APIClient for WhatsMinerV2 {
52    async fn get_api_result(&self, command: &MinerCommand) -> anyhow::Result<Value> {
53        match command {
54            MinerCommand::RPC { .. } => self.rpc.get_api_result(command).await,
55            _ => Err(anyhow::anyhow!(
56                "Unsupported command type for WhatsMiner API"
57            )),
58        }
59    }
60}
61
62impl GetDataLocations for WhatsMinerV2 {
63    fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
64        const RPC_GET_MINER_INFO: MinerCommand = MinerCommand::RPC {
65            command: "get_miner_info",
66            parameters: None,
67        };
68        const RPC_SUMMARY: MinerCommand = MinerCommand::RPC {
69            command: "summary",
70            parameters: None,
71        };
72        const RPC_DEVS: MinerCommand = MinerCommand::RPC {
73            command: "devs",
74            parameters: None,
75        };
76        const RPC_POOLS: MinerCommand = MinerCommand::RPC {
77            command: "pools",
78            parameters: None,
79        };
80        const RPC_STATUS: MinerCommand = MinerCommand::RPC {
81            command: "status",
82            parameters: None,
83        };
84        const RPC_GET_VERSION: MinerCommand = MinerCommand::RPC {
85            command: "get_version",
86            parameters: None,
87        };
88        const RPC_GET_PSU: MinerCommand = MinerCommand::RPC {
89            command: "get_psu",
90            parameters: None,
91        };
92        const RPC_GET_ERROR_CODE: MinerCommand = MinerCommand::RPC {
93            command: "get_error_code",
94            parameters: None,
95        };
96
97        match data_field {
98            DataField::Mac => vec![(
99                RPC_GET_MINER_INFO,
100                DataExtractor {
101                    func: get_by_pointer,
102                    key: Some("/Msg/mac"),
103                    tag: None,
104                },
105            )],
106            DataField::ApiVersion => vec![(
107                RPC_GET_VERSION,
108                DataExtractor {
109                    func: get_by_pointer,
110                    key: Some("/Msg/api_ver"),
111                    tag: None,
112                },
113            )],
114            DataField::FirmwareVersion => vec![(
115                RPC_GET_VERSION,
116                DataExtractor {
117                    func: get_by_pointer,
118                    key: Some("/Msg/fw_ver"),
119                    tag: None,
120                },
121            )],
122            DataField::ControlBoardVersion => vec![(
123                RPC_GET_VERSION,
124                DataExtractor {
125                    func: get_by_pointer,
126                    key: Some("/Msg/platform"),
127                    tag: None,
128                },
129            )],
130            DataField::Hostname => vec![(
131                RPC_GET_MINER_INFO,
132                DataExtractor {
133                    func: get_by_pointer,
134                    key: Some("/Msg/hostname"),
135                    tag: None,
136                },
137            )],
138            DataField::LightFlashing => vec![(
139                RPC_GET_MINER_INFO,
140                DataExtractor {
141                    func: get_by_pointer,
142                    key: Some("/Msg/ledstat"),
143                    tag: None,
144                },
145            )],
146            DataField::WattageLimit => vec![(
147                RPC_SUMMARY,
148                DataExtractor {
149                    func: get_by_pointer,
150                    key: Some("/SUMMARY/0/Power Limit"),
151                    tag: None,
152                },
153            )],
154            DataField::Fans => vec![(
155                RPC_SUMMARY,
156                DataExtractor {
157                    func: get_by_pointer,
158                    key: Some("/SUMMARY/0"),
159                    tag: None,
160                },
161            )],
162            DataField::PsuFans => vec![(
163                RPC_GET_PSU,
164                DataExtractor {
165                    func: get_by_pointer,
166                    key: Some("/Msg/fan_speed"),
167                    tag: None,
168                },
169            )],
170            DataField::Hashboards => vec![(
171                RPC_DEVS,
172                DataExtractor {
173                    func: get_by_pointer,
174                    key: Some(""),
175                    tag: None,
176                },
177            )],
178            DataField::Pools => vec![(
179                RPC_POOLS,
180                DataExtractor {
181                    func: get_by_pointer,
182                    key: Some("/POOLS"),
183                    tag: None,
184                },
185            )],
186            DataField::Uptime => vec![(
187                RPC_SUMMARY,
188                DataExtractor {
189                    func: get_by_pointer,
190                    key: Some("/SUMMARY/0/Elapsed"),
191                    tag: None,
192                },
193            )],
194            DataField::Wattage => vec![(
195                RPC_SUMMARY,
196                DataExtractor {
197                    func: get_by_pointer,
198                    key: Some("/SUMMARY/0/Power"),
199                    tag: None,
200                },
201            )],
202            DataField::Hashrate => vec![(
203                RPC_SUMMARY,
204                DataExtractor {
205                    func: get_by_pointer,
206                    key: Some("/SUMMARY/0/HS RT"),
207                    tag: None,
208                },
209            )],
210            DataField::ExpectedHashrate => vec![(
211                RPC_SUMMARY,
212                DataExtractor {
213                    func: get_by_pointer,
214                    key: Some("/SUMMARY/0/Factory GHS"),
215                    tag: None,
216                },
217            )],
218            DataField::FluidTemperature => vec![(
219                RPC_SUMMARY,
220                DataExtractor {
221                    func: get_by_pointer,
222                    key: Some("/SUMMARY/0/Env Temp"),
223                    tag: None,
224                },
225            )],
226            DataField::IsMining => vec![(
227                RPC_STATUS,
228                DataExtractor {
229                    func: get_by_pointer,
230                    key: Some("/SUMMARY/0/btmineroff"),
231                    tag: None,
232                },
233            )],
234            DataField::Messages => vec![(
235                RPC_GET_ERROR_CODE,
236                DataExtractor {
237                    func: get_by_pointer,
238                    key: Some("/Msg/error_code"),
239                    tag: None,
240                },
241            )],
242            _ => vec![],
243        }
244    }
245}
246
247impl GetIP for WhatsMinerV2 {
248    fn get_ip(&self) -> IpAddr {
249        self.ip
250    }
251}
252impl GetDeviceInfo for WhatsMinerV2 {
253    fn get_device_info(&self) -> DeviceInfo {
254        self.device_info
255    }
256}
257
258impl CollectData for WhatsMinerV2 {
259    fn get_collector(&self) -> DataCollector<'_> {
260        DataCollector::new(self)
261    }
262}
263
264impl GetMAC for WhatsMinerV2 {
265    fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
266        data.extract::<String>(DataField::Mac)
267            .and_then(|s| MacAddr::from_str(&s).ok())
268    }
269}
270
271impl GetSerialNumber for WhatsMinerV2 {}
272impl GetHostname for WhatsMinerV2 {
273    fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
274        data.extract::<String>(DataField::Hostname)
275    }
276}
277impl GetApiVersion for WhatsMinerV2 {
278    fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
279        data.extract::<String>(DataField::ApiVersion)
280    }
281}
282impl GetFirmwareVersion for WhatsMinerV2 {
283    fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
284        data.extract::<String>(DataField::FirmwareVersion)
285    }
286}
287impl GetControlBoardVersion for WhatsMinerV2 {
288    fn parse_control_board_version(
289        &self,
290        data: &HashMap<DataField, Value>,
291    ) -> Option<MinerControlBoard> {
292        data.extract::<String>(DataField::ControlBoardVersion)
293            .and_then(|s| MinerControlBoard::from_str(&s).ok())
294    }
295}
296impl GetHashboards for WhatsMinerV2 {
297    fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
298        let mut hashboards: Vec<BoardData> = Vec::new();
299        let board_count = self.device_info.hardware.boards.unwrap_or(3);
300        let hashboard_data = data.get(&DataField::Hashboards);
301
302        for idx in 0..board_count {
303            let hashrate = hashboard_data
304                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/MHS av")))
305                .and_then(|val| val.as_f64())
306                .map(|f| {
307                    HashRate {
308                        value: f,
309                        unit: HashRateUnit::MegaHash,
310                        algo: String::from("SHA256"),
311                    }
312                    .as_unit(HashRateUnit::TeraHash)
313                });
314            let expected_hashrate = hashboard_data
315                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Factory GHS")))
316                .and_then(|val| val.as_f64())
317                .map(|f| {
318                    HashRate {
319                        value: f,
320                        unit: HashRateUnit::GigaHash,
321                        algo: String::from("SHA256"),
322                    }
323                    .as_unit(HashRateUnit::TeraHash)
324                });
325            let board_temperature = hashboard_data
326                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Temperature")))
327                .and_then(|val| val.as_f64())
328                .map(Temperature::from_celsius);
329            let intake_temperature = hashboard_data
330                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Chip Temp Min")))
331                .and_then(|val| val.as_f64())
332                .map(Temperature::from_celsius);
333            let outlet_temperature = hashboard_data
334                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Chip Temp Max")))
335                .and_then(|val| val.as_f64())
336                .map(Temperature::from_celsius);
337            let serial_number = hashboard_data
338                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/PCB SN")))
339                .and_then(|val| val.as_str())
340                .map(String::from);
341            let working_chips = hashboard_data
342                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Effective Chips")))
343                .and_then(|val| val.as_u64())
344                .map(|u| u as u16);
345            let frequency = hashboard_data
346                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Frequency")))
347                .and_then(|val| val.as_f64())
348                .map(Frequency::from_megahertz);
349
350            let active = Some(hashrate.clone().map(|h| h.value).unwrap_or(0f64) > 0f64);
351            hashboards.push(BoardData {
352                hashrate,
353                position: idx,
354                expected_hashrate,
355                board_temperature,
356                intake_temperature,
357                outlet_temperature,
358                expected_chips: self.device_info.hardware.chips,
359                working_chips,
360                serial_number,
361                chips: vec![],
362                voltage: None, // TODO
363                frequency,
364                tuned: Some(true),
365                active,
366            });
367        }
368        hashboards
369    }
370}
371impl GetHashrate for WhatsMinerV2 {
372    fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
373        data.extract_map::<f64, _>(DataField::Hashrate, |f| {
374            HashRate {
375                value: f,
376                unit: HashRateUnit::MegaHash,
377                algo: String::from("SHA256"),
378            }
379            .as_unit(HashRateUnit::TeraHash)
380        })
381    }
382}
383impl GetExpectedHashrate for WhatsMinerV2 {
384    fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
385        data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| {
386            HashRate {
387                value: f,
388                unit: HashRateUnit::GigaHash,
389                algo: String::from("SHA256"),
390            }
391            .as_unit(HashRateUnit::TeraHash)
392        })
393    }
394}
395impl GetFans for WhatsMinerV2 {
396    fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
397        let mut fans: Vec<FanData> = Vec::new();
398        for (idx, direction) in ["In", "Out"].iter().enumerate() {
399            let fan = data.extract_nested_map::<f64, _>(
400                DataField::Fans,
401                &format!("Fan Speed {direction}"),
402                |rpm| FanData {
403                    position: idx as i16,
404                    rpm: Some(AngularVelocity::from_rpm(rpm)),
405                },
406            );
407            if let Some(f) = fan {
408                fans.push(f)
409            }
410        }
411        fans
412    }
413}
414impl GetPsuFans for WhatsMinerV2 {
415    fn parse_psu_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
416        let mut psu_fans: Vec<FanData> = Vec::new();
417
418        let psu_fan = data.extract_map::<String, _>(DataField::PsuFans, |rpm| FanData {
419            position: 0i16,
420            rpm: Some(AngularVelocity::from_rpm(rpm.parse().unwrap())),
421        });
422        if let Some(f) = psu_fan {
423            psu_fans.push(f)
424        }
425        psu_fans
426    }
427}
428impl GetFluidTemperature for WhatsMinerV2 {
429    fn parse_fluid_temperature(&self, data: &HashMap<DataField, Value>) -> Option<Temperature> {
430        data.extract_map::<f64, _>(DataField::FluidTemperature, Temperature::from_celsius)
431    }
432}
433impl GetWattage for WhatsMinerV2 {
434    fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
435        data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
436    }
437}
438impl GetWattageLimit for WhatsMinerV2 {
439    fn parse_wattage_limit(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
440        data.extract_map::<f64, _>(DataField::WattageLimit, Power::from_watts)
441    }
442}
443impl GetLightFlashing for WhatsMinerV2 {
444    fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
445        data.extract_map::<String, _>(DataField::LightFlashing, |l| l != "auto")
446    }
447}
448impl GetMessages for WhatsMinerV2 {
449    fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
450        let mut messages = Vec::new();
451
452        let errors_raw = data.get(&DataField::Messages);
453
454        if let Some(errors_response) = errors_raw {
455            for obj in errors_response.as_array().unwrap_or(&Vec::new()).iter() {
456                let object = obj.as_object();
457                if let Some(obj) = object {
458                    for (code, time) in obj.iter() {
459                        dbg!(time);
460                        let timestamp = NaiveDateTime::parse_from_str(
461                            time.as_str().unwrap(),
462                            "%Y-%m-%d %H:%M:%S",
463                        )
464                        .map(|t| DateTime::<Utc>::from_naive_utc_and_offset(t, Utc))
465                        .map(|dt| dt.timestamp_millis() as u32);
466
467                        dbg!(&timestamp);
468
469                        if let Ok(ts) = timestamp {
470                            messages.push(MinerMessage {
471                                timestamp: ts,
472                                code: code.parse::<u64>().unwrap_or(0),
473                                message: "".to_string(),
474                                severity: MessageSeverity::Error,
475                            })
476                        }
477                    }
478                }
479            }
480        }
481
482        messages
483    }
484}
485impl GetUptime for WhatsMinerV2 {
486    fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
487        data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
488    }
489}
490impl GetIsMining for WhatsMinerV2 {
491    fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
492        data.extract_map::<String, _>(DataField::IsMining, |l| l != "false")
493            .unwrap_or(true)
494    }
495}
496impl GetPools for WhatsMinerV2 {
497    fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
498        let mut pools: Vec<PoolData> = Vec::new();
499        let pools_raw = data.get(&DataField::Pools);
500        if let Some(pools_response) = pools_raw {
501            for (idx, _) in pools_response
502                .as_array()
503                .unwrap_or(&Vec::new())
504                .iter()
505                .enumerate()
506            {
507                let user = pools_raw
508                    .and_then(|val| val.pointer(&format!("/{idx}/User")))
509                    .map(|val| String::from(val.as_str().unwrap_or("")));
510
511                let alive = pools_raw
512                    .and_then(|val| val.pointer(&format!("/{idx}/Status")))
513                    .map(|val| val.as_str())
514                    .map(|val| val == Some("Alive"));
515
516                let active = pools_raw
517                    .and_then(|val| val.pointer(&format!("/{idx}/Stratum Active")))
518                    .and_then(|val| val.as_bool());
519
520                let url = pools_raw
521                    .and_then(|val| val.pointer(&format!("/{idx}/URL")))
522                    .map(|val| PoolURL::from(String::from(val.as_str().unwrap_or(""))));
523
524                let accepted_shares = pools_raw
525                    .and_then(|val| val.pointer(&format!("/{idx}/Accepted")))
526                    .and_then(|val| val.as_u64());
527
528                let rejected_shares = pools_raw
529                    .and_then(|val| val.pointer(&format!("/{idx}/Rejected")))
530                    .and_then(|val| val.as_u64());
531
532                pools.push(PoolData {
533                    position: Some(idx as u16),
534                    url,
535                    accepted_shares,
536                    rejected_shares,
537                    active,
538                    alive,
539                    user,
540                });
541            }
542        }
543        pools
544    }
545}
546
547#[async_trait]
548impl SetFaultLight for WhatsMinerV2 {
549    async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
550        let parameters = match fault {
551            false => Some(
552                json!({"auto": true, "color": "red", "period": 60, "duration": 20, "start": 0}),
553            ),
554            true => Some(
555                json!({"auto": false, "color": "red", "period": 60, "duration": 20, "start": 0}),
556            ),
557        };
558
559        let data = self.rpc.send_command("set_led", true, parameters).await;
560        Ok(data.is_ok())
561    }
562}
563
564#[async_trait]
565impl SetPowerLimit for WhatsMinerV2 {
566    async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
567        let parameters = Some(json!({"power_limit": limit.as_watts().to_string()}));
568        let data = self
569            .rpc
570            .send_command("adjust_power_limit", true, parameters)
571            .await;
572        Ok(data.is_ok())
573    }
574}
575
576#[async_trait]
577impl Restart for WhatsMinerV2 {
578    async fn restart(&self) -> anyhow::Result<bool> {
579        let data = self.rpc.send_command("reboot", true, None).await;
580        Ok(data.is_ok())
581    }
582}
583
584#[async_trait]
585impl Pause for WhatsMinerV2 {
586    #[allow(unused_variables)]
587    async fn pause(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
588        let data = self
589            .rpc
590            .send_command("power_off", true, Some(json!({"respbefore": "true"}))) // Has to be string for some reason
591            .await;
592        Ok(data.is_ok())
593    }
594}
595
596#[async_trait]
597impl Resume for WhatsMinerV2 {
598    #[allow(unused_variables)]
599    async fn resume(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
600        let data = self.rpc.send_command("power_on", true, None).await;
601        Ok(data.is_ok())
602    }
603}