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

1use 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::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
13use crate::data::device::{MinerControlBoard, MinerMake};
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) -> 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 WhatsMinerV3 {
63    fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
64        const RPC_GET_DEVICE_INFO: MinerCommand = MinerCommand::RPC {
65            command: "get.device.info",
66            parameters: None,
67        };
68        let rpc_get_miner_status_summary: MinerCommand = MinerCommand::RPC {
69            command: "get.miner.status",
70            parameters: Some(json!("summary")),
71        };
72        let rpc_get_miner_status_pools: MinerCommand = MinerCommand::RPC {
73            command: "get.miner.status",
74            parameters: Some(json!("pools")),
75        };
76        let rpc_get_miner_status_edevs: MinerCommand = MinerCommand::RPC {
77            command: "get.miner.status",
78            parameters: Some(json!("edevs")),
79        };
80
81        match data_field {
82            DataField::Mac => vec![(
83                RPC_GET_DEVICE_INFO,
84                DataExtractor {
85                    func: get_by_pointer,
86                    key: Some("/msg/network/mac"),
87                    tag: None,
88                },
89            )],
90            DataField::ApiVersion => vec![(
91                RPC_GET_DEVICE_INFO,
92                DataExtractor {
93                    func: get_by_pointer,
94                    key: Some("/msg/system/api"),
95                    tag: None,
96                },
97            )],
98            DataField::FirmwareVersion => vec![(
99                RPC_GET_DEVICE_INFO,
100                DataExtractor {
101                    func: get_by_pointer,
102                    key: Some("/msg/system/fwversion"),
103                    tag: None,
104                },
105            )],
106            DataField::ControlBoardVersion => vec![(
107                RPC_GET_DEVICE_INFO,
108                DataExtractor {
109                    func: get_by_pointer,
110                    key: Some("/msg/system/platform"),
111                    tag: None,
112                },
113            )],
114            DataField::SerialNumber => vec![(
115                RPC_GET_DEVICE_INFO,
116                DataExtractor {
117                    func: get_by_pointer,
118                    key: Some("/msg/miner/miner-sn"),
119                    tag: None,
120                },
121            )],
122            DataField::Hostname => vec![(
123                RPC_GET_DEVICE_INFO,
124                DataExtractor {
125                    func: get_by_pointer,
126                    key: Some("/msg/network/hostname"),
127                    tag: None,
128                },
129            )],
130            DataField::LightFlashing => vec![(
131                RPC_GET_DEVICE_INFO,
132                DataExtractor {
133                    func: get_by_pointer,
134                    key: Some("/msg/system/ledstatus"),
135                    tag: None,
136                },
137            )],
138            DataField::WattageLimit => vec![(
139                RPC_GET_DEVICE_INFO,
140                DataExtractor {
141                    func: get_by_pointer,
142                    key: Some("/msg/miner/power-limit-set"),
143                    tag: None,
144                },
145            )],
146            DataField::Fans => vec![(
147                rpc_get_miner_status_summary,
148                DataExtractor {
149                    func: get_by_pointer,
150                    key: Some("/msg/summary"),
151                    tag: None,
152                },
153            )],
154            DataField::PsuFans => vec![(
155                RPC_GET_DEVICE_INFO,
156                DataExtractor {
157                    func: get_by_pointer,
158                    key: Some("/msg/power/fanspeed"),
159                    tag: None,
160                },
161            )],
162            DataField::Hashboards => vec![
163                (
164                    RPC_GET_DEVICE_INFO,
165                    DataExtractor {
166                        func: get_by_pointer,
167                        key: Some("/msg/miner"),
168                        tag: None,
169                    },
170                ),
171                (
172                    rpc_get_miner_status_edevs,
173                    DataExtractor {
174                        func: get_by_key,
175                        key: Some("msg"),
176                        tag: None,
177                    },
178                ),
179            ],
180            DataField::Pools => vec![(
181                rpc_get_miner_status_pools,
182                DataExtractor {
183                    func: get_by_pointer,
184                    key: Some("/msg/pools"),
185                    tag: None,
186                },
187            )],
188            DataField::Uptime => vec![(
189                rpc_get_miner_status_summary,
190                DataExtractor {
191                    func: get_by_pointer,
192                    key: Some("/msg/summary/elapsed"),
193                    tag: None,
194                },
195            )],
196            DataField::Wattage => vec![(
197                rpc_get_miner_status_summary,
198                DataExtractor {
199                    func: get_by_pointer,
200                    key: Some("/msg/summary/power-realtime"),
201                    tag: None,
202                },
203            )],
204            DataField::Hashrate => vec![(
205                rpc_get_miner_status_summary,
206                DataExtractor {
207                    func: get_by_pointer,
208                    key: Some("/msg/summary/hash-realtime"),
209                    tag: None,
210                },
211            )],
212            DataField::ExpectedHashrate => vec![(
213                rpc_get_miner_status_summary,
214                DataExtractor {
215                    func: get_by_pointer,
216                    key: Some("/msg/summary/factory-hash"),
217                    tag: None,
218                },
219            )],
220            DataField::FluidTemperature => vec![(
221                rpc_get_miner_status_summary,
222                DataExtractor {
223                    func: get_by_pointer,
224                    key: Some("/msg/summary/environment-temperature"),
225                    tag: None,
226                },
227            )],
228            _ => vec![],
229        }
230    }
231}
232
233impl GetIP for WhatsMinerV3 {
234    fn get_ip(&self) -> IpAddr {
235        self.ip
236    }
237}
238impl GetDeviceInfo for WhatsMinerV3 {
239    fn get_device_info(&self) -> DeviceInfo {
240        self.device_info
241    }
242}
243
244impl CollectData for WhatsMinerV3 {
245    fn get_collector(&self) -> DataCollector<'_> {
246        DataCollector::new(self)
247    }
248}
249
250impl GetMAC for WhatsMinerV3 {
251    fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
252        data.extract::<String>(DataField::Mac)
253            .and_then(|s| MacAddr::from_str(&s).ok())
254    }
255}
256
257impl GetSerialNumber for WhatsMinerV3 {}
258impl GetHostname for WhatsMinerV3 {
259    fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
260        data.extract::<String>(DataField::Hostname)
261    }
262}
263impl GetApiVersion for WhatsMinerV3 {
264    fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
265        data.extract::<String>(DataField::ApiVersion)
266    }
267}
268impl GetFirmwareVersion for WhatsMinerV3 {
269    fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
270        data.extract::<String>(DataField::FirmwareVersion)
271    }
272}
273impl GetControlBoardVersion for WhatsMinerV3 {
274    fn parse_control_board_version(
275        &self,
276        data: &HashMap<DataField, Value>,
277    ) -> Option<MinerControlBoard> {
278        data.extract::<String>(DataField::ControlBoardVersion)
279            .and_then(|s| MinerControlBoard::from_str(&s).ok())
280    }
281}
282impl GetHashboards for WhatsMinerV3 {
283    fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
284        let mut hashboards: Vec<BoardData> = Vec::new();
285        let board_count = self.device_info.hardware.boards.unwrap_or(3);
286        for idx in 0..board_count {
287            let hashrate = data
288                .get(&DataField::Hashboards)
289                .and_then(|val| val.pointer(&format!("/edevs/{idx}/hash-average")))
290                .and_then(|val| val.as_f64())
291                .map(|f| HashRate {
292                    value: f,
293                    unit: HashRateUnit::TeraHash,
294                    algo: String::from("SHA256"),
295                });
296            let expected_hashrate = data
297                .get(&DataField::Hashboards)
298                .and_then(|val| val.pointer(&format!("/edevs/{idx}/factory-hash")))
299                .and_then(|val| val.as_f64())
300                .map(|f| HashRate {
301                    value: f,
302                    unit: HashRateUnit::TeraHash,
303                    algo: String::from("SHA256"),
304                });
305            let board_temperature = data
306                .get(&DataField::Hashboards)
307                .and_then(|val| val.pointer(&format!("/edevs/{idx}/chip-temp-min")))
308                .and_then(|val| val.as_f64())
309                .map(Temperature::from_celsius);
310            let intake_temperature = data
311                .get(&DataField::Hashboards)
312                .and_then(|val| val.pointer(&format!("/edevs/{idx}/chip-temp-min")))
313                .and_then(|val| val.as_f64())
314                .map(Temperature::from_celsius);
315            let outlet_temperature = data
316                .get(&DataField::Hashboards)
317                .and_then(|val| val.pointer(&format!("/edevs/{idx}/chip-temp-max")))
318                .and_then(|val| val.as_f64())
319                .map(Temperature::from_celsius);
320            let serial_number =
321                data.extract_nested::<String>(DataField::Hashboards, &format!("pcbsn{idx}"));
322
323            let working_chips = data
324                .get(&DataField::Hashboards)
325                .and_then(|val| val.pointer(&format!("/edevs/{idx}/effective-chips")))
326                .and_then(|val| val.as_u64())
327                .map(|u| u as u16);
328            let frequency = data
329                .get(&DataField::Hashboards)
330                .and_then(|val| val.pointer(&format!("/edevs/{idx}/freq")))
331                .and_then(|val| val.as_f64())
332                .map(Frequency::from_megahertz);
333
334            let active = Some(hashrate.clone().map(|h| h.value).unwrap_or(0f64) > 0f64);
335            hashboards.push(BoardData {
336                hashrate,
337                position: idx,
338                expected_hashrate,
339                board_temperature,
340                intake_temperature,
341                outlet_temperature,
342                expected_chips: self.device_info.hardware.chips,
343                working_chips,
344                serial_number,
345                chips: vec![],
346                voltage: None, // TODO
347                frequency,
348                tuned: Some(true),
349                active,
350            });
351        }
352        hashboards
353    }
354}
355impl GetHashrate for WhatsMinerV3 {
356    fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
357        data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
358            value: f,
359            unit: HashRateUnit::TeraHash,
360            algo: String::from("SHA256"),
361        })
362    }
363}
364impl GetExpectedHashrate for WhatsMinerV3 {
365    fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
366        data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| HashRate {
367            value: f,
368            unit: HashRateUnit::TeraHash,
369            algo: String::from("SHA256"),
370        })
371    }
372}
373impl GetFans for WhatsMinerV3 {
374    fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
375        let mut fans: Vec<FanData> = Vec::new();
376        for (idx, direction) in ["in", "out"].iter().enumerate() {
377            let fan = data.extract_nested_map::<f64, _>(
378                DataField::Fans,
379                &format!("fan-speed-{direction}"),
380                |rpm| FanData {
381                    position: idx as i16,
382                    rpm: Some(AngularVelocity::from_rpm(rpm)),
383                },
384            );
385            if let Some(fan_data) = fan {
386                fans.push(fan_data);
387            }
388        }
389        fans
390    }
391}
392impl GetPsuFans for WhatsMinerV3 {
393    fn parse_psu_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
394        let mut psu_fans: Vec<FanData> = Vec::new();
395
396        let psu_fan = data.extract_map::<f64, _>(DataField::PsuFans, |rpm| FanData {
397            position: 0i16,
398            rpm: Some(AngularVelocity::from_rpm(rpm)),
399        });
400        if let Some(fan_data) = psu_fan {
401            psu_fans.push(fan_data);
402        }
403        psu_fans
404    }
405}
406impl GetFluidTemperature for WhatsMinerV3 {
407    fn parse_fluid_temperature(&self, data: &HashMap<DataField, Value>) -> Option<Temperature> {
408        data.extract_map::<f64, _>(DataField::FluidTemperature, Temperature::from_celsius)
409    }
410}
411impl GetWattage for WhatsMinerV3 {
412    fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
413        data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
414    }
415}
416impl GetWattageLimit for WhatsMinerV3 {
417    fn parse_wattage_limit(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
418        data.extract_map::<String, _>(DataField::WattageLimit, |p| p.parse::<f64>().ok())?
419            .map(Power::from_watts)
420    }
421}
422impl GetLightFlashing for WhatsMinerV3 {
423    fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
424        data.extract_map::<String, _>(DataField::LightFlashing, |l| l != "auto")
425    }
426}
427impl GetMessages for WhatsMinerV3 {}
428impl GetUptime for WhatsMinerV3 {
429    fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
430        data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
431    }
432}
433impl GetIsMining for WhatsMinerV3 {}
434impl GetPools for WhatsMinerV3 {
435    fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
436        let mut pools: Vec<PoolData> = Vec::new();
437        let pools_raw = data.get(&DataField::Pools);
438        if let Some(pools_response) = pools_raw {
439            for (idx, _) in pools_response
440                .as_array()
441                .unwrap_or(&Vec::new())
442                .iter()
443                .enumerate()
444            {
445                let user = data
446                    .get(&DataField::Pools)
447                    .and_then(|val| val.pointer(&format!("/{idx}/account")))
448                    .map(|val| String::from(val.as_str().unwrap_or("")));
449
450                let alive = data
451                    .get(&DataField::Pools)
452                    .and_then(|val| val.pointer(&format!("/{idx}/status")))
453                    .map(|val| val.as_str())
454                    .map(|val| val == Some("alive"));
455
456                let active = data
457                    .get(&DataField::Pools)
458                    .and_then(|val| val.pointer(&format!("/{idx}/stratum-active")))
459                    .and_then(|val| val.as_bool());
460
461                let url = data
462                    .get(&DataField::Pools)
463                    .and_then(|val| val.pointer(&format!("/{idx}/url")))
464                    .map(|val| PoolURL::from(String::from(val.as_str().unwrap_or(""))));
465
466                pools.push(PoolData {
467                    position: Some(idx as u16),
468                    url,
469                    accepted_shares: None,
470                    rejected_shares: None,
471                    active,
472                    alive,
473                    user,
474                });
475            }
476        }
477        pools
478    }
479}
480
481#[async_trait]
482impl SetFaultLight for WhatsMinerV3 {
483    async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
484        let parameters = match fault {
485            true => Some(json!("auto")),
486            false => Some(json!([{"color": "red", "period": 60, "duration": 20, "start": 0}])),
487        };
488
489        let data = self
490            .rpc
491            .send_command("set.system.led", true, parameters)
492            .await;
493
494        Ok(data.is_ok())
495    }
496}
497
498#[async_trait]
499impl SetPowerLimit for WhatsMinerV3 {
500    async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
501        let data = self
502            .rpc
503            .send_command("set.miner.power_limit", true, Some(json!(limit)))
504            .await;
505
506        Ok(data.is_ok())
507    }
508}
509
510#[async_trait]
511impl Restart for WhatsMinerV3 {
512    async fn restart(&self) -> anyhow::Result<bool> {
513        let data = self.rpc.send_command("set.system.reboot", true, None).await;
514
515        Ok(data.is_ok())
516    }
517}
518
519#[async_trait]
520impl Pause for WhatsMinerV3 {
521    async fn pause(&self, _at_time: Option<Duration>) -> anyhow::Result<bool> {
522        // might not work as intended, if issues are found then switch to "enable" + "disable"
523        // see api docs - https://apidoc.whatsminer.com/#api-Miner-btminer_service_set
524        let data = self
525            .rpc
526            .send_command("set.miner.service", true, Some(json!("stop")))
527            .await;
528
529        Ok(data.is_ok())
530    }
531}
532
533#[async_trait]
534impl Resume for WhatsMinerV3 {
535    async fn resume(&self, _at_time: Option<Duration>) -> anyhow::Result<bool> {
536        let data = self
537            .rpc
538            .send_command("set.miner.service", true, Some(json!("start")))
539            .await;
540
541        Ok(data.is_ok())
542    }
543}