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