1use 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::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
13use crate::data::device::{MinerControlBoard, MinerMake};
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) -> 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 WhatsMinerV1 {
63 fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
64 const RPC_SUMMARY: MinerCommand = MinerCommand::RPC {
65 command: "summary",
66 parameters: None,
67 };
68 const RPC_DEVS: MinerCommand = MinerCommand::RPC {
69 command: "devs",
70 parameters: None,
71 };
72 const RPC_POOLS: MinerCommand = MinerCommand::RPC {
73 command: "pools",
74 parameters: None,
75 };
76 const RPC_STATUS: MinerCommand = MinerCommand::RPC {
77 command: "status",
78 parameters: None,
79 };
80 const RPC_GET_VERSION: MinerCommand = MinerCommand::RPC {
81 command: "get_version",
82 parameters: None,
83 };
84 const RPC_GET_PSU: MinerCommand = MinerCommand::RPC {
85 command: "get_psu",
86 parameters: None,
87 };
88
89 match data_field {
90 DataField::Mac => vec![(
91 RPC_SUMMARY,
92 DataExtractor {
93 func: get_by_pointer,
94 key: Some("/SUMMARY/0/MAC"),
95 tag: None,
96 },
97 )],
98 DataField::ApiVersion => vec![(
99 RPC_GET_VERSION,
100 DataExtractor {
101 func: get_by_pointer,
102 key: Some("/Msg/api_ver"),
103 tag: None,
104 },
105 )],
106 DataField::FirmwareVersion => vec![(
107 RPC_GET_VERSION,
108 DataExtractor {
109 func: get_by_pointer,
110 key: Some("/Msg/fw_ver"),
111 tag: None,
112 },
113 )],
114 DataField::ControlBoardVersion => vec![(
115 RPC_SUMMARY,
116 DataExtractor {
117 func: get_by_pointer,
118 key: Some("/SUMMARY/0/CB Platform"),
119 tag: None,
120 },
121 )],
122 DataField::WattageLimit => vec![(
123 RPC_SUMMARY,
124 DataExtractor {
125 func: get_by_pointer,
126 key: Some("/SUMMARY/0/Power Limit"),
127 tag: None,
128 },
129 )],
130 DataField::Fans => vec![(
131 RPC_SUMMARY,
132 DataExtractor {
133 func: get_by_pointer,
134 key: Some("/SUMMARY/0"),
135 tag: None,
136 },
137 )],
138 DataField::PsuFans => vec![(
139 RPC_GET_PSU,
140 DataExtractor {
141 func: get_by_pointer,
142 key: Some("/Msg/fan_speed"),
143 tag: None,
144 },
145 )],
146 DataField::Hashboards => vec![(
147 RPC_DEVS,
148 DataExtractor {
149 func: get_by_pointer,
150 key: Some(""),
151 tag: None,
152 },
153 )],
154 DataField::Pools => vec![(
155 RPC_POOLS,
156 DataExtractor {
157 func: get_by_pointer,
158 key: Some("/POOLS"),
159 tag: None,
160 },
161 )],
162 DataField::Uptime => vec![(
163 RPC_SUMMARY,
164 DataExtractor {
165 func: get_by_pointer,
166 key: Some("/SUMMARY/0/Elapsed"),
167 tag: None,
168 },
169 )],
170 DataField::Wattage => vec![(
171 RPC_SUMMARY,
172 DataExtractor {
173 func: get_by_pointer,
174 key: Some("/SUMMARY/0/Power"),
175 tag: None,
176 },
177 )],
178 DataField::Hashrate => vec![(
179 RPC_SUMMARY,
180 DataExtractor {
181 func: get_by_pointer,
182 key: Some("/SUMMARY/0/HS RT"),
183 tag: None,
184 },
185 )],
186 DataField::ExpectedHashrate => vec![(
187 RPC_SUMMARY,
188 DataExtractor {
189 func: get_by_pointer,
190 key: Some("/SUMMARY/0/Factory GHS"),
191 tag: None,
192 },
193 )],
194 DataField::FluidTemperature => vec![(
195 RPC_SUMMARY,
196 DataExtractor {
197 func: get_by_pointer,
198 key: Some("/SUMMARY/0/Env Temp"),
199 tag: None,
200 },
201 )],
202 DataField::IsMining => vec![(
203 RPC_STATUS,
204 DataExtractor {
205 func: get_by_pointer,
206 key: Some("/SUMMARY/0/btmineroff"),
207 tag: None,
208 },
209 )],
210 DataField::Messages => vec![(
211 RPC_SUMMARY,
212 DataExtractor {
213 func: get_by_pointer,
214 key: Some("/SUMMARY/0"),
215 tag: None,
216 },
217 )],
218 _ => vec![],
219 }
220 }
221}
222
223impl GetIP for WhatsMinerV1 {
224 fn get_ip(&self) -> IpAddr {
225 self.ip
226 }
227}
228impl GetDeviceInfo for WhatsMinerV1 {
229 fn get_device_info(&self) -> DeviceInfo {
230 self.device_info
231 }
232}
233
234impl CollectData for WhatsMinerV1 {
235 fn get_collector(&self) -> DataCollector<'_> {
236 DataCollector::new(self)
237 }
238}
239
240impl GetMAC for WhatsMinerV1 {
241 fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
242 data.extract::<String>(DataField::Mac)
243 .and_then(|s| MacAddr::from_str(&s).ok())
244 }
245}
246
247impl GetSerialNumber for WhatsMinerV1 {}
248impl GetHostname for WhatsMinerV1 {}
249impl GetApiVersion for WhatsMinerV1 {
250 fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
251 data.extract::<String>(DataField::ApiVersion)
252 .and_then(|s| Some(s.strip_prefix("whatsminer v")?.to_string()))
253 }
254}
255impl GetFirmwareVersion for WhatsMinerV1 {
256 fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
257 data.extract::<String>(DataField::FirmwareVersion)
258 }
259}
260impl GetControlBoardVersion for WhatsMinerV1 {
261 fn parse_control_board_version(
262 &self,
263 data: &HashMap<DataField, Value>,
264 ) -> Option<MinerControlBoard> {
265 data.extract::<String>(DataField::ControlBoardVersion)
266 .and_then(|s| {
267 MinerControlBoard::from_str(s.to_uppercase().strip_prefix("ALLWINNER_")?).ok()
268 })
269 }
270}
271impl GetHashboards for WhatsMinerV1 {
272 fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
273 let mut hashboards: Vec<BoardData> = Vec::new();
274 let board_count = self.device_info.hardware.boards.unwrap_or(3);
275 let hashboard_data = data.get(&DataField::Hashboards);
276
277 for idx in 0..board_count {
278 let hashrate = hashboard_data
279 .and_then(|val| val.pointer(&format!("/DEVS/{}/MHS av", idx)))
280 .and_then(|val| val.as_f64())
281 .map(|f| {
282 HashRate {
283 value: f,
284 unit: HashRateUnit::MegaHash,
285 algo: String::from("SHA256"),
286 }
287 .as_unit(HashRateUnit::TeraHash)
288 });
289 let expected_hashrate = hashboard_data
290 .and_then(|val| val.pointer(&format!("/DEVS/{}/Factory GHS", idx)))
291 .and_then(|val| val.as_f64())
292 .map(|f| {
293 HashRate {
294 value: f,
295 unit: HashRateUnit::GigaHash,
296 algo: String::from("SHA256"),
297 }
298 .as_unit(HashRateUnit::TeraHash)
299 });
300 let board_temperature = hashboard_data
301 .and_then(|val| val.pointer(&format!("/DEVS/{}/Temperature", idx)))
302 .and_then(|val| val.as_f64())
303 .map(Temperature::from_celsius);
304 let intake_temperature = hashboard_data
305 .and_then(|val| val.pointer(&format!("/DEVS/{}/Chip Temp Min", idx)))
306 .and_then(|val| val.as_f64())
307 .map(Temperature::from_celsius);
308 let outlet_temperature = hashboard_data
309 .and_then(|val| val.pointer(&format!("/DEVS/{}/Chip Temp Max", idx)))
310 .and_then(|val| val.as_f64())
311 .map(Temperature::from_celsius);
312 let serial_number = hashboard_data
313 .and_then(|val| val.pointer(&format!("/DEVS/{}/PCB SN", idx)))
314 .and_then(|val| val.as_str())
315 .map(String::from);
316 let working_chips = hashboard_data
317 .and_then(|val| val.pointer(&format!("/DEVS/{}/Effective Chips", idx)))
318 .and_then(|val| val.as_u64())
319 .map(|u| u as u16);
320 let frequency = hashboard_data
321 .and_then(|val| val.pointer(&format!("/DEVS/{}/Frequency", idx)))
322 .and_then(|val| val.as_f64())
323 .map(Frequency::from_megahertz);
324
325 let active = Some(hashrate.clone().map(|h| h.value).unwrap_or(0f64) > 0f64);
326 hashboards.push(BoardData {
327 hashrate,
328 position: idx,
329 expected_hashrate,
330 board_temperature,
331 intake_temperature,
332 outlet_temperature,
333 expected_chips: self.device_info.hardware.chips,
334 working_chips,
335 serial_number,
336 chips: vec![],
337 voltage: None, frequency,
339 tuned: Some(true),
340 active,
341 });
342 }
343 hashboards
344 }
345}
346impl GetHashrate for WhatsMinerV1 {
347 fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
348 data.extract_map::<f64, _>(DataField::Hashrate, |f| {
349 HashRate {
350 value: f,
351 unit: HashRateUnit::MegaHash,
352 algo: String::from("SHA256"),
353 }
354 .as_unit(HashRateUnit::TeraHash)
355 })
356 }
357}
358impl GetExpectedHashrate for WhatsMinerV1 {
359 fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
360 data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| {
361 HashRate {
362 value: f,
363 unit: HashRateUnit::GigaHash,
364 algo: String::from("SHA256"),
365 }
366 .as_unit(HashRateUnit::TeraHash)
367 })
368 }
369}
370impl GetFans for WhatsMinerV1 {
371 fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
372 let mut fans: Vec<FanData> = Vec::new();
373 for (idx, direction) in ["In", "Out"].iter().enumerate() {
374 let fan = data.extract_nested_map::<f64, _>(
375 DataField::Fans,
376 &format!("Fan Speed {}", direction),
377 |rpm| FanData {
378 position: idx as i16,
379 rpm: Some(AngularVelocity::from_rpm(rpm)),
380 },
381 );
382 if let Some(f) = fan {
383 fans.push(f)
384 }
385 }
386 fans
387 }
388}
389impl GetPsuFans for WhatsMinerV1 {
390 fn parse_psu_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
391 let mut psu_fans: Vec<FanData> = Vec::new();
392
393 let psu_fan = data.extract_map::<String, _>(DataField::PsuFans, |rpm| FanData {
394 position: 0i16,
395 rpm: Some(AngularVelocity::from_rpm(rpm.parse().unwrap())),
396 });
397 if let Some(f) = psu_fan {
398 psu_fans.push(f)
399 }
400 psu_fans
401 }
402}
403impl GetFluidTemperature for WhatsMinerV1 {
404 fn parse_fluid_temperature(&self, data: &HashMap<DataField, Value>) -> Option<Temperature> {
405 data.extract_map::<f64, _>(DataField::FluidTemperature, Temperature::from_celsius)
406 }
407}
408impl GetWattage for WhatsMinerV1 {
409 fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
410 data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
411 }
412}
413impl GetWattageLimit for WhatsMinerV1 {
414 fn parse_wattage_limit(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
415 data.extract_map::<f64, _>(DataField::WattageLimit, Power::from_watts)
416 }
417}
418impl GetLightFlashing for WhatsMinerV1 {}
419impl GetMessages for WhatsMinerV1 {
420 fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
421 let mut messages = Vec::new();
422
423 let error_count = data
424 .get(&DataField::Messages)
425 .and_then(|val| {
426 val.pointer("/Error Code Count")
427 .and_then(|val| val.as_u64())
428 })
429 .unwrap_or(0u64) as usize;
430 for idx in 0..error_count {
431 let e_code = data
432 .get(&DataField::Messages)
433 .and_then(|val| val.pointer(&format!("/Error Code {}", idx)))
434 .and_then(|val| val.as_u64());
435 if let Some(code) = e_code {
436 messages.push(MinerMessage::new(
437 0,
438 code,
439 "".to_string(), MessageSeverity::Error,
441 ));
442 }
443 }
444
445 messages
446 }
447}
448impl GetUptime for WhatsMinerV1 {
449 fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
450 data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
451 }
452}
453impl GetIsMining for WhatsMinerV1 {
454 fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
455 data.extract_map::<String, _>(DataField::IsMining, |l| l != "false")
456 .unwrap_or(true)
457 }
458}
459impl GetPools for WhatsMinerV1 {
460 fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
461 let mut pools: Vec<PoolData> = Vec::new();
462 let pools_raw = data.get(&DataField::Pools);
463 if let Some(pools_response) = pools_raw {
464 for (idx, _) in pools_response
465 .as_array()
466 .unwrap_or(&Vec::new())
467 .iter()
468 .enumerate()
469 {
470 let user = pools_raw
471 .and_then(|val| val.pointer(&format!("/{}/User", idx)))
472 .map(|val| String::from(val.as_str().unwrap_or("")));
473
474 let alive = pools_raw
475 .and_then(|val| val.pointer(&format!("/{}/Status", idx)))
476 .map(|val| val.as_str())
477 .map(|val| val == Some("Alive"));
478
479 let active = pools_raw
480 .and_then(|val| val.pointer(&format!("/{}/Stratum Active", idx)))
481 .and_then(|val| val.as_bool());
482
483 let url = pools_raw
484 .and_then(|val| val.pointer(&format!("/{}/URL", idx)))
485 .map(|val| PoolURL::from(String::from(val.as_str().unwrap_or(""))));
486
487 let accepted_shares = pools_raw
488 .and_then(|val| val.pointer(&format!("/{}/Accepted", idx)))
489 .and_then(|val| val.as_u64());
490
491 let rejected_shares = pools_raw
492 .and_then(|val| val.pointer(&format!("/{}/Rejected", idx)))
493 .and_then(|val| val.as_u64());
494
495 pools.push(PoolData {
496 position: Some(idx as u16),
497 url,
498 accepted_shares,
499 rejected_shares,
500 active,
501 alive,
502 user,
503 });
504 }
505 }
506 pools
507 }
508}
509
510#[async_trait]
511impl SetFaultLight for WhatsMinerV1 {
512 #[allow(unused_variables)]
513 async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
514 anyhow::bail!("Unsupported command");
515 }
516}
517
518#[async_trait]
519impl SetPowerLimit for WhatsMinerV1 {
520 #[allow(unused_variables)]
521 async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
522 anyhow::bail!("Unsupported command");
523 }
524}
525
526#[async_trait]
527impl Restart for WhatsMinerV1 {
528 async fn restart(&self) -> anyhow::Result<bool> {
529 anyhow::bail!("Unsupported command");
530 }
531}
532
533#[async_trait]
534impl Pause for WhatsMinerV1 {
535 #[allow(unused_variables)]
536 async fn pause(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
537 anyhow::bail!("Unsupported command");
538 }
539}
540
541#[async_trait]
542impl Resume for WhatsMinerV1 {
543 #[allow(unused_variables)]
544 async fn resume(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
545 anyhow::bail!("Unsupported command");
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552 use crate::data::device::models::whatsminer::WhatsMinerModel;
553 use crate::test::api::MockAPIClient;
554 use crate::test::json::btminer::v1::{
555 DEVS_COMMAND, GET_PSU_COMMAND, GET_VERSION_COMMAND, POOLS_COMMAND, STATUS_COMMAND,
556 SUMMARY_COMMAND,
557 };
558
559 #[tokio::test]
560 async fn test_whatsminer_v1_data_parsers() -> anyhow::Result<()> {
561 let miner = WhatsMinerV1::new(
562 IpAddr::from([127, 0, 0, 1]),
563 MinerModel::WhatsMiner(WhatsMinerModel::M20SV10),
564 );
565 let mut results = HashMap::new();
566 let summary_command: MinerCommand = MinerCommand::RPC {
567 command: "summary",
568 parameters: None,
569 };
570 let status_command: MinerCommand = MinerCommand::RPC {
571 command: "status",
572 parameters: None,
573 };
574 let pools_command: MinerCommand = MinerCommand::RPC {
575 command: "pools",
576 parameters: None,
577 };
578 let devs_command: MinerCommand = MinerCommand::RPC {
579 command: "devs",
580 parameters: None,
581 };
582 let get_version_command: MinerCommand = MinerCommand::RPC {
583 command: "get_version",
584 parameters: None,
585 };
586 let get_psu_command: MinerCommand = MinerCommand::RPC {
587 command: "get_psu",
588 parameters: None,
589 };
590
591 results.insert(summary_command, Value::from_str(SUMMARY_COMMAND)?);
592 results.insert(status_command, Value::from_str(STATUS_COMMAND)?);
593 results.insert(pools_command, Value::from_str(POOLS_COMMAND)?);
594 results.insert(devs_command, Value::from_str(DEVS_COMMAND)?);
595 results.insert(get_version_command, Value::from_str(GET_VERSION_COMMAND)?);
596 results.insert(get_psu_command, Value::from_str(GET_PSU_COMMAND)?);
597
598 let mock_api = MockAPIClient::new(results);
599
600 let mut collector = DataCollector::new_with_client(&miner, &mock_api);
601 let data = collector.collect_all().await;
602
603 let miner_data = miner.parse_data(data);
604
605 assert_eq!(&miner_data.ip, &miner.ip);
606 assert_eq!(
607 miner_data.mac,
608 Some(MacAddr::from_str("C4:08:28:00:A4:19")?)
609 );
610 assert_eq!(miner_data.api_version, Some("1.4.0".to_string()));
611 assert_eq!(
612 miner_data.firmware_version,
613 Some("20210322.22.REL".to_string())
614 );
615 assert_eq!(
616 miner_data.control_board_version,
617 Some(MinerControlBoard::H3)
618 );
619 assert_eq!(
620 miner_data.hashrate,
621 Some(HashRate {
622 value: 67.39480097,
623 unit: HashRateUnit::TeraHash,
624 algo: String::from("SHA256"),
625 })
626 );
627 assert_eq!(
628 miner_data.expected_hashrate,
629 Some(HashRate {
630 value: 68.796,
631 unit: HashRateUnit::TeraHash,
632 algo: String::from("SHA256"),
633 })
634 );
635 assert_eq!(miner_data.wattage, Some(Power::from_watts(3417f64)));
636 assert_eq!(miner_data.wattage_limit, Some(Power::from_watts(3500f64)));
637 assert_eq!(miner_data.uptime, Some(Duration::from_secs(10154)));
638 assert_eq!(miner_data.fans.len(), 2);
639 assert_eq!(miner_data.pools.len(), 3);
640
641 Ok(())
642 }
643}