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, 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}