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