1use crate::data::board::BoardData;
2use crate::data::device::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
3use crate::data::device::{MinerControlBoard, MinerMake};
4use crate::data::fan::FanData;
5use crate::data::hashrate::{HashRate, HashRateUnit};
6use crate::data::pool::{PoolData, PoolURL};
7use crate::miners::backends::traits::*;
8use crate::miners::commands::MinerCommand;
9use crate::miners::data::{
10 DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer,
11};
12use anyhow;
13use async_trait::async_trait;
14use chrono::{DateTime, NaiveDateTime, Utc};
15use macaddr::MacAddr;
16use measurements::{AngularVelocity, Frequency, Power, Temperature};
17use serde_json::{Value, json};
18use std::collections::HashMap;
19use std::net::IpAddr;
20use std::str::FromStr;
21use std::time::Duration;
22
23use crate::data::message::{MessageSeverity, MinerMessage};
24use rpc::WhatsMinerRPCAPI;
25
26mod rpc;
27
28#[derive(Debug)]
29pub struct WhatsMinerV2 {
30 pub ip: IpAddr,
31 pub rpc: WhatsMinerRPCAPI,
32 pub device_info: DeviceInfo,
33}
34
35impl WhatsMinerV2 {
36 pub fn new(ip: IpAddr, model: MinerModel) -> Self {
37 WhatsMinerV2 {
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 WhatsMinerV2 {
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 WhatsMinerV2 {
63 fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
64 const RPC_GET_MINER_INFO: MinerCommand = MinerCommand::RPC {
65 command: "get_miner_info",
66 parameters: None,
67 };
68 const RPC_SUMMARY: MinerCommand = MinerCommand::RPC {
69 command: "summary",
70 parameters: None,
71 };
72 const RPC_DEVS: MinerCommand = MinerCommand::RPC {
73 command: "devs",
74 parameters: None,
75 };
76 const RPC_POOLS: MinerCommand = MinerCommand::RPC {
77 command: "pools",
78 parameters: None,
79 };
80 const RPC_STATUS: MinerCommand = MinerCommand::RPC {
81 command: "status",
82 parameters: None,
83 };
84 const RPC_GET_VERSION: MinerCommand = MinerCommand::RPC {
85 command: "get_version",
86 parameters: None,
87 };
88 const RPC_GET_PSU: MinerCommand = MinerCommand::RPC {
89 command: "get_psu",
90 parameters: None,
91 };
92 const RPC_GET_ERROR_CODE: MinerCommand = MinerCommand::RPC {
93 command: "get_error_code",
94 parameters: None,
95 };
96
97 match data_field {
98 DataField::Mac => vec![(
99 RPC_GET_MINER_INFO,
100 DataExtractor {
101 func: get_by_pointer,
102 key: Some("/Msg/mac"),
103 tag: None,
104 },
105 )],
106 DataField::ApiVersion => vec![(
107 RPC_GET_VERSION,
108 DataExtractor {
109 func: get_by_pointer,
110 key: Some("/Msg/api_ver"),
111 tag: None,
112 },
113 )],
114 DataField::FirmwareVersion => vec![(
115 RPC_GET_VERSION,
116 DataExtractor {
117 func: get_by_pointer,
118 key: Some("/Msg/fw_ver"),
119 tag: None,
120 },
121 )],
122 DataField::ControlBoardVersion => vec![(
123 RPC_GET_VERSION,
124 DataExtractor {
125 func: get_by_pointer,
126 key: Some("/Msg/platform"),
127 tag: None,
128 },
129 )],
130 DataField::Hostname => vec![(
131 RPC_GET_MINER_INFO,
132 DataExtractor {
133 func: get_by_pointer,
134 key: Some("/Msg/hostname"),
135 tag: None,
136 },
137 )],
138 DataField::LightFlashing => vec![(
139 RPC_GET_MINER_INFO,
140 DataExtractor {
141 func: get_by_pointer,
142 key: Some("/Msg/ledstat"),
143 tag: None,
144 },
145 )],
146 DataField::WattageLimit => vec![(
147 RPC_SUMMARY,
148 DataExtractor {
149 func: get_by_pointer,
150 key: Some("/SUMMARY/0/Power Limit"),
151 tag: None,
152 },
153 )],
154 DataField::Fans => vec![(
155 RPC_SUMMARY,
156 DataExtractor {
157 func: get_by_pointer,
158 key: Some("/SUMMARY/0"),
159 tag: None,
160 },
161 )],
162 DataField::PsuFans => vec![(
163 RPC_GET_PSU,
164 DataExtractor {
165 func: get_by_pointer,
166 key: Some("/Msg/fan_speed"),
167 tag: None,
168 },
169 )],
170 DataField::Hashboards => vec![(
171 RPC_DEVS,
172 DataExtractor {
173 func: get_by_pointer,
174 key: Some(""),
175 tag: None,
176 },
177 )],
178 DataField::Pools => vec![(
179 RPC_POOLS,
180 DataExtractor {
181 func: get_by_pointer,
182 key: Some("/POOLS"),
183 tag: None,
184 },
185 )],
186 DataField::Uptime => vec![(
187 RPC_SUMMARY,
188 DataExtractor {
189 func: get_by_pointer,
190 key: Some("/SUMMARY/0/Elapsed"),
191 tag: None,
192 },
193 )],
194 DataField::Wattage => vec![(
195 RPC_SUMMARY,
196 DataExtractor {
197 func: get_by_pointer,
198 key: Some("/SUMMARY/0/Power"),
199 tag: None,
200 },
201 )],
202 DataField::Hashrate => vec![(
203 RPC_SUMMARY,
204 DataExtractor {
205 func: get_by_pointer,
206 key: Some("/SUMMARY/0/HS RT"),
207 tag: None,
208 },
209 )],
210 DataField::ExpectedHashrate => vec![(
211 RPC_SUMMARY,
212 DataExtractor {
213 func: get_by_pointer,
214 key: Some("/SUMMARY/0/Factory GHS"),
215 tag: None,
216 },
217 )],
218 DataField::FluidTemperature => vec![(
219 RPC_SUMMARY,
220 DataExtractor {
221 func: get_by_pointer,
222 key: Some("/SUMMARY/0/Env Temp"),
223 tag: None,
224 },
225 )],
226 DataField::IsMining => vec![(
227 RPC_STATUS,
228 DataExtractor {
229 func: get_by_pointer,
230 key: Some("/SUMMARY/0/btmineroff"),
231 tag: None,
232 },
233 )],
234 DataField::Messages => vec![(
235 RPC_GET_ERROR_CODE,
236 DataExtractor {
237 func: get_by_pointer,
238 key: Some("/Msg/error_code"),
239 tag: None,
240 },
241 )],
242 _ => vec![],
243 }
244 }
245}
246
247impl GetIP for WhatsMinerV2 {
248 fn get_ip(&self) -> IpAddr {
249 self.ip
250 }
251}
252impl GetDeviceInfo for WhatsMinerV2 {
253 fn get_device_info(&self) -> DeviceInfo {
254 self.device_info
255 }
256}
257
258impl CollectData for WhatsMinerV2 {
259 fn get_collector(&self) -> DataCollector<'_> {
260 DataCollector::new(self)
261 }
262}
263
264impl GetMAC for WhatsMinerV2 {
265 fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
266 data.extract::<String>(DataField::Mac)
267 .and_then(|s| MacAddr::from_str(&s).ok())
268 }
269}
270
271impl GetSerialNumber for WhatsMinerV2 {}
272impl GetHostname for WhatsMinerV2 {
273 fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
274 data.extract::<String>(DataField::Hostname)
275 }
276}
277impl GetApiVersion for WhatsMinerV2 {
278 fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
279 data.extract::<String>(DataField::ApiVersion)
280 }
281}
282impl GetFirmwareVersion for WhatsMinerV2 {
283 fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
284 data.extract::<String>(DataField::FirmwareVersion)
285 }
286}
287impl GetControlBoardVersion for WhatsMinerV2 {
288 fn parse_control_board_version(
289 &self,
290 data: &HashMap<DataField, Value>,
291 ) -> Option<MinerControlBoard> {
292 data.extract::<String>(DataField::ControlBoardVersion)
293 .and_then(|s| MinerControlBoard::from_str(&s).ok())
294 }
295}
296impl GetHashboards for WhatsMinerV2 {
297 fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
298 let mut hashboards: Vec<BoardData> = Vec::new();
299 let board_count = self.device_info.hardware.boards.unwrap_or(3);
300 let hashboard_data = data.get(&DataField::Hashboards);
301
302 for idx in 0..board_count {
303 let hashrate = hashboard_data
304 .and_then(|val| val.pointer(&format!("/DEVS/{idx}/MHS av")))
305 .and_then(|val| val.as_f64())
306 .map(|f| {
307 HashRate {
308 value: f,
309 unit: HashRateUnit::MegaHash,
310 algo: String::from("SHA256"),
311 }
312 .as_unit(HashRateUnit::TeraHash)
313 });
314 let expected_hashrate = hashboard_data
315 .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Factory GHS")))
316 .and_then(|val| val.as_f64())
317 .map(|f| {
318 HashRate {
319 value: f,
320 unit: HashRateUnit::GigaHash,
321 algo: String::from("SHA256"),
322 }
323 .as_unit(HashRateUnit::TeraHash)
324 });
325 let board_temperature = hashboard_data
326 .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Temperature")))
327 .and_then(|val| val.as_f64())
328 .map(Temperature::from_celsius);
329 let intake_temperature = hashboard_data
330 .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Chip Temp Min")))
331 .and_then(|val| val.as_f64())
332 .map(Temperature::from_celsius);
333 let outlet_temperature = hashboard_data
334 .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Chip Temp Max")))
335 .and_then(|val| val.as_f64())
336 .map(Temperature::from_celsius);
337 let serial_number = hashboard_data
338 .and_then(|val| val.pointer(&format!("/DEVS/{idx}/PCB SN")))
339 .and_then(|val| val.as_str())
340 .map(String::from);
341 let working_chips = hashboard_data
342 .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Effective Chips")))
343 .and_then(|val| val.as_u64())
344 .map(|u| u as u16);
345 let frequency = hashboard_data
346 .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Frequency")))
347 .and_then(|val| val.as_f64())
348 .map(Frequency::from_megahertz);
349
350 let active = Some(hashrate.clone().map(|h| h.value).unwrap_or(0f64) > 0f64);
351 hashboards.push(BoardData {
352 hashrate,
353 position: idx,
354 expected_hashrate,
355 board_temperature,
356 intake_temperature,
357 outlet_temperature,
358 expected_chips: self.device_info.hardware.chips,
359 working_chips,
360 serial_number,
361 chips: vec![],
362 voltage: None, frequency,
364 tuned: Some(true),
365 active,
366 });
367 }
368 hashboards
369 }
370}
371impl GetHashrate for WhatsMinerV2 {
372 fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
373 data.extract_map::<f64, _>(DataField::Hashrate, |f| {
374 HashRate {
375 value: f,
376 unit: HashRateUnit::MegaHash,
377 algo: String::from("SHA256"),
378 }
379 .as_unit(HashRateUnit::TeraHash)
380 })
381 }
382}
383impl GetExpectedHashrate for WhatsMinerV2 {
384 fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
385 data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| {
386 HashRate {
387 value: f,
388 unit: HashRateUnit::GigaHash,
389 algo: String::from("SHA256"),
390 }
391 .as_unit(HashRateUnit::TeraHash)
392 })
393 }
394}
395impl GetFans for WhatsMinerV2 {
396 fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
397 let mut fans: Vec<FanData> = Vec::new();
398 for (idx, direction) in ["In", "Out"].iter().enumerate() {
399 let fan = data.extract_nested_map::<f64, _>(
400 DataField::Fans,
401 &format!("Fan Speed {direction}"),
402 |rpm| FanData {
403 position: idx as i16,
404 rpm: Some(AngularVelocity::from_rpm(rpm)),
405 },
406 );
407 if let Some(f) = fan {
408 fans.push(f)
409 }
410 }
411 fans
412 }
413}
414impl GetPsuFans for WhatsMinerV2 {
415 fn parse_psu_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
416 let mut psu_fans: Vec<FanData> = Vec::new();
417
418 let psu_fan = data.extract_map::<String, _>(DataField::PsuFans, |rpm| FanData {
419 position: 0i16,
420 rpm: Some(AngularVelocity::from_rpm(rpm.parse().unwrap())),
421 });
422 if let Some(f) = psu_fan {
423 psu_fans.push(f)
424 }
425 psu_fans
426 }
427}
428impl GetFluidTemperature for WhatsMinerV2 {
429 fn parse_fluid_temperature(&self, data: &HashMap<DataField, Value>) -> Option<Temperature> {
430 data.extract_map::<f64, _>(DataField::FluidTemperature, Temperature::from_celsius)
431 }
432}
433impl GetWattage for WhatsMinerV2 {
434 fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
435 data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
436 }
437}
438impl GetWattageLimit for WhatsMinerV2 {
439 fn parse_wattage_limit(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
440 data.extract_map::<f64, _>(DataField::WattageLimit, Power::from_watts)
441 }
442}
443impl GetLightFlashing for WhatsMinerV2 {
444 fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
445 data.extract_map::<String, _>(DataField::LightFlashing, |l| l != "auto")
446 }
447}
448impl GetMessages for WhatsMinerV2 {
449 fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
450 let mut messages = Vec::new();
451
452 let errors_raw = data.get(&DataField::Messages);
453
454 if let Some(errors_response) = errors_raw {
455 for obj in errors_response.as_array().unwrap_or(&Vec::new()).iter() {
456 let object = obj.as_object();
457 if let Some(obj) = object {
458 for (code, time) in obj.iter() {
459 dbg!(time);
460 let timestamp = NaiveDateTime::parse_from_str(
461 time.as_str().unwrap(),
462 "%Y-%m-%d %H:%M:%S",
463 )
464 .map(|t| DateTime::<Utc>::from_naive_utc_and_offset(t, Utc))
465 .map(|dt| dt.timestamp_millis() as u32);
466
467 dbg!(×tamp);
468
469 if let Ok(ts) = timestamp {
470 messages.push(MinerMessage {
471 timestamp: ts,
472 code: code.parse::<u64>().unwrap_or(0),
473 message: "".to_string(),
474 severity: MessageSeverity::Error,
475 })
476 }
477 }
478 }
479 }
480 }
481
482 messages
483 }
484}
485impl GetUptime for WhatsMinerV2 {
486 fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
487 data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
488 }
489}
490impl GetIsMining for WhatsMinerV2 {
491 fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
492 data.extract_map::<String, _>(DataField::IsMining, |l| l != "false")
493 .unwrap_or(true)
494 }
495}
496impl GetPools for WhatsMinerV2 {
497 fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
498 let mut pools: Vec<PoolData> = Vec::new();
499 let pools_raw = data.get(&DataField::Pools);
500 if let Some(pools_response) = pools_raw {
501 for (idx, _) in pools_response
502 .as_array()
503 .unwrap_or(&Vec::new())
504 .iter()
505 .enumerate()
506 {
507 let user = pools_raw
508 .and_then(|val| val.pointer(&format!("/{idx}/User")))
509 .map(|val| String::from(val.as_str().unwrap_or("")));
510
511 let alive = pools_raw
512 .and_then(|val| val.pointer(&format!("/{idx}/Status")))
513 .map(|val| val.as_str())
514 .map(|val| val == Some("Alive"));
515
516 let active = pools_raw
517 .and_then(|val| val.pointer(&format!("/{idx}/Stratum Active")))
518 .and_then(|val| val.as_bool());
519
520 let url = pools_raw
521 .and_then(|val| val.pointer(&format!("/{idx}/URL")))
522 .map(|val| PoolURL::from(String::from(val.as_str().unwrap_or(""))));
523
524 let accepted_shares = pools_raw
525 .and_then(|val| val.pointer(&format!("/{idx}/Accepted")))
526 .and_then(|val| val.as_u64());
527
528 let rejected_shares = pools_raw
529 .and_then(|val| val.pointer(&format!("/{idx}/Rejected")))
530 .and_then(|val| val.as_u64());
531
532 pools.push(PoolData {
533 position: Some(idx as u16),
534 url,
535 accepted_shares,
536 rejected_shares,
537 active,
538 alive,
539 user,
540 });
541 }
542 }
543 pools
544 }
545}
546
547#[async_trait]
548impl SetFaultLight for WhatsMinerV2 {
549 async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
550 let parameters = match fault {
551 false => Some(
552 json!({"auto": true, "color": "red", "period": 60, "duration": 20, "start": 0}),
553 ),
554 true => Some(
555 json!({"auto": false, "color": "red", "period": 60, "duration": 20, "start": 0}),
556 ),
557 };
558
559 let data = self.rpc.send_command("set_led", true, parameters).await;
560 Ok(data.is_ok())
561 }
562}
563
564#[async_trait]
565impl SetPowerLimit for WhatsMinerV2 {
566 async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
567 let parameters = Some(json!({"power_limit": limit.as_watts().to_string()}));
568 let data = self
569 .rpc
570 .send_command("adjust_power_limit", true, parameters)
571 .await;
572 Ok(data.is_ok())
573 }
574}
575
576#[async_trait]
577impl Restart for WhatsMinerV2 {
578 async fn restart(&self) -> anyhow::Result<bool> {
579 let data = self.rpc.send_command("reboot", true, None).await;
580 Ok(data.is_ok())
581 }
582}
583
584#[async_trait]
585impl Pause for WhatsMinerV2 {
586 #[allow(unused_variables)]
587 async fn pause(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
588 let data = self
589 .rpc
590 .send_command("power_off", true, Some(json!({"respbefore": "true"}))) .await;
592 Ok(data.is_ok())
593 }
594}
595
596#[async_trait]
597impl Resume for WhatsMinerV2 {
598 #[allow(unused_variables)]
599 async fn resume(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
600 let data = self.rpc.send_command("power_on", true, None).await;
601 Ok(data.is_ok())
602 }
603}