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