1use std::collections::HashMap;
2use std::net::IpAddr;
3use std::str::FromStr;
4use std::time::Duration;
5
6use macaddr::MacAddr;
7use measurements::{AngularVelocity, Frequency, Power, Temperature, Voltage};
8use serde_json::Value;
9
10use crate::data::board::{BoardData, ChipData};
11use crate::data::device::MinerMake;
12use crate::data::device::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
13use crate::data::fan::FanData;
14use crate::data::hashrate::{HashRate, HashRateUnit};
15use crate::data::pool::{PoolData, PoolURL};
16use crate::miners::backends::traits::*;
17use crate::miners::commands::MinerCommand;
18use crate::miners::data::{
19 DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer,
20};
21use web::PowerPlayWebAPI;
22
23pub mod web;
24
25#[derive(Debug)]
26pub struct PowerPlay {
27 ip: IpAddr,
28 web: PowerPlayWebAPI,
29 device_info: DeviceInfo,
30}
31
32impl PowerPlay {
33 pub fn new(ip: IpAddr, make: MinerMake, model: MinerModel) -> Self {
34 PowerPlay {
35 ip,
36 web: PowerPlayWebAPI::new(ip, 4028),
37 device_info: DeviceInfo::new(make, model, MinerFirmware::EPic, HashAlgorithm::SHA256),
38 }
39 }
40}
41
42impl GetDataLocations for PowerPlay {
43 fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
44 fn cmd(endpoint: &'static str) -> MinerCommand {
45 MinerCommand::WebAPI {
46 command: endpoint,
47 parameters: None,
48 }
49 }
50
51 let summary_cmd = cmd("summary");
52 let network_cmd = cmd("network");
53 let capabilities_cmd = cmd("capabilities");
54 let chip_temps_cmd = cmd("temps/chip");
55 let chip_voltages_cmd = cmd("voltages");
56 let chip_hashrates_cmd = cmd("hashrate");
57 let chip_clocks_cmd = cmd("clocks");
58 let temps_cmd = cmd("temps");
59
60 match data_field {
61 DataField::Mac => vec![(
62 network_cmd,
63 DataExtractor {
64 func: get_by_pointer,
65 key: Some(""),
66 tag: None,
67 },
68 )],
69 DataField::Hostname => vec![(
70 summary_cmd,
71 DataExtractor {
72 func: get_by_pointer,
73 key: Some("/Hostname"),
74 tag: None,
75 },
76 )],
77 DataField::Uptime => vec![(
78 summary_cmd,
79 DataExtractor {
80 func: get_by_pointer,
81 key: Some("/Session/Uptime"),
82 tag: None,
83 },
84 )],
85 DataField::Wattage => vec![(
86 summary_cmd,
87 DataExtractor {
88 func: get_by_pointer,
89 key: Some("/Power Supply Stats/Input Power"),
90 tag: None,
91 },
92 )],
93 DataField::Fans => vec![(
94 summary_cmd,
95 DataExtractor {
96 func: get_by_pointer,
97 key: Some("/Fans Rpm"),
98 tag: None,
99 },
100 )],
101 DataField::Hashboards => vec![
102 (
103 temps_cmd,
104 DataExtractor {
105 func: get_by_pointer,
106 key: Some(""),
107 tag: Some("Board Temps"),
108 },
109 ),
110 (
111 summary_cmd,
112 DataExtractor {
113 func: get_by_pointer,
114 key: Some(""),
115 tag: Some("Summary"),
116 },
117 ),
118 (
119 chip_temps_cmd,
120 DataExtractor {
121 func: get_by_pointer,
122 key: Some(""),
123 tag: Some("Chip Temps"),
124 },
125 ),
126 (
127 chip_voltages_cmd,
128 DataExtractor {
129 func: get_by_pointer,
130 key: Some(""),
131 tag: Some("Chip Voltages"),
132 },
133 ),
134 (
135 chip_hashrates_cmd,
136 DataExtractor {
137 func: get_by_pointer,
138 key: Some(""),
139 tag: Some("Chip Hashrates"),
140 },
141 ),
142 (
143 chip_clocks_cmd,
144 DataExtractor {
145 func: get_by_pointer,
146 key: Some(""),
147 tag: Some("Chip Clocks"),
148 },
149 ),
150 (
151 capabilities_cmd,
152 DataExtractor {
153 func: get_by_pointer,
154 key: Some(""),
155 tag: Some("Capabilities"),
156 },
157 ),
158 ],
159 DataField::Pools => vec![(
160 summary_cmd,
161 DataExtractor {
162 func: get_by_pointer,
163 key: Some(""),
164 tag: None,
165 },
166 )],
167 DataField::IsMining => vec![(
168 summary_cmd,
169 DataExtractor {
170 func: get_by_pointer,
171 key: Some("/Status/Operating State"),
172 tag: None,
173 },
174 )],
175 DataField::LightFlashing => vec![(
176 summary_cmd,
177 DataExtractor {
178 func: get_by_pointer,
179 key: Some("/Misc/Locate Miner State"),
180 tag: None,
181 },
182 )],
183 DataField::ControlBoardVersion => vec![(
184 capabilities_cmd,
185 DataExtractor {
186 func: get_by_pointer,
187 key: Some("/Control Board Version/cpuHardware"),
188 tag: None,
189 },
190 )],
191 DataField::SerialNumber => vec![(
192 capabilities_cmd,
193 DataExtractor {
194 func: get_by_pointer,
195 key: Some("/Control Board Version/cpuSerial"),
196 tag: None,
197 },
198 )],
199 DataField::ExpectedHashrate => vec![(
200 capabilities_cmd,
201 DataExtractor {
202 func: get_by_pointer,
203 key: Some("/Default Hashrate"),
204 tag: None,
205 },
206 )],
207 DataField::FirmwareVersion => vec![(
208 summary_cmd,
209 DataExtractor {
210 func: get_by_pointer,
211 key: Some("/Software"),
212 tag: None,
213 },
214 )],
215 DataField::Hashrate => vec![(
216 summary_cmd,
217 DataExtractor {
218 func: get_by_pointer,
219 key: Some("/HBs"),
220 tag: None,
221 },
222 )],
223 _ => vec![],
224 }
225 }
226}
227
228impl GetIP for PowerPlay {
229 fn get_ip(&self) -> IpAddr {
230 self.ip
231 }
232}
233
234impl GetDeviceInfo for PowerPlay {
235 fn get_device_info(&self) -> DeviceInfo {
236 self.device_info
237 }
238}
239
240impl CollectData for PowerPlay {
241 fn get_collector(&self) -> DataCollector<'_> {
242 DataCollector::new(self, &self.web)
243 }
244}
245
246impl GetMAC for PowerPlay {
247 fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
248 match serde_json::from_value::<HashMap<String, Value>>(data.get(&DataField::Mac)?.clone())
249 .ok()
250 .and_then(|inner| inner.get("dhcp").or_else(|| inner.get("static")).cloned())
251 .and_then(|obj| {
252 obj.get("mac_address")
253 .and_then(|v| v.as_str())
254 .map(String::from)
255 }) {
256 Some(mac_str) => MacAddr::from_str(&mac_str).ok(),
257 None => None,
258 }
259 }
260}
261
262impl GetSerialNumber for PowerPlay {
263 fn parse_serial_number(&self, data: &HashMap<DataField, Value>) -> Option<String> {
264 data.extract::<String>(DataField::SerialNumber)
265 }
266}
267
268impl GetHostname for PowerPlay {
269 fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
270 data.extract::<String>(DataField::Hostname)
271 }
272}
273
274impl GetApiVersion for PowerPlay {
275 fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
276 data.extract::<String>(DataField::ApiVersion)
277 }
278}
279
280impl GetFirmwareVersion for PowerPlay {
281 fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
282 data.extract::<String>(DataField::FirmwareVersion)
283 }
284}
285
286impl GetControlBoardVersion for PowerPlay {
287 fn parse_control_board_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
288 data.extract::<String>(DataField::ControlBoardVersion)
289 }
290}
291
292impl GetHashboards for PowerPlay {
293 fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
294 let mut hashboards: Vec<BoardData> = Vec::new();
295 for _ in 0..self.device_info.hardware.boards.unwrap_or_default() {
296 hashboards.push(BoardData {
297 position: 0,
298 hashrate: None,
299 expected_hashrate: None,
300 board_temperature: None,
301 intake_temperature: None,
302 outlet_temperature: None,
303 expected_chips: None,
304 working_chips: None,
305 serial_number: None,
306 chips: vec![],
307 voltage: None,
308 frequency: None,
309 tuned: None,
310 active: None,
311 });
312 }
313
314 data.get(&DataField::Hashboards)
315 .and_then(|v| v.pointer("/Summary/HBStatus"))
316 .and_then(|v| {
317 v.as_array().map(|boards| {
318 boards.iter().for_each(|board| {
319 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
320 && let Some(hashboard) = hashboards.get_mut(idx as usize)
321 {
322 hashboard.position = idx as u8;
323 if let Some(v) = board.get("Enabled").and_then(|v| v.as_bool()) {
324 hashboard.active = Some(v);
325 }
326 }
327 })
328 })
329 });
330
331 for board in &mut hashboards {
333 board.expected_chips = self.device_info.hardware.chips;
334 if board.active.unwrap_or(false) {
336 board.chips = vec![
337 ChipData {
338 position: 0,
339 hashrate: None,
340 temperature: None,
341 voltage: None,
342 frequency: None,
343 tuned: None,
344 working: None,
345 };
346 self.device_info.hardware.chips.unwrap_or_default() as usize
347 ];
348 }
349 }
350
351 if let Some(serial_numbers) = data
353 .get(&DataField::Hashboards)
354 .and_then(|v| v.pointer("/Capabilities/Board Serial Numbers"))
355 .and_then(|v| v.as_array())
356 {
357 for serial in serial_numbers {
358 for hb in hashboards.iter_mut() {
361 if hb.serial_number.is_none() && hb.active.unwrap_or(false) {
362 if let Some(serial_str) = serial.as_str() {
363 hb.serial_number = Some(serial_str.to_string());
364 }
365 break; }
367 }
368 }
369 };
370
371 data.get(&DataField::Hashboards)
373 .and_then(|v| v.pointer("/Summary/HBs"))
374 .and_then(|v| {
375 v.as_array().map(|boards| {
376 boards.iter().for_each(|board| {
377 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
378 && let Some(hashboard) = hashboards.get_mut(idx as usize)
379 {
380 if let Some(h) = board
382 .get("Hashrate")
383 .and_then(|v| v.as_array())
384 .and_then(|v| v.first().and_then(|f| f.as_f64()))
385 {
386 hashboard.hashrate = Some(HashRate {
387 value: h,
388 unit: HashRateUnit::MegaHash,
389 algo: String::from("SHA256"),
390 })
391 };
392
393 if let Some(h) = board
395 .get("Hashrate")
396 .and_then(|v| v.as_array())
397 .and_then(|v| {
398 Some((
399 v.first().and_then(|f| f.as_f64())?,
400 v.get(1).and_then(|f| f.as_f64())?,
401 ))
402 })
403 {
404 hashboard.expected_hashrate = Some(HashRate {
405 value: h.0 / h.1,
406 unit: HashRateUnit::MegaHash,
407 algo: String::from("SHA256"),
408 })
409 };
410
411 if let Some(f) = board.get("Core Clock Avg").and_then(|v| v.as_f64()) {
413 hashboard.frequency = Some(Frequency::from_megahertz(f))
414 };
415
416 if let Some(v) = board.get("Input Voltage").and_then(|v| v.as_f64()) {
418 hashboard.voltage = Some(Voltage::from_volts(v));
419 };
420 if let Some(v) = board.get("Temperature").and_then(|v| v.as_f64()) {
422 hashboard.board_temperature = Some(Temperature::from_celsius(v));
423 };
424 };
425 })
426 })
427 });
428
429 data.get(&DataField::Hashboards)
431 .and_then(|v| v.pointer("/Board Temps"))
432 .and_then(|v| {
433 v.as_array().map(|boards| {
434 boards.iter().for_each(|board| {
435 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
436 && let Some(hashboard) = hashboards.get_mut(idx as usize)
437 {
438 if let Some(h) = board.get("Data").and_then(|v| {
440 v.as_array().and_then(|arr| {
441 arr.iter()
442 .filter_map(|v| v.as_f64())
443 .max_by(|a, b| a.partial_cmp(b).unwrap())
444 })
445 }) {
446 hashboard.outlet_temperature = Some(Temperature::from_celsius(h));
447 };
448
449 if let Some(h) = board.get("Data").and_then(|v| {
450 v.as_array().and_then(|arr| {
451 arr.iter()
452 .filter_map(|v| v.as_f64())
453 .min_by(|a, b| a.partial_cmp(b).unwrap())
454 })
455 }) {
456 hashboard.intake_temperature = Some(Temperature::from_celsius(h));
457 };
458 };
459 })
460 })
461 });
462
463 data.get(&DataField::Hashboards)
465 .and_then(|v| v.pointer("/Chip Temps"))
466 .and_then(|v| {
467 v.as_array().map(|boards| {
468 boards.iter().for_each(|board| {
469 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
470 && let Some(hashboard) = hashboards.get_mut(idx as usize)
471 && let Some(t) =
472 board.get("Data").and_then(|v| v.as_array()).map(|arr| {
473 arr.iter()
474 .filter_map(|v| v.as_f64())
475 .map(Temperature::from_celsius)
476 .collect::<Vec<Temperature>>()
477 })
478 {
479 for (chip_no, temp) in t.iter().enumerate() {
480 if let Some(chip_data) = hashboard.chips.get_mut(chip_no) {
481 chip_data.position = chip_no as u16;
482 chip_data.temperature = Some(*temp);
483 }
484 }
485 };
486 })
487 })
488 });
489
490 data.get(&DataField::Hashboards)
492 .and_then(|v| v.pointer("/Chip Voltages"))
493 .and_then(|v| {
494 v.as_array().map(|boards| {
495 boards.iter().for_each(|board| {
496 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
497 && let Some(hashboard) = hashboards.get_mut(idx as usize)
498 && let Some(t) =
499 board.get("Data").and_then(|v| v.as_array()).map(|arr| {
500 arr.iter()
501 .filter_map(|v| v.as_f64())
502 .map(Voltage::from_millivolts)
503 .collect::<Vec<Voltage>>()
504 })
505 {
506 for (chip_no, voltage) in t.iter().enumerate() {
507 if let Some(chip_data) = hashboard.chips.get_mut(chip_no) {
508 chip_data.position = chip_no as u16;
509 chip_data.voltage = Some(*voltage);
510 }
511 }
512 };
513 })
514 })
515 });
516
517 data.get(&DataField::Hashboards)
519 .and_then(|v| v.pointer("/Chip Clocks"))
520 .and_then(|v| {
521 v.as_array().map(|boards| {
522 boards.iter().for_each(|board| {
523 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
524 && let Some(hashboard) = hashboards.get_mut(idx as usize)
525 && let Some(t) =
526 board.get("Data").and_then(|v| v.as_array()).map(|arr| {
527 arr.iter()
528 .filter_map(|v| v.as_f64())
529 .map(Frequency::from_megahertz)
530 .collect::<Vec<Frequency>>()
531 })
532 {
533 for (chip_no, freq) in t.iter().enumerate() {
534 if let Some(chip_data) = hashboard.chips.get_mut(chip_no) {
535 chip_data.position = chip_no as u16;
536 chip_data.frequency = Some(*freq);
537 }
538 }
539 };
540 })
541 })
542 });
543
544 data.get(&DataField::Hashboards)
547 .and_then(|v| v.pointer("/Chip Hashrates"))
548 .and_then(|v| {
549 v.as_array().map(|boards| {
550 boards.iter().for_each(|board| {
551 if let Some(idx) = board.get("Index").and_then(|v| v.as_u64())
552 && let Some(hashboard) = hashboards.get_mut(idx as usize)
553 && let Some(t) =
554 board.get("Data").and_then(|v| v.as_array()).map(|arr| {
555 arr.iter()
556 .filter_map(|inner| inner.as_array())
557 .filter_map(|inner| inner.first().and_then(|v| v.as_f64()))
558 .map(|hr| HashRate {
559 value: hr,
560 unit: HashRateUnit::MegaHash,
561 algo: String::from("SHA256"),
562 })
563 .collect::<Vec<HashRate>>()
564 })
565 {
566 for (chip_no, hashrate) in t.iter().enumerate() {
567 if let Some(chip_data) = hashboard.chips.get_mut(chip_no) {
568 chip_data.position = chip_no as u16;
569 chip_data.working = Some(true);
570 chip_data.hashrate = Some(hashrate.clone());
571 }
572 }
573 };
574 })
575 })
576 });
577
578 hashboards
579 }
580}
581
582impl GetHashrate for PowerPlay {
583 fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
584 let mut total_hashrate: f64 = 0.0;
585
586 data.get(&DataField::Hashrate).and_then(|v| {
587 v.as_array().map(|boards| {
588 boards.iter().for_each(|board| {
589 if let Some(_idx) = board.get("Index").and_then(|v| v.as_u64()) {
590 if let Some(h) = board
592 .get("Hashrate")
593 .and_then(|v| v.as_array())
594 .and_then(|v| v.first().and_then(|f| f.as_f64()))
595 {
596 total_hashrate += h;
597 };
598 }
599 })
600 })
601 });
602
603 Some(HashRate {
604 value: total_hashrate,
605 unit: HashRateUnit::MegaHash,
606 algo: String::from("SHA256"),
607 })
608 }
609}
610
611impl GetExpectedHashrate for PowerPlay {
612 fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
613 data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| HashRate {
614 value: f,
615 unit: HashRateUnit::TeraHash,
616 algo: String::from("SHA256"),
617 })
618 }
619}
620
621impl GetFans for PowerPlay {
622 fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
623 let mut fans: Vec<FanData> = Vec::new();
624
625 if let Some(fans_data) = data.get(&DataField::Fans)
626 && let Some(obj) = fans_data.as_object()
627 {
628 for (key, value) in obj {
629 if let Some(num) = value.as_f64() {
630 if let Some(pos_str) = key.strip_prefix("Fans Speed ")
632 && let Ok(pos) = pos_str.parse::<i16>()
633 {
634 fans.push(FanData {
635 position: pos,
636 rpm: Some(AngularVelocity::from_rpm(num)),
637 });
638 }
639 }
640 }
641 }
642
643 fans
644 }
645}
646
647impl GetPsuFans for PowerPlay {}
648
649impl GetFluidTemperature for PowerPlay {}
650
651impl GetWattage for PowerPlay {
652 fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
653 data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
654 }
655}
656
657impl GetWattageLimit for PowerPlay {}
658
659impl GetLightFlashing for PowerPlay {
660 fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
661 data.extract::<bool>(DataField::LightFlashing)
662 }
663}
664
665impl GetMessages for PowerPlay {}
666
667impl GetUptime for PowerPlay {
668 fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
669 data.extract::<u64>(DataField::Uptime)
670 .map(Duration::from_secs)
671 }
672}
673
674impl GetIsMining for PowerPlay {
675 fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
676 data.extract::<String>(DataField::IsMining)
677 .map(|state| state != "Idling")
678 .unwrap_or(false)
679 }
680}
681
682impl GetPools for PowerPlay {
683 fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
684 let mut pools_vec: Vec<PoolData> = Vec::new();
685
686 if let Some(configs) = data
687 .get(&DataField::Pools)
688 .and_then(|v| v.pointer("/StratumConfigs"))
689 .and_then(|v| v.as_array())
690 {
691 for (idx, config) in configs.iter().enumerate() {
692 let url = config.get("pool").and_then(|v| v.as_str()).and_then(|s| {
693 if s.is_empty() {
694 None
695 } else {
696 Some(PoolURL::from(s.to_string()))
697 }
698 });
699 let user = config
700 .get("login")
701 .and_then(|v| v.as_str())
702 .map(String::from);
703 pools_vec.push(PoolData {
704 position: Some(idx as u16),
705 url,
706 accepted_shares: None,
707 rejected_shares: None,
708 active: Some(false),
709 alive: None,
710 user,
711 });
712 }
713 }
714
715 if let Some(stratum) = data
716 .get(&DataField::Pools)
717 .and_then(|v| v.pointer("/Stratum"))
718 .and_then(|v| v.as_object())
719 {
720 for pool in pools_vec.iter_mut() {
721 if pool.position
722 == stratum
723 .get("Config Id")
724 .and_then(|v| v.as_u64().map(|v| v as u16))
725 {
726 pool.active = Some(true);
727 pool.alive = stratum.get("IsPoolConnected").and_then(|v| v.as_bool());
728 pool.user = stratum
729 .get("Current User")
730 .and_then(|v| v.as_str())
731 .map(String::from);
732 pool.url = stratum
733 .get("Current Pool")
734 .and_then(|v| v.as_str())
735 .and_then(|s| {
736 if s.is_empty() {
737 None
738 } else {
739 Some(PoolURL::from(s.to_string()))
740 }
741 });
742
743 if let Some(session) = data
745 .get(&DataField::Pools)
746 .and_then(|v| v.pointer("/Session"))
747 .and_then(|v| v.as_object())
748 {
749 pool.accepted_shares = session.get("Accepted").and_then(|v| v.as_u64());
750 pool.rejected_shares = session.get("Rejected").and_then(|v| v.as_u64());
751 }
752 }
753 }
754 }
755
756 pools_vec
757 }
758}