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

1use anyhow::{Result, anyhow};
2use async_trait::async_trait;
3use macaddr::MacAddr;
4use measurements::{AngularVelocity, Frequency, Power, Temperature};
5use serde_json::{Value, json};
6use std::collections::HashMap;
7use std::net::IpAddr;
8use std::str::FromStr;
9use std::time::Duration;
10
11use crate::data::board::BoardData;
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_key,
21    get_by_pointer,
22};
23
24pub(crate) use rpc::WhatsMinerRPCAPI;
25
26mod rpc;
27
28#[derive(Debug)]
29pub struct WhatsMinerV3 {
30    pub ip: IpAddr,
31    pub rpc: WhatsMinerRPCAPI,
32    pub device_info: DeviceInfo,
33}
34
35impl WhatsMinerV3 {
36    pub fn new(ip: IpAddr, model: MinerModel) -> Self {
37        WhatsMinerV3 {
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 WhatsMinerV3 {
52    async fn get_api_result(&self, command: &MinerCommand) -> Result<Value> {
53        match command {
54            MinerCommand::RPC { .. } => self.rpc.get_api_result(command).await,
55            _ => Err(anyhow!("Unsupported command type for WhatsMiner API")),
56        }
57    }
58}
59
60impl GetDataLocations for WhatsMinerV3 {
61    fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
62        let get_device_info_cmd: MinerCommand = MinerCommand::RPC {
63            command: "get.device.info",
64            parameters: None,
65        };
66        let get_miner_status_summary_cmd: MinerCommand = MinerCommand::RPC {
67            command: "get.miner.status",
68            parameters: Some(json!("summary")),
69        };
70        let get_miner_status_pools_cmd: MinerCommand = MinerCommand::RPC {
71            command: "get.miner.status",
72            parameters: Some(json!("pools")),
73        };
74        let get_miner_status_edevs_cmd: MinerCommand = MinerCommand::RPC {
75            command: "get.miner.status",
76            parameters: Some(json!("edevs")),
77        };
78
79        match data_field {
80            DataField::Mac => vec![(
81                get_device_info_cmd,
82                DataExtractor {
83                    func: get_by_pointer,
84                    key: Some("/msg/network/mac"),
85                    tag: None,
86                },
87            )],
88            DataField::ApiVersion => vec![(
89                get_device_info_cmd,
90                DataExtractor {
91                    func: get_by_pointer,
92                    key: Some("/msg/system/api"),
93                    tag: None,
94                },
95            )],
96            DataField::FirmwareVersion => vec![(
97                get_device_info_cmd,
98                DataExtractor {
99                    func: get_by_pointer,
100                    key: Some("/msg/system/fwversion"),
101                    tag: None,
102                },
103            )],
104            DataField::ControlBoardVersion => vec![(
105                get_device_info_cmd,
106                DataExtractor {
107                    func: get_by_pointer,
108                    key: Some("/msg/system/platform"),
109                    tag: None,
110                },
111            )],
112            DataField::SerialNumber => vec![(
113                get_device_info_cmd,
114                DataExtractor {
115                    func: get_by_pointer,
116                    key: Some("/msg/miner/miner-sn"),
117                    tag: None,
118                },
119            )],
120            DataField::Hostname => vec![(
121                get_device_info_cmd,
122                DataExtractor {
123                    func: get_by_pointer,
124                    key: Some("/msg/network/hostname"),
125                    tag: None,
126                },
127            )],
128            DataField::LightFlashing => vec![(
129                get_device_info_cmd,
130                DataExtractor {
131                    func: get_by_pointer,
132                    key: Some("/msg/system/ledstatus"),
133                    tag: None,
134                },
135            )],
136            DataField::WattageLimit => vec![(
137                get_device_info_cmd,
138                DataExtractor {
139                    func: get_by_pointer,
140                    key: Some("/msg/miner/power-limit-set"),
141                    tag: None,
142                },
143            )],
144            DataField::Fans => vec![(
145                get_miner_status_summary_cmd,
146                DataExtractor {
147                    func: get_by_pointer,
148                    key: Some("/msg/summary"),
149                    tag: None,
150                },
151            )],
152            DataField::PsuFans => vec![(
153                get_device_info_cmd,
154                DataExtractor {
155                    func: get_by_pointer,
156                    key: Some("/msg/power/fanspeed"),
157                    tag: None,
158                },
159            )],
160            DataField::Hashboards => vec![
161                (
162                    get_device_info_cmd,
163                    DataExtractor {
164                        func: get_by_pointer,
165                        key: Some("/msg/miner"),
166                        tag: None,
167                    },
168                ),
169                (
170                    get_miner_status_edevs_cmd,
171                    DataExtractor {
172                        func: get_by_key,
173                        key: Some("msg"),
174                        tag: None,
175                    },
176                ),
177            ],
178            DataField::Pools => vec![(
179                get_miner_status_pools_cmd,
180                DataExtractor {
181                    func: get_by_pointer,
182                    key: Some("/msg/pools"),
183                    tag: None,
184                },
185            )],
186            DataField::Uptime => vec![(
187                get_miner_status_summary_cmd,
188                DataExtractor {
189                    func: get_by_pointer,
190                    key: Some("/msg/summary/elapsed"),
191                    tag: None,
192                },
193            )],
194            DataField::Wattage => vec![(
195                get_miner_status_summary_cmd,
196                DataExtractor {
197                    func: get_by_pointer,
198                    key: Some("/msg/summary/power-realtime"),
199                    tag: None,
200                },
201            )],
202            DataField::Hashrate => vec![(
203                get_miner_status_summary_cmd,
204                DataExtractor {
205                    func: get_by_pointer,
206                    key: Some("/msg/summary/hash-realtime"),
207                    tag: None,
208                },
209            )],
210            DataField::ExpectedHashrate => vec![(
211                get_miner_status_summary_cmd,
212                DataExtractor {
213                    func: get_by_pointer,
214                    key: Some("/msg/summary/factory-hash"),
215                    tag: None,
216                },
217            )],
218            DataField::FluidTemperature => vec![(
219                get_miner_status_summary_cmd,
220                DataExtractor {
221                    func: get_by_pointer,
222                    key: Some("/msg/summary/environment-temperature"),
223                    tag: None,
224                },
225            )],
226            _ => vec![],
227        }
228    }
229}
230
231impl GetIP for WhatsMinerV3 {
232    fn get_ip(&self) -> IpAddr {
233        self.ip
234    }
235}
236impl GetDeviceInfo for WhatsMinerV3 {
237    fn get_device_info(&self) -> DeviceInfo {
238        self.device_info
239    }
240}
241
242impl CollectData for WhatsMinerV3 {
243    fn get_collector(&self) -> DataCollector<'_> {
244        DataCollector::new(self)
245    }
246}
247
248impl GetMAC for WhatsMinerV3 {
249    fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
250        data.extract::<String>(DataField::Mac)
251            .and_then(|s| MacAddr::from_str(&s).ok())
252    }
253}
254
255impl GetSerialNumber for WhatsMinerV3 {}
256impl GetHostname for WhatsMinerV3 {
257    fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
258        data.extract::<String>(DataField::Hostname)
259    }
260}
261impl GetApiVersion for WhatsMinerV3 {
262    fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
263        data.extract::<String>(DataField::ApiVersion)
264    }
265}
266impl GetFirmwareVersion for WhatsMinerV3 {
267    fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
268        data.extract::<String>(DataField::FirmwareVersion)
269    }
270}
271impl GetControlBoardVersion for WhatsMinerV3 {
272    fn parse_control_board_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
273        data.extract::<String>(DataField::ControlBoardVersion)
274    }
275}
276impl GetHashboards for WhatsMinerV3 {
277    fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
278        let mut hashboards: Vec<BoardData> = Vec::new();
279        let board_count = self.device_info.hardware.boards.unwrap_or(3);
280        for idx in 0..board_count {
281            let hashrate = data
282                .get(&DataField::Hashboards)
283                .and_then(|val| val.pointer(&format!("/edevs/{idx}/hash-average")))
284                .and_then(|val| val.as_f64())
285                .map(|f| HashRate {
286                    value: f,
287                    unit: HashRateUnit::TeraHash,
288                    algo: String::from("SHA256"),
289                });
290            let expected_hashrate = data
291                .get(&DataField::Hashboards)
292                .and_then(|val| val.pointer(&format!("/edevs/{idx}/factory-hash")))
293                .and_then(|val| val.as_f64())
294                .map(|f| HashRate {
295                    value: f,
296                    unit: HashRateUnit::TeraHash,
297                    algo: String::from("SHA256"),
298                });
299            let board_temperature = data
300                .get(&DataField::Hashboards)
301                .and_then(|val| val.pointer(&format!("/edevs/{idx}/chip-temp-min")))
302                .and_then(|val| val.as_f64())
303                .map(Temperature::from_celsius);
304            let intake_temperature = data
305                .get(&DataField::Hashboards)
306                .and_then(|val| val.pointer(&format!("/edevs/{idx}/chip-temp-min")))
307                .and_then(|val| val.as_f64())
308                .map(Temperature::from_celsius);
309            let outlet_temperature = data
310                .get(&DataField::Hashboards)
311                .and_then(|val| val.pointer(&format!("/edevs/{idx}/chip-temp-max")))
312                .and_then(|val| val.as_f64())
313                .map(Temperature::from_celsius);
314            let serial_number =
315                data.extract_nested::<String>(DataField::Hashboards, &format!("pcbsn{idx}"));
316
317            let working_chips = data
318                .get(&DataField::Hashboards)
319                .and_then(|val| val.pointer(&format!("/edevs/{idx}/effective-chips")))
320                .and_then(|val| val.as_u64())
321                .map(|u| u as u16);
322            let frequency = data
323                .get(&DataField::Hashboards)
324                .and_then(|val| val.pointer(&format!("/edevs/{idx}/freq")))
325                .and_then(|val| val.as_f64())
326                .map(Frequency::from_megahertz);
327
328            let active = Some(hashrate.clone().map(|h| h.value).unwrap_or(0f64) > 0f64);
329            hashboards.push(BoardData {
330                hashrate,
331                position: idx,
332                expected_hashrate,
333                board_temperature,
334                intake_temperature,
335                outlet_temperature,
336                expected_chips: self.device_info.hardware.chips,
337                working_chips,
338                serial_number,
339                chips: vec![],
340                voltage: None, // TODO
341                frequency,
342                tuned: Some(true),
343                active,
344            });
345        }
346        hashboards
347    }
348}
349impl GetHashrate for WhatsMinerV3 {
350    fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
351        data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
352            value: f,
353            unit: HashRateUnit::TeraHash,
354            algo: String::from("SHA256"),
355        })
356    }
357}
358impl GetExpectedHashrate for WhatsMinerV3 {
359    fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
360        data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| HashRate {
361            value: f,
362            unit: HashRateUnit::TeraHash,
363            algo: String::from("SHA256"),
364        })
365    }
366}
367impl GetFans for WhatsMinerV3 {
368    fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
369        let mut fans: Vec<FanData> = Vec::new();
370        for (idx, direction) in ["in", "out"].iter().enumerate() {
371            let fan = data.extract_nested_map::<f64, _>(
372                DataField::Fans,
373                &format!("fan-speed-{direction}"),
374                |rpm| FanData {
375                    position: idx as i16,
376                    rpm: Some(AngularVelocity::from_rpm(rpm)),
377                },
378            );
379            if let Some(fan_data) = fan {
380                fans.push(fan_data);
381            }
382        }
383        fans
384    }
385}
386impl GetPsuFans for WhatsMinerV3 {
387    fn parse_psu_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
388        let mut psu_fans: Vec<FanData> = Vec::new();
389
390        let psu_fan = data.extract_map::<f64, _>(DataField::PsuFans, |rpm| FanData {
391            position: 0i16,
392            rpm: Some(AngularVelocity::from_rpm(rpm)),
393        });
394        if let Some(fan_data) = psu_fan {
395            psu_fans.push(fan_data);
396        }
397        psu_fans
398    }
399}
400impl GetFluidTemperature for WhatsMinerV3 {
401    fn parse_fluid_temperature(&self, data: &HashMap<DataField, Value>) -> Option<Temperature> {
402        data.extract_map::<f64, _>(DataField::FluidTemperature, Temperature::from_celsius)
403    }
404}
405impl GetWattage for WhatsMinerV3 {
406    fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
407        data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
408    }
409}
410impl GetWattageLimit for WhatsMinerV3 {
411    fn parse_wattage_limit(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
412        data.extract_map::<String, _>(DataField::WattageLimit, |p| p.parse::<f64>().ok())?
413            .map(Power::from_watts)
414    }
415}
416impl GetLightFlashing for WhatsMinerV3 {
417    fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
418        data.extract_map::<String, _>(DataField::LightFlashing, |l| l != "auto")
419    }
420}
421impl GetMessages for WhatsMinerV3 {}
422impl GetUptime for WhatsMinerV3 {
423    fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
424        data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
425    }
426}
427impl GetIsMining for WhatsMinerV3 {}
428impl GetPools for WhatsMinerV3 {
429    fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
430        let mut pools: Vec<PoolData> = Vec::new();
431        let pools_raw = data.get(&DataField::Pools);
432        if let Some(pools_response) = pools_raw {
433            for (idx, _) in pools_response
434                .as_array()
435                .unwrap_or(&Vec::new())
436                .iter()
437                .enumerate()
438            {
439                let user = data
440                    .get(&DataField::Pools)
441                    .and_then(|val| val.pointer(&format!("/{idx}/account")))
442                    .map(|val| String::from(val.as_str().unwrap_or("")));
443
444                let alive = data
445                    .get(&DataField::Pools)
446                    .and_then(|val| val.pointer(&format!("/{idx}/status")))
447                    .map(|val| val.as_str())
448                    .map(|val| val == Some("alive"));
449
450                let active = data
451                    .get(&DataField::Pools)
452                    .and_then(|val| val.pointer(&format!("/{idx}/stratum-active")))
453                    .and_then(|val| val.as_bool());
454
455                let url = data
456                    .get(&DataField::Pools)
457                    .and_then(|val| val.pointer(&format!("/{idx}/url")))
458                    .map(|val| PoolURL::from(String::from(val.as_str().unwrap_or(""))));
459
460                pools.push(PoolData {
461                    position: Some(idx as u16),
462                    url,
463                    accepted_shares: None,
464                    rejected_shares: None,
465                    active,
466                    alive,
467                    user,
468                });
469            }
470        }
471        pools
472    }
473}