1use crate::data::board::BoardData;
2use crate::data::device::{
3 DeviceInfo, HashAlgorithm, MinerControlBoard, MinerFirmware, MinerMake, MinerModel,
4};
5use crate::data::fan::FanData;
6use crate::data::hashrate::{HashRate, HashRateUnit};
7use crate::data::message::{MessageSeverity, MinerMessage};
8use crate::data::pool::{PoolData, PoolURL};
9use crate::miners::backends::traits::*;
10use crate::miners::commands::MinerCommand;
11use crate::miners::data::{
12 DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer,
13};
14use anyhow;
15use async_trait::async_trait;
16use macaddr::MacAddr;
17use measurements::{AngularVelocity, Frequency, Power, Temperature};
18use serde_json::{Value, json};
19use std::collections::HashMap;
20use std::fmt::Display;
21use std::net::IpAddr;
22use std::str::FromStr;
23use std::time::Duration;
24
25use rpc::AntMinerRPCAPI;
26use web::AntMinerWebAPI;
27
28mod rpc;
29mod web;
30
31#[derive(Debug)]
32pub struct AntMinerV2020 {
33 pub ip: IpAddr,
34 pub rpc: AntMinerRPCAPI,
35 pub web: AntMinerWebAPI,
36 pub device_info: DeviceInfo,
37}
38
39enum MinerMode {
40 Sleep,
41 Low,
42 Normal,
43 High,
44}
45
46impl Display for MinerMode {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 let str = match self {
49 MinerMode::Normal => "0".to_string(),
50 MinerMode::Sleep => "1".to_string(),
51 MinerMode::Low => "3".to_string(),
52 _ => "0".to_string(),
53 };
54 write!(f, "{}", str)
55 }
56}
57
58impl AntMinerV2020 {
59 pub fn new(ip: IpAddr, model: MinerModel) -> Self {
60 AntMinerV2020 {
61 ip,
62 rpc: AntMinerRPCAPI::new(ip),
63 web: AntMinerWebAPI::new(ip),
64 device_info: DeviceInfo::new(
65 MinerMake::AntMiner,
66 model,
67 MinerFirmware::Stock,
68 HashAlgorithm::SHA256,
69 ),
70 }
71 }
72
73 pub fn with_auth(
74 ip: IpAddr,
75 model: MinerModel,
76 firmware: MinerFirmware,
77 username: String,
78 password: String,
79 ) -> Self {
80 AntMinerV2020 {
81 ip,
82 rpc: AntMinerRPCAPI::new(ip),
83 web: AntMinerWebAPI::with_auth(ip, username, password),
84 device_info: DeviceInfo::new(
85 MinerMake::AntMiner,
86 model,
87 firmware,
88 HashAlgorithm::SHA256,
89 ),
90 }
91 }
92
93 fn parse_temp_string(temp_str: &str) -> Option<Temperature> {
94 let temps: Vec<f64> = temp_str
95 .split('-')
96 .filter_map(|s| s.parse().ok())
97 .filter(|&temp| temp > 0.0)
98 .collect();
99
100 if !temps.is_empty() {
101 let avg = temps.iter().sum::<f64>() / temps.len() as f64;
102 Some(Temperature::from_celsius(avg))
103 } else {
104 None
105 }
106 }
107
108 fn _calculate_average_temp_s21_hyd(chain: &Value) -> Option<Temperature> {
109 let mut temps = Vec::new();
110
111 if let Some(temp_pic) = chain.get("temp_pic").and_then(|v| v.as_array()) {
112 for i in 1..=3 {
113 if let Some(temp) = temp_pic.get(i).and_then(|v| v.as_f64())
114 && temp != 0.0
115 {
116 temps.push(temp);
117 }
118 }
119 }
120
121 if let Some(temp_pcb) = chain.get("temp_pcb").and_then(|v| v.as_array()) {
122 if let Some(temp) = temp_pcb.get(1).and_then(|v| v.as_f64())
123 && temp != 0.0
124 {
125 temps.push(temp);
126 }
127 if let Some(temp) = temp_pcb.get(3).and_then(|v| v.as_f64())
128 && temp != 0.0
129 {
130 temps.push(temp);
131 }
132 }
133
134 if !temps.is_empty() {
135 let avg = temps.iter().sum::<f64>() / temps.len() as f64;
136 Some(Temperature::from_celsius(avg))
137 } else {
138 None
139 }
140 }
141
142 fn _calculate_average_temp_pcb(chain: &Value) -> Option<Temperature> {
143 if let Some(temp_pcb) = chain.get("temp_pcb").and_then(|v| v.as_array()) {
144 let temps: Vec<f64> = temp_pcb
145 .iter()
146 .filter_map(|v| v.as_f64())
147 .filter(|&temp| temp != 0.0)
148 .collect();
149
150 if !temps.is_empty() {
151 let avg = temps.iter().sum::<f64>() / temps.len() as f64;
152 Some(Temperature::from_celsius(avg))
153 } else {
154 None
155 }
156 } else {
157 None
158 }
159 }
160
161 fn _calculate_average_temp_chip(chain: &Value) -> Option<Temperature> {
162 if let Some(temp_chip) = chain.get("temp_chip").and_then(|v| v.as_array()) {
163 let temps: Vec<f64> = temp_chip
164 .iter()
165 .filter_map(|v| v.as_f64())
166 .filter(|&temp| temp != 0.0)
167 .collect();
168
169 if !temps.is_empty() {
170 let avg = temps.iter().sum::<f64>() / temps.len() as f64;
171 Some(Temperature::from_celsius(avg))
172 } else {
173 None
174 }
175 } else {
176 None
177 }
178 }
179}
180
181#[async_trait]
182impl APIClient for AntMinerV2020 {
183 async fn get_api_result(&self, command: &MinerCommand) -> anyhow::Result<Value> {
184 match command {
185 MinerCommand::RPC { .. } => self.rpc.get_api_result(command).await,
186 MinerCommand::WebAPI { .. } => self.web.get_api_result(command).await,
187 _ => Err(anyhow::anyhow!("Unsupported command type for Antminer API")),
188 }
189 }
190}
191
192impl GetDataLocations for AntMinerV2020 {
193 fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
194 const RPC_VERSION: MinerCommand = MinerCommand::RPC {
195 command: "version",
196 parameters: None,
197 };
198
199 const RPC_STATS: MinerCommand = MinerCommand::RPC {
200 command: "stats",
201 parameters: None,
202 };
203
204 const RPC_SUMMARY: MinerCommand = MinerCommand::RPC {
205 command: "summary",
206 parameters: None,
207 };
208
209 const RPC_POOLS: MinerCommand = MinerCommand::RPC {
210 command: "pools",
211 parameters: None,
212 };
213
214 const WEB_SYSTEM_INFO: MinerCommand = MinerCommand::WebAPI {
215 command: "get_system_info",
216 parameters: None,
217 };
218
219 const WEB_BLINK_STATUS: MinerCommand = MinerCommand::WebAPI {
220 command: "get_blink_status",
221 parameters: None,
222 };
223
224 const WEB_MINER_CONF: MinerCommand = MinerCommand::WebAPI {
225 command: "get_miner_conf",
226 parameters: None,
227 };
228
229 const WEB_SUMMARY: MinerCommand = MinerCommand::WebAPI {
230 command: "summary",
231 parameters: None,
232 };
233
234 const WEB_MINER_TYPE: MinerCommand = MinerCommand::WebAPI {
235 command: "miner_type",
236 parameters: None,
237 };
238
239 match data_field {
240 DataField::Mac => vec![(
241 WEB_SYSTEM_INFO,
242 DataExtractor {
243 func: get_by_pointer,
244 key: Some("/macaddr"),
245 tag: None,
246 },
247 )],
248 DataField::ApiVersion => vec![(
249 RPC_VERSION,
250 DataExtractor {
251 func: get_by_pointer,
252 key: Some("/VERSION/0/API"),
253 tag: None,
254 },
255 )],
256 DataField::FirmwareVersion => vec![(
257 RPC_VERSION,
258 DataExtractor {
259 func: get_by_pointer,
260 key: Some("/VERSION/0/CompileTime"),
261 tag: None,
262 },
263 )],
264 DataField::Hostname => vec![(
265 WEB_SYSTEM_INFO,
266 DataExtractor {
267 func: get_by_pointer,
268 key: Some("/hostname"),
269 tag: None,
270 },
271 )],
272 DataField::ControlBoardVersion => vec![(
273 WEB_MINER_TYPE,
274 DataExtractor {
275 func: get_by_pointer,
276 key: Some("/subtype"),
277 tag: None,
278 },
279 )],
280 DataField::Hashrate => vec![(
281 RPC_SUMMARY,
282 DataExtractor {
283 func: get_by_pointer,
284 key: Some("/SUMMARY/0/GHS 5s"),
285 tag: None,
286 },
287 )],
288 DataField::ExpectedHashrate => vec![(
289 RPC_STATS,
290 DataExtractor {
291 func: get_by_pointer,
292 key: Some("/STATS/1/total_rateideal"),
293 tag: None,
294 },
295 )],
296 DataField::Fans => vec![(
297 RPC_STATS,
298 DataExtractor {
299 func: get_by_pointer,
300 key: Some("/STATS/1"),
301 tag: None,
302 },
303 )],
304 DataField::Hashboards => vec![(
305 RPC_STATS,
306 DataExtractor {
307 func: get_by_pointer,
308 key: Some("/STATS/1"),
309 tag: None,
310 },
311 )],
312 DataField::LightFlashing => vec![(
313 WEB_BLINK_STATUS,
314 DataExtractor {
315 func: get_by_pointer,
316 key: Some("/blink"),
317 tag: None,
318 },
319 )],
320 DataField::IsMining => vec![(
321 WEB_MINER_CONF,
322 DataExtractor {
323 func: get_by_pointer,
324 key: Some("/bitmain-work-mode"),
325 tag: None,
326 },
327 )],
328 DataField::Uptime => vec![(
329 RPC_STATS,
330 DataExtractor {
331 func: get_by_pointer,
332 key: Some("/STATS/1/Elapsed"),
333 tag: None,
334 },
335 )],
336 DataField::Pools => vec![(
337 RPC_POOLS,
338 DataExtractor {
339 func: get_by_pointer,
340 key: Some("/POOLS"),
341 tag: None,
342 },
343 )],
344 DataField::Wattage => vec![(
345 RPC_STATS,
346 DataExtractor {
347 func: get_by_pointer,
348 key: Some("/STATS/1"),
349 tag: None,
350 },
351 )],
352 DataField::SerialNumber => vec![
353 (
354 WEB_SYSTEM_INFO,
355 DataExtractor {
356 func: get_by_pointer,
357 key: Some("/serial_no"), tag: None,
359 },
360 ),
361 (
362 WEB_SYSTEM_INFO,
363 DataExtractor {
364 func: get_by_pointer,
365 key: Some("/serinum"), tag: None,
367 },
368 ),
369 ],
370 DataField::Messages => vec![(
371 WEB_SUMMARY,
372 DataExtractor {
373 func: get_by_pointer,
374 key: Some("/SUMMARY/0/status"),
375 tag: None,
376 },
377 )],
378 _ => vec![],
379 }
380 }
381}
382
383impl GetIP for AntMinerV2020 {
384 fn get_ip(&self) -> IpAddr {
385 self.ip
386 }
387}
388
389impl GetDeviceInfo for AntMinerV2020 {
390 fn get_device_info(&self) -> DeviceInfo {
391 self.device_info
392 }
393}
394
395impl CollectData for AntMinerV2020 {
396 fn get_collector(&self) -> DataCollector<'_> {
397 DataCollector::new(self)
398 }
399}
400
401impl GetMAC for AntMinerV2020 {
402 fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
403 data.extract::<String>(DataField::Mac)
404 .and_then(|s| MacAddr::from_str(&s).ok())
405 }
406}
407
408impl GetHostname for AntMinerV2020 {
409 fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
410 data.extract::<String>(DataField::Hostname)
411 }
412}
413
414impl GetApiVersion for AntMinerV2020 {
415 fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
416 data.extract::<String>(DataField::ApiVersion)
417 }
418}
419
420impl GetFirmwareVersion for AntMinerV2020 {
421 fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
422 data.extract::<String>(DataField::FirmwareVersion)
423 }
424}
425
426impl GetHashboards for AntMinerV2020 {
427 fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
428 let mut hashboards: Vec<BoardData> = Vec::new();
429 let board_count = self.device_info.hardware.boards.unwrap_or(3);
430
431 for idx in 0..board_count {
432 hashboards.push(BoardData {
433 hashrate: None,
434 position: idx,
435 expected_hashrate: None,
436 board_temperature: None,
437 intake_temperature: None,
438 outlet_temperature: None,
439 expected_chips: self.device_info.hardware.chips,
440 working_chips: None,
441 serial_number: None,
442 chips: vec![],
443 voltage: None,
444 frequency: None,
445 tuned: Some(false),
446 active: Some(false),
447 });
448 }
449
450 if let Some(stats_data) = data.get(&DataField::Hashboards) {
451 for idx in 1..=board_count {
452 let board_idx = (idx - 1) as usize;
453 if board_idx >= hashboards.len() {
454 break;
455 }
456
457 if let Some(hashrate) = stats_data
458 .get(format!("chain_rate{}", idx))
459 .and_then(|v| v.as_str())
460 .and_then(|s| s.parse::<f64>().ok())
461 .map(|f| {
462 HashRate {
463 value: f,
464 unit: HashRateUnit::GigaHash,
465 algo: String::from("SHA256"),
466 }
467 .as_unit(HashRateUnit::TeraHash)
468 })
469 {
470 hashboards[board_idx].hashrate = Some(hashrate);
471 }
472
473 if let Some(working_chips) = stats_data
474 .get(format!("chain_acn{}", idx))
475 .and_then(|v| v.as_u64())
476 .map(|u| u as u16)
477 {
478 hashboards[board_idx].working_chips = Some(working_chips);
479 }
480
481 if let Some(board_temp) = stats_data
482 .get(format!("temp_pcb{}", idx))
483 .and_then(|v| v.as_str())
484 .and_then(Self::parse_temp_string)
485 {
486 hashboards[board_idx].board_temperature = Some(board_temp);
487 }
488
489 if let Some(frequency) = stats_data
490 .get(format!("freq{}", idx))
491 .and_then(|v| v.as_u64())
492 .map(|f| Frequency::from_megahertz(f as f64))
493 {
494 hashboards[board_idx].frequency = Some(frequency);
495 }
496
497 let has_hashrate = hashboards[board_idx]
498 .hashrate
499 .as_ref()
500 .map(|h| h.value > 0.0)
501 .unwrap_or(false);
502 let has_chips = hashboards[board_idx]
503 .working_chips
504 .map(|chips| chips > 0)
505 .unwrap_or(false);
506
507 hashboards[board_idx].active = Some(has_hashrate || has_chips);
508 hashboards[board_idx].tuned = Some(has_hashrate || has_chips);
509 }
510 }
511
512 hashboards
513 }
514}
515
516impl GetHashrate for AntMinerV2020 {
517 fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
518 data.extract_map::<f64, _>(DataField::Hashrate, |f| {
519 HashRate {
520 value: f,
521 unit: HashRateUnit::GigaHash,
522 algo: String::from("SHA256"),
523 }
524 .as_unit(HashRateUnit::TeraHash)
525 })
526 }
527}
528
529impl GetExpectedHashrate for AntMinerV2020 {
530 fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
531 data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| {
532 HashRate {
533 value: f,
534 unit: HashRateUnit::GigaHash,
535 algo: String::from("SHA256"),
536 }
537 .as_unit(HashRateUnit::TeraHash)
538 })
539 }
540}
541
542impl GetFans for AntMinerV2020 {
543 fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
544 let mut fans: Vec<FanData> = Vec::new();
545
546 if let Some(stats_data) = data.get(&DataField::Fans) {
547 for i in 1..=self.device_info.hardware.fans.unwrap_or(4) {
548 if let Some(fan_speed) =
549 stats_data.get(format!("fan{}", i)).and_then(|v| v.as_f64())
550 && fan_speed > 0.0
551 {
552 fans.push(FanData {
553 position: (i - 1) as i16,
554 rpm: Some(AngularVelocity::from_rpm(fan_speed)),
555 });
556 }
557 }
558 }
559
560 fans
561 }
562}
563
564impl GetLightFlashing for AntMinerV2020 {
565 fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
566 data.extract::<bool>(DataField::LightFlashing).or_else(|| {
567 data.extract::<String>(DataField::LightFlashing)
568 .map(|s| s.to_lowercase() == "true" || s == "1")
569 })
570 }
571}
572
573impl GetUptime for AntMinerV2020 {
574 fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
575 data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
576 }
577}
578
579impl GetIsMining for AntMinerV2020 {
580 fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
581 data.extract::<String>(DataField::IsMining)
582 .map(|status| {
583 let status_lower = status.to_lowercase();
584 status_lower != "stopped" && status_lower != "idle" && status_lower != "sleep"
585 })
586 .or_else(|| data.extract::<f64>(DataField::Hashrate).map(|hr| hr > 0.0))
587 .unwrap_or(false)
588 }
589}
590
591impl GetPools for AntMinerV2020 {
592 fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
593 let mut pools: Vec<PoolData> = Vec::new();
594
595 if let Some(pools_data) = data.get(&DataField::Pools)
596 && let Some(pools_array) = pools_data.as_array()
597 {
598 for (idx, pool_info) in pools_array.iter().enumerate() {
599 let url = pool_info
600 .get("URL")
601 .and_then(|v| v.as_str())
602 .map(|s| PoolURL::from(s.to_string()));
603
604 let user = pool_info
605 .get("User")
606 .and_then(|v| v.as_str())
607 .map(String::from);
608
609 let alive = pool_info
610 .get("Status")
611 .and_then(|v| v.as_str())
612 .map(|s| s == "Alive");
613
614 let active = pool_info.get("Stratum Active").and_then(|v| v.as_bool());
615
616 let accepted_shares = pool_info.get("Accepted").and_then(|v| v.as_u64());
617
618 let rejected_shares = pool_info.get("Rejected").and_then(|v| v.as_u64());
619
620 pools.push(PoolData {
621 position: Some(idx as u16),
622 url,
623 accepted_shares,
624 rejected_shares,
625 active,
626 alive,
627 user,
628 });
629 }
630 }
631
632 pools
633 }
634}
635
636impl GetSerialNumber for AntMinerV2020 {
637 fn parse_serial_number(&self, data: &HashMap<DataField, Value>) -> Option<String> {
638 data.extract::<String>(DataField::SerialNumber)
639 }
640}
641
642impl GetControlBoardVersion for AntMinerV2020 {
643 fn parse_control_board_version(
644 &self,
645 data: &HashMap<DataField, Value>,
646 ) -> Option<MinerControlBoard> {
647 data.extract::<String>(DataField::ControlBoardVersion)
648 .and_then(|s| MinerControlBoard::from_str(s.split("_").collect::<Vec<&str>>()[0]).ok())
649 }
650}
651
652impl GetWattage for AntMinerV2020 {
653 fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
654 if let Some(stats_data) = data.get(&DataField::Wattage) {
655 if let Some(chain_power) = stats_data.get("chain_power")
656 && let Some(power_str) = chain_power.as_str()
657 {
658 if let Some(watt_part) = power_str.split_whitespace().next()
660 && let Ok(watts) = watt_part.parse::<f64>()
661 {
662 return Some(Power::from_watts(watts));
663 }
664 }
665
666 if let Some(power) = stats_data
667 .get("power")
668 .or_else(|| stats_data.get("Power"))
669 .and_then(|v| v.as_f64())
670 {
671 return Some(Power::from_watts(power));
672 }
673 }
674 None
675 }
676}
677
678impl GetWattageLimit for AntMinerV2020 {}
679
680impl GetFluidTemperature for AntMinerV2020 {
681 fn parse_fluid_temperature(&self, data: &HashMap<DataField, Value>) -> Option<Temperature> {
682 if self.device_info.model.to_string().contains("S21+ Hyd")
684 && let Some(hashboards_data) = data.get(&DataField::Hashboards)
685 && let Some(chains) = hashboards_data.as_array()
686 {
687 let mut temps = Vec::new();
688
689 for chain in chains {
690 if let Some(temp_pcb) = chain.get("temp_pcb").and_then(|v| v.as_array()) {
691 if let Some(inlet) = temp_pcb.first().and_then(|v| v.as_f64())
693 && inlet != 0.0
694 {
695 temps.push(inlet);
696 }
697 if let Some(outlet) = temp_pcb.get(2).and_then(|v| v.as_f64())
698 && outlet != 0.0
699 {
700 temps.push(outlet);
701 }
702 }
703 }
704
705 if !temps.is_empty() {
706 let avg = temps.iter().sum::<f64>() / temps.len() as f64;
707 return Some(Temperature::from_celsius(avg));
708 }
709 }
710 None
711 }
712}
713
714impl GetPsuFans for AntMinerV2020 {}
715
716impl GetMessages for AntMinerV2020 {
717 fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
718 let mut messages = Vec::new();
719
720 if let Some(status_data) = data.get(&DataField::Messages)
721 && let Some(status_array) = status_data.as_array()
722 {
723 for (idx, item) in status_array.iter().enumerate() {
724 if let Some(status) = item.get("status").and_then(|v| v.as_str())
725 && status != "s"
726 {
727 let message_text = item
729 .get("msg")
730 .and_then(|v| v.as_str())
731 .unwrap_or("Unknown error")
732 .to_string();
733
734 let severity = match status.to_lowercase().as_str() {
735 "e" => MessageSeverity::Error,
736 "w" => MessageSeverity::Warning,
737 _ => MessageSeverity::Info,
738 };
739
740 messages.push(MinerMessage::new(0, idx as u64, message_text, severity));
741 }
742 }
743 }
744
745 messages
746 }
747}
748
749#[async_trait]
750impl SetFaultLight for AntMinerV2020 {
751 #[allow(unused_variables)]
752 async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
753 Ok(self.web.blink(fault).await.is_ok())
754 }
755}
756
757#[async_trait]
758impl SetPowerLimit for AntMinerV2020 {
759 #[allow(unused_variables)]
760 async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
761 anyhow::bail!("Unsupported command");
762 }
763}
764
765#[async_trait]
766impl Restart for AntMinerV2020 {
767 async fn restart(&self) -> anyhow::Result<bool> {
768 Ok(self.web.reboot().await.is_ok())
769 }
770}
771
772#[async_trait]
773impl Pause for AntMinerV2020 {
774 #[allow(unused_variables)]
775 async fn pause(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
776 Ok(self
777 .web
778 .set_miner_conf(json!({"miner-mode": MinerMode::Sleep.to_string()}))
779 .await
780 .is_ok())
781 }
782}
783
784#[async_trait]
785impl Resume for AntMinerV2020 {
786 #[allow(unused_variables)]
787 async fn resume(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
788 Ok(self
789 .web
790 .set_miner_conf(json!({"miner-mode": MinerMode::Normal.to_string()}))
791 .await
792 .is_ok())
793 }
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799 use crate::data::device::models::antminer::AntMinerModel;
800 use crate::test::api::MockAPIClient;
801 use crate::test::json::bmminer::antminer_modern::{
802 AM_DEVS, AM_POOLS, AM_STATS, AM_SUMMARY, AM_VERSION,
803 };
804
805 #[tokio::test]
806 async fn test_antminer() {
807 let miner = AntMinerV2020::new(
808 IpAddr::from([127, 0, 0, 1]),
809 MinerModel::AntMiner(AntMinerModel::S19Pro),
810 );
811
812 let mut results = HashMap::new();
813
814 let stats_cmd = MinerCommand::RPC {
815 command: "stats",
816 parameters: None,
817 };
818
819 let version_cmd = MinerCommand::RPC {
820 command: "version",
821 parameters: None,
822 };
823
824 let summary_cmd = MinerCommand::RPC {
825 command: "summary",
826 parameters: None,
827 };
828
829 let devs_cmd = MinerCommand::RPC {
830 command: "devs",
831 parameters: None,
832 };
833
834 let pools_cmd = MinerCommand::RPC {
835 command: "pools",
836 parameters: None,
837 };
838
839 results.insert(stats_cmd, Value::from_str(AM_STATS).unwrap());
840 results.insert(version_cmd, Value::from_str(AM_VERSION).unwrap());
841 results.insert(summary_cmd, Value::from_str(AM_SUMMARY).unwrap());
842 results.insert(devs_cmd, Value::from_str(AM_DEVS).unwrap());
843 results.insert(pools_cmd, Value::from_str(AM_POOLS).unwrap());
844
845 let mock_api = MockAPIClient::new(results);
846
847 let mut collector = DataCollector::new_with_client(&miner, &mock_api);
848 let data = collector.collect_all().await;
849
850 let miner_data = miner.parse_data(data);
851
852 assert_eq!(miner_data.ip.to_string(), "127.0.0.1".to_owned());
853 assert_eq!(miner_data.hashboards.len(), 3);
854 assert_eq!(miner_data.light_flashing, None);
855 assert_eq!(miner_data.fans.len(), 4);
856 assert_eq!(
857 miner_data.expected_hashrate.unwrap(),
858 HashRate {
859 value: 110.0,
860 unit: HashRateUnit::TeraHash,
861 algo: "SHA256".to_string(),
862 }
863 );
864 assert_eq!(
865 miner_data.hashrate.unwrap(),
866 HashRate {
867 value: 110.56689,
868 unit: HashRateUnit::TeraHash,
869 algo: "SHA256".to_string(),
870 }
871 );
872 }
873}