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

1use anyhow::{Result, anyhow};
2use async_trait::async_trait;
3use macaddr::MacAddr;
4use measurements::{AngularVelocity, Frequency, Power, Temperature};
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;
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::message::{MessageSeverity, MinerMessage};
17use crate::data::pool::{PoolData, PoolURL};
18use crate::miners::backends::traits::*;
19use crate::miners::commands::MinerCommand;
20use crate::miners::data::{
21    DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer,
22};
23
24use rpc::WhatsMinerRPCAPI;
25
26mod rpc;
27
28#[derive(Debug)]
29pub struct WhatsMinerV1 {
30    pub ip: IpAddr,
31    pub rpc: WhatsMinerRPCAPI,
32    pub device_info: DeviceInfo,
33}
34
35impl WhatsMinerV1 {
36    pub fn new(ip: IpAddr, model: MinerModel) -> Self {
37        WhatsMinerV1 {
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 WhatsMinerV1 {
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 WhatsMinerV1 {
61    fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
62        let summary_cmd: MinerCommand = MinerCommand::RPC {
63            command: "summary",
64            parameters: None,
65        };
66        let devs_cmd: MinerCommand = MinerCommand::RPC {
67            command: "devs",
68            parameters: None,
69        };
70        let pools_cmd: MinerCommand = MinerCommand::RPC {
71            command: "pools",
72            parameters: None,
73        };
74        let status_cmd: MinerCommand = MinerCommand::RPC {
75            command: "status",
76            parameters: None,
77        };
78        let get_version_cmd: MinerCommand = MinerCommand::RPC {
79            command: "get_version",
80            parameters: None,
81        };
82        let get_psu_cmd: MinerCommand = MinerCommand::RPC {
83            command: "get_psu",
84            parameters: None,
85        };
86
87        match data_field {
88            DataField::Mac => vec![(
89                summary_cmd,
90                DataExtractor {
91                    func: get_by_pointer,
92                    key: Some("/SUMMARY/0/MAC"),
93                    tag: None,
94                },
95            )],
96            DataField::ApiVersion => vec![(
97                get_version_cmd,
98                DataExtractor {
99                    func: get_by_pointer,
100                    key: Some("/Msg/api_ver"),
101                    tag: None,
102                },
103            )],
104            DataField::FirmwareVersion => vec![(
105                get_version_cmd,
106                DataExtractor {
107                    func: get_by_pointer,
108                    key: Some("/Msg/fw_ver"),
109                    tag: None,
110                },
111            )],
112            DataField::ControlBoardVersion => vec![(
113                get_version_cmd,
114                DataExtractor {
115                    func: get_by_pointer,
116                    key: Some("/Msg/platform"),
117                    tag: None,
118                },
119            )],
120            DataField::WattageLimit => vec![(
121                summary_cmd,
122                DataExtractor {
123                    func: get_by_pointer,
124                    key: Some("/SUMMARY/0/Power Limit"),
125                    tag: None,
126                },
127            )],
128            DataField::Fans => vec![(
129                summary_cmd,
130                DataExtractor {
131                    func: get_by_pointer,
132                    key: Some("/SUMMARY/0"),
133                    tag: None,
134                },
135            )],
136            DataField::PsuFans => vec![(
137                get_psu_cmd,
138                DataExtractor {
139                    func: get_by_pointer,
140                    key: Some("/Msg/fan_speed"),
141                    tag: None,
142                },
143            )],
144            DataField::Hashboards => vec![(
145                devs_cmd,
146                DataExtractor {
147                    func: get_by_pointer,
148                    key: Some(""),
149                    tag: None,
150                },
151            )],
152            DataField::Pools => vec![(
153                pools_cmd,
154                DataExtractor {
155                    func: get_by_pointer,
156                    key: Some("/POOLS"),
157                    tag: None,
158                },
159            )],
160            DataField::Uptime => vec![(
161                summary_cmd,
162                DataExtractor {
163                    func: get_by_pointer,
164                    key: Some("/SUMMARY/0/Elapsed"),
165                    tag: None,
166                },
167            )],
168            DataField::Wattage => vec![(
169                summary_cmd,
170                DataExtractor {
171                    func: get_by_pointer,
172                    key: Some("/SUMMARY/0/Power"),
173                    tag: None,
174                },
175            )],
176            DataField::Hashrate => vec![(
177                summary_cmd,
178                DataExtractor {
179                    func: get_by_pointer,
180                    key: Some("/SUMMARY/0/HS RT"),
181                    tag: None,
182                },
183            )],
184            DataField::ExpectedHashrate => vec![(
185                summary_cmd,
186                DataExtractor {
187                    func: get_by_pointer,
188                    key: Some("/SUMMARY/0/Factory GHS"),
189                    tag: None,
190                },
191            )],
192            DataField::FluidTemperature => vec![(
193                summary_cmd,
194                DataExtractor {
195                    func: get_by_pointer,
196                    key: Some("/SUMMARY/0/Env Temp"),
197                    tag: None,
198                },
199            )],
200            DataField::IsMining => vec![(
201                status_cmd,
202                DataExtractor {
203                    func: get_by_pointer,
204                    key: Some("/SUMMARY/0/btmineroff"),
205                    tag: None,
206                },
207            )],
208            DataField::Messages => vec![(
209                summary_cmd,
210                DataExtractor {
211                    func: get_by_pointer,
212                    key: Some("/SUMMARY/0"),
213                    tag: None,
214                },
215            )],
216            _ => vec![],
217        }
218    }
219}
220
221impl GetIP for WhatsMinerV1 {
222    fn get_ip(&self) -> IpAddr {
223        self.ip
224    }
225}
226impl GetDeviceInfo for WhatsMinerV1 {
227    fn get_device_info(&self) -> DeviceInfo {
228        self.device_info
229    }
230}
231
232impl CollectData for WhatsMinerV1 {
233    fn get_collector(&self) -> DataCollector<'_> {
234        DataCollector::new(self)
235    }
236}
237
238impl GetMAC for WhatsMinerV1 {
239    fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
240        data.extract::<String>(DataField::Mac)
241            .and_then(|s| MacAddr::from_str(&s).ok())
242    }
243}
244
245impl GetSerialNumber for WhatsMinerV1 {}
246impl GetHostname for WhatsMinerV1 {}
247impl GetApiVersion for WhatsMinerV1 {
248    fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
249        data.extract::<String>(DataField::ApiVersion)
250    }
251}
252impl GetFirmwareVersion for WhatsMinerV1 {
253    fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
254        data.extract::<String>(DataField::FirmwareVersion)
255    }
256}
257impl GetControlBoardVersion for WhatsMinerV1 {
258    fn parse_control_board_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
259        data.extract::<String>(DataField::ControlBoardVersion)
260    }
261}
262impl GetHashboards for WhatsMinerV1 {
263    fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
264        let mut hashboards: Vec<BoardData> = Vec::new();
265        let board_count = self.device_info.hardware.boards.unwrap_or(3);
266        let hashboard_data = data.get(&DataField::Hashboards);
267
268        for idx in 0..board_count {
269            let hashrate = hashboard_data
270                .and_then(|val| val.pointer(&format!("/DEVS/{}/MHS av", idx)))
271                .and_then(|val| val.as_f64())
272                .map(|f| {
273                    HashRate {
274                        value: f,
275                        unit: HashRateUnit::MegaHash,
276                        algo: String::from("SHA256"),
277                    }
278                    .as_unit(HashRateUnit::TeraHash)
279                });
280            let expected_hashrate = hashboard_data
281                .and_then(|val| val.pointer(&format!("/DEVS/{}/Factory GHS", idx)))
282                .and_then(|val| val.as_f64())
283                .map(|f| {
284                    HashRate {
285                        value: f,
286                        unit: HashRateUnit::GigaHash,
287                        algo: String::from("SHA256"),
288                    }
289                    .as_unit(HashRateUnit::TeraHash)
290                });
291            let board_temperature = hashboard_data
292                .and_then(|val| val.pointer(&format!("/DEVS/{}/Temperature", idx)))
293                .and_then(|val| val.as_f64())
294                .map(Temperature::from_celsius);
295            let intake_temperature = hashboard_data
296                .and_then(|val| val.pointer(&format!("/DEVS/{}/Chip Temp Min", idx)))
297                .and_then(|val| val.as_f64())
298                .map(Temperature::from_celsius);
299            let outlet_temperature = hashboard_data
300                .and_then(|val| val.pointer(&format!("/DEVS/{}/Chip Temp Max", idx)))
301                .and_then(|val| val.as_f64())
302                .map(Temperature::from_celsius);
303            let serial_number = hashboard_data
304                .and_then(|val| val.pointer(&format!("/DEVS/{}/PCB SN", idx)))
305                .and_then(|val| val.as_str())
306                .map(String::from);
307            let working_chips = hashboard_data
308                .and_then(|val| val.pointer(&format!("/DEVS/{}/Effective Chips", idx)))
309                .and_then(|val| val.as_u64())
310                .map(|u| u as u16);
311            let frequency = hashboard_data
312                .and_then(|val| val.pointer(&format!("/DEVS/{}/Frequency", idx)))
313                .and_then(|val| val.as_f64())
314                .map(Frequency::from_megahertz);
315
316            let active = Some(hashrate.clone().map(|h| h.value).unwrap_or(0f64) > 0f64);
317            hashboards.push(BoardData {
318                hashrate,
319                position: idx,
320                expected_hashrate,
321                board_temperature,
322                intake_temperature,
323                outlet_temperature,
324                expected_chips: self.device_info.hardware.chips,
325                working_chips,
326                serial_number,
327                chips: vec![],
328                voltage: None, // TODO
329                frequency,
330                tuned: Some(true),
331                active,
332            });
333        }
334        hashboards
335    }
336}
337impl GetHashrate for WhatsMinerV1 {
338    fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
339        data.extract_map::<f64, _>(DataField::Hashrate, |f| {
340            HashRate {
341                value: f,
342                unit: HashRateUnit::MegaHash,
343                algo: String::from("SHA256"),
344            }
345            .as_unit(HashRateUnit::TeraHash)
346        })
347    }
348}
349impl GetExpectedHashrate for WhatsMinerV1 {
350    fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
351        data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| {
352            HashRate {
353                value: f,
354                unit: HashRateUnit::GigaHash,
355                algo: String::from("SHA256"),
356            }
357            .as_unit(HashRateUnit::TeraHash)
358        })
359    }
360}
361impl GetFans for WhatsMinerV1 {
362    fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
363        let mut fans: Vec<FanData> = Vec::new();
364        for (idx, direction) in ["In", "Out"].iter().enumerate() {
365            let fan = data.extract_nested_map::<f64, _>(
366                DataField::Fans,
367                &format!("Fan Speed {}", direction),
368                |rpm| FanData {
369                    position: idx as i16,
370                    rpm: Some(AngularVelocity::from_rpm(rpm)),
371                },
372            );
373            if let Some(f) = fan {
374                fans.push(f)
375            }
376        }
377        fans
378    }
379}
380impl GetPsuFans for WhatsMinerV1 {
381    fn parse_psu_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
382        let mut psu_fans: Vec<FanData> = Vec::new();
383
384        let psu_fan = data.extract_map::<String, _>(DataField::PsuFans, |rpm| FanData {
385            position: 0i16,
386            rpm: Some(AngularVelocity::from_rpm(rpm.parse().unwrap())),
387        });
388        if let Some(f) = psu_fan {
389            psu_fans.push(f)
390        }
391        psu_fans
392    }
393}
394impl GetFluidTemperature for WhatsMinerV1 {
395    fn parse_fluid_temperature(&self, data: &HashMap<DataField, Value>) -> Option<Temperature> {
396        data.extract_map::<f64, _>(DataField::FluidTemperature, Temperature::from_celsius)
397    }
398}
399impl GetWattage for WhatsMinerV1 {
400    fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
401        data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
402    }
403}
404impl GetWattageLimit for WhatsMinerV1 {
405    fn parse_wattage_limit(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
406        data.extract_map::<f64, _>(DataField::WattageLimit, Power::from_watts)
407    }
408}
409impl GetLightFlashing for WhatsMinerV1 {}
410impl GetMessages for WhatsMinerV1 {
411    fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
412        let mut messages = Vec::new();
413
414        let error_count = data
415            .get(&DataField::Messages)
416            .and_then(|val| {
417                val.pointer("/Error Code Count")
418                    .and_then(|val| val.as_u64())
419            })
420            .unwrap_or(0u64) as usize;
421        for idx in 0..error_count {
422            let e_code = data
423                .get(&DataField::Messages)
424                .and_then(|val| val.pointer(&format!("/Error Code {}", idx)))
425                .and_then(|val| val.as_u64());
426            if let Some(code) = e_code {
427                messages.push(MinerMessage::new(
428                    0,
429                    code,
430                    "".to_string(), // TODO: parse message from mapping
431                    MessageSeverity::Error,
432                ));
433            }
434        }
435
436        messages
437    }
438}
439impl GetUptime for WhatsMinerV1 {
440    fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
441        data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
442    }
443}
444impl GetIsMining for WhatsMinerV1 {
445    fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
446        data.extract_map::<String, _>(DataField::IsMining, |l| l != "false")
447            .unwrap_or(true)
448    }
449}
450impl GetPools for WhatsMinerV1 {
451    fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
452        let mut pools: Vec<PoolData> = Vec::new();
453        let pools_raw = data.get(&DataField::Pools);
454        if let Some(pools_response) = pools_raw {
455            for (idx, _) in pools_response
456                .as_array()
457                .unwrap_or(&Vec::new())
458                .iter()
459                .enumerate()
460            {
461                let user = pools_raw
462                    .and_then(|val| val.pointer(&format!("/{}/User", idx)))
463                    .map(|val| String::from(val.as_str().unwrap_or("")));
464
465                let alive = pools_raw
466                    .and_then(|val| val.pointer(&format!("/{}/Status", idx)))
467                    .map(|val| val.as_str())
468                    .map(|val| val == Some("Alive"));
469
470                let active = pools_raw
471                    .and_then(|val| val.pointer(&format!("/{}/Stratum Active", idx)))
472                    .and_then(|val| val.as_bool());
473
474                let url = pools_raw
475                    .and_then(|val| val.pointer(&format!("/{}/URL", idx)))
476                    .map(|val| PoolURL::from(String::from(val.as_str().unwrap_or(""))));
477
478                let accepted_shares = pools_raw
479                    .and_then(|val| val.pointer(&format!("/{}/Accepted", idx)))
480                    .and_then(|val| val.as_u64());
481
482                let rejected_shares = pools_raw
483                    .and_then(|val| val.pointer(&format!("/{}/Rejected", idx)))
484                    .and_then(|val| val.as_u64());
485
486                pools.push(PoolData {
487                    position: Some(idx as u16),
488                    url,
489                    accepted_shares,
490                    rejected_shares,
491                    active,
492                    alive,
493                    user,
494                });
495            }
496        }
497        pools
498    }
499}