1#![allow(dead_code)]
2
3use crate::data::board::{BoardData, ChipData};
4use crate::data::device::MinerMake;
5use crate::data::device::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
6use crate::data::fan::FanData;
7use crate::data::hashrate::{HashRate, HashRateUnit};
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};
14
15use crate::miners::api::RPCAPIClient;
16use crate::miners::backends::avalonminer::shared::rpc::AvalonMinerRPCAPI;
17use anyhow::{Result, anyhow};
18use async_trait::async_trait;
19use macaddr::MacAddr;
20use measurements::{AngularVelocity, Power, Temperature, Voltage};
21use serde_json::{Value, json};
22use std::collections::HashMap;
23use std::net::IpAddr;
24use std::str::FromStr;
25use std::time::{Duration, SystemTime, UNIX_EPOCH};
26
27#[derive(Debug)]
28pub struct AvalonQMiner {
29 ip: IpAddr,
30 rpc: AvalonMinerRPCAPI,
31 device_info: DeviceInfo,
32}
33
34impl AvalonQMiner {
35 pub fn new(ip: IpAddr, model: MinerModel, miner_firmware: MinerFirmware) -> Self {
36 Self {
37 ip,
38 rpc: AvalonMinerRPCAPI::new(ip),
39 device_info: DeviceInfo::new(
40 MinerMake::AvalonMiner,
41 model,
42 miner_firmware,
43 HashAlgorithm::SHA256,
44 ),
45 }
46 }
47
48 pub async fn reboot(&self) -> Result<bool> {
50 let data = self.rpc.send_command("restart", false, None).await?;
51
52 if let Some(status) = data.get("STATUS").and_then(|s| s.as_str()) {
53 return Ok(status == "RESTART");
54 }
55
56 Ok(false)
57 }
58}
59#[async_trait]
60impl Pause for AvalonQMiner {
61 async fn pause(&self, after: Option<Duration>) -> Result<bool> {
62 let offset = after.unwrap_or(Duration::from_secs(5));
63 let shutdown_time = SystemTime::now() + offset;
64
65 let timestamp = shutdown_time
66 .duration_since(UNIX_EPOCH)
67 .expect("Shutdown time is before UNIX epoch")
68 .as_secs();
69
70 let data = self
71 .rpc
72 .send_command(
73 "ascset",
74 false,
75 Some(json!(["0", format!("softoff,1:{}", timestamp)])),
76 )
77 .await?;
78
79 if let Some(status) = data.get("STATUS").and_then(|s| s.as_array())
80 && !status.is_empty()
81 && let Some(status_code) = status[0].get("STATUS").and_then(|s| s.as_str())
82 && status_code == "I"
83 && let Some(msg) = status[0].get("Msg").and_then(|m| m.as_str())
84 {
85 return Ok(msg.contains("success softoff"));
86 }
87
88 Ok(false)
89 }
90}
91#[async_trait]
92impl Resume for AvalonQMiner {
93 async fn resume(&self, after: Option<Duration>) -> Result<bool> {
94 let offset = after.unwrap_or(Duration::from_secs(5));
95 let shutdown_time = SystemTime::now() + offset;
96
97 let timestamp = shutdown_time
98 .duration_since(UNIX_EPOCH)
99 .expect("Shutdown time is before UNIX epoch")
100 .as_secs();
101
102 let data = self
103 .rpc
104 .send_command(
105 "ascset",
106 false,
107 Some(json!(["0", format!("softon,1:{}", timestamp)])),
108 )
109 .await?;
110
111 if let Some(status) = data.get("STATUS").and_then(|s| s.as_array())
112 && !status.is_empty()
113 && let Some(status_code) = status[0].get("STATUS").and_then(|s| s.as_str())
114 && status_code == "I"
115 && let Some(msg) = status[0].get("Msg").and_then(|m| m.as_str())
116 {
117 return Ok(msg.contains("success softon"));
118 }
119 Ok(false)
120 }
121}
122#[async_trait]
123impl SetFaultLight for AvalonQMiner {
124 async fn set_fault_light(&self, fault: bool) -> Result<bool> {
125 let command = if fault { "1-1" } else { "1-0" };
126
127 let data = self
128 .rpc
129 .send_command("ascset", false, Some(json!(["0", "led", command])))
130 .await?;
131
132 if let Some(status) = data.get("STATUS").and_then(|s| s.as_array())
133 && let Some(msg) = status
134 .first()
135 .and_then(|s| s.get("Msg"))
136 .and_then(|m| m.as_str())
137 {
138 return Ok(msg == "ASC 0 set OK");
139 }
140
141 Err(anyhow!("Failed to set fault light to {}", command))
142 }
143}
144
145#[async_trait]
146impl SetPowerLimit for AvalonQMiner {
147 async fn set_power_limit(&self, limit: Power) -> Result<bool> {
148 let data = self
149 .rpc
150 .send_command(
151 "ascset",
152 false,
153 Some(json!(["0", "worklevel,set", limit.to_string()])),
154 )
155 .await?;
156
157 if let Some(status) = data.get("STATUS").and_then(|s| s.as_array())
158 && !status.is_empty()
159 && let Some(msg) = status[0].get("Msg").and_then(|m| m.as_str())
160 {
161 return Ok(msg == "ASC 0 set OK");
162 }
163
164 Err(anyhow!("Failed to set power limit"))
165 }
166}
167
168impl GetDataLocations for AvalonQMiner {
169 fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
170 let version_cmd: MinerCommand = MinerCommand::RPC {
171 command: "version",
172 parameters: None,
173 };
174 let stats_cmd: MinerCommand = MinerCommand::RPC {
175 command: "stats",
176 parameters: None,
177 };
178 let devs_cmd: MinerCommand = MinerCommand::RPC {
179 command: "devs",
180 parameters: None,
181 };
182 let pools_cmd: MinerCommand = MinerCommand::RPC {
183 command: "pools",
184 parameters: None,
185 };
186
187 match data_field {
188 DataField::Mac => vec![(
189 version_cmd,
190 DataExtractor {
191 func: get_by_pointer,
192 key: Some("/VERSION/0/MAC"),
193 tag: None,
194 },
195 )],
196 DataField::ApiVersion => vec![(
197 version_cmd,
198 DataExtractor {
199 func: get_by_pointer,
200 key: Some("/VERSION/0/API"),
201 tag: None,
202 },
203 )],
204 DataField::FirmwareVersion => vec![(
205 version_cmd,
206 DataExtractor {
207 func: get_by_pointer,
208 key: Some("/VERSION/0/CGMiner"),
209 tag: None,
210 },
211 )],
212 DataField::Hashrate => vec![(
213 devs_cmd,
214 DataExtractor {
215 func: get_by_pointer,
216 key: Some("/DEVS/0/MHS 1m"),
217 tag: None,
218 },
219 )],
220 DataField::ExpectedHashrate => vec![(
221 stats_cmd,
222 DataExtractor {
223 func: get_by_pointer,
224 key: Some("/STATS/0/MM ID0:Summary/STATS/GHSmm"),
225 tag: None,
226 },
227 )],
228 DataField::Hashboards => vec![(
229 stats_cmd,
230 DataExtractor {
231 func: get_by_pointer,
232 key: Some("/STATS/0/HBinfo"),
233 tag: None,
234 },
235 )],
236 DataField::AverageTemperature => vec![(
237 stats_cmd,
238 DataExtractor {
239 func: get_by_pointer,
240 key: Some("/STATS/0/MM ID0:Summary/STATS/ITemp"),
241 tag: None,
242 },
243 )],
244 DataField::WattageLimit => vec![(
245 stats_cmd,
246 DataExtractor {
247 func: get_by_pointer,
248 key: Some("/STATS/0/MM ID0:Summary/STATS/MPO"),
249 tag: None,
250 },
251 )],
252 DataField::Wattage => vec![(
253 stats_cmd,
254 DataExtractor {
255 func: get_by_pointer,
256 key: Some("/STATS/0/MM ID0:Summary/STATS/WALLPOWER"),
257 tag: None,
258 },
259 )],
260 DataField::Fans => vec![(
261 stats_cmd,
262 DataExtractor {
263 func: get_by_pointer,
264 key: Some("/STATS/0/MM ID0:Summary/STATS"),
265 tag: None,
266 },
267 )],
268 DataField::LightFlashing => vec![(
269 stats_cmd,
270 DataExtractor {
271 func: get_by_pointer,
272 key: Some("/STATS/0/MM ID0:Summary/STATS/Led"),
273 tag: None,
274 },
275 )],
276 DataField::Uptime => vec![(
277 stats_cmd,
278 DataExtractor {
279 func: get_by_pointer,
280 key: Some("/STATS/0/Elapsed"),
281 tag: None,
282 },
283 )],
284 DataField::Pools => vec![(
285 pools_cmd,
286 DataExtractor {
287 func: get_by_pointer,
288 key: Some("/POOLS"),
289 tag: None,
290 },
291 )],
292 _ => vec![],
293 }
294 }
295}
296
297impl GetIP for AvalonQMiner {
298 fn get_ip(&self) -> IpAddr {
299 self.ip
300 }
301}
302
303impl GetDeviceInfo for AvalonQMiner {
304 fn get_device_info(&self) -> DeviceInfo {
305 self.device_info
306 }
307}
308
309impl CollectData for AvalonQMiner {
310 fn get_collector(&self) -> DataCollector<'_> {
311 DataCollector::new(self, &self.rpc)
312 }
313}
314
315impl GetMAC for AvalonQMiner {
316 fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
317 data.extract::<String>(DataField::Mac).and_then(|raw| {
318 let mut mac = raw.trim().to_lowercase();
319 if mac.len() == 12 && !mac.contains(':') {
321 let mut colon = String::with_capacity(17);
322 for (i, byte) in mac.chars().enumerate() {
323 if i > 0 && i % 2 == 0 {
324 colon.push(':');
325 }
326 colon.push(byte);
327 }
328 mac = colon;
329 }
330 MacAddr::from_str(&mac).ok()
331 })
332 }
333}
334
335impl GetSerialNumber for AvalonQMiner {}
336
337impl GetHostname for AvalonQMiner {}
338
339impl GetApiVersion for AvalonQMiner {
340 fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
341 data.extract::<String>(DataField::ApiVersion)
342 }
343}
344
345impl GetFirmwareVersion for AvalonQMiner {
346 fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
347 data.extract::<String>(DataField::FirmwareVersion)
348 }
349}
350
351impl GetControlBoardVersion for AvalonQMiner {}
352
353impl GetHashboards for AvalonQMiner {
354 fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
355 let hw = &self.device_info.hardware;
356 let board_cnt = hw.boards.unwrap_or(1) as usize;
357 let chips_per = hw.chips.unwrap_or(0);
358
359 let hb_info = match data.get(&DataField::Hashboards).and_then(|v| v.as_object()) {
360 Some(v) => v,
361 _ => return Vec::new(),
362 };
363
364 let summary = match data.get(&DataField::Fans) {
365 Some(v) => v,
366 _ => return Vec::new(),
367 }; (0..board_cnt)
370 .map(|idx| {
371 let key = format!("HB{idx}");
372
373 let intake = summary["ITemp"][idx]
375 .as_f64()
376 .map(Temperature::from_celsius);
377
378 let board_t = summary["HBITemp"][idx]
379 .as_f64()
380 .map(Temperature::from_celsius);
381
382 let hashrate = summary["MGHS"][idx].as_f64().map(|r| HashRate {
383 value: r,
384 unit: HashRateUnit::GigaHash,
385 algo: "SHA256".into(),
386 });
387
388 let temps: Vec<f64> = hb_info[&key]["PVT_T0"]
390 .as_array()
391 .map(|a| a.iter().filter_map(|v| v.as_f64()).collect())
392 .unwrap_or_default();
393
394 let volts: Vec<f64> = hb_info[&key]["PVT_V0"]
395 .as_array()
396 .map(|a| a.iter().filter_map(|v| v.as_f64()).collect())
397 .unwrap_or_default();
398
399 let works: Vec<f64> = hb_info[&key]["MW0"]
400 .as_array()
401 .map(|a| a.iter().filter_map(|v| v.as_f64()).collect())
402 .unwrap_or_default();
403
404 let chips: Vec<ChipData> = temps
405 .iter()
406 .zip(volts.iter())
407 .zip(works.iter())
408 .enumerate()
409 .map(|(pos, ((&t, &v), &w))| ChipData {
410 position: pos as u16,
411 temperature: Some(Temperature::from_celsius(t)),
412 voltage: Some(Voltage::from_millivolts(v)),
413 working: Some(w > 0.0),
414 ..Default::default()
415 })
416 .collect();
417
418 BoardData {
419 position: idx as u8,
420 expected_chips: Some(chips_per),
421 working_chips: Some(chips.len() as u16),
422 chips: chips.clone(),
423 intake_temperature: intake,
424 board_temperature: board_t,
425 hashrate,
426 active: Some(!chips.is_empty()),
427 ..Default::default()
428 }
429 })
430 .collect()
431 }
432}
433
434impl GetHashrate for AvalonQMiner {
435 fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
436 data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
437 value: f,
438 unit: HashRateUnit::MegaHash,
439 algo: "SHA256".into(),
440 })
441 }
442}
443
444impl GetExpectedHashrate for AvalonQMiner {
445 fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
446 data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| HashRate {
447 value: f,
448 unit: HashRateUnit::GigaHash,
449 algo: "SHA256".into(),
450 })
451 }
452}
453
454impl GetFans for AvalonQMiner {
455 fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
456 let stats = match data.get(&DataField::Fans) {
457 Some(v) => v,
458 _ => return Vec::new(),
459 };
460
461 let expected_fans = self.device_info.hardware.fans.unwrap_or(0) as usize;
462 if expected_fans == 0 {
463 return Vec::new();
464 }
465
466 (1..=expected_fans)
467 .filter_map(|idx| {
468 let key = format!("Fan{idx}");
469 stats
470 .get(&key)
471 .and_then(|val| val.as_f64())
472 .map(|rpm| FanData {
473 position: idx as i16,
474 rpm: Some(AngularVelocity::from_rpm(rpm)),
475 })
476 })
477 .collect()
478 }
479}
480
481impl GetPsuFans for AvalonQMiner {}
482
483impl GetWattage for AvalonQMiner {
484 fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
485 data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
486 }
487}
488
489impl GetWattageLimit for AvalonQMiner {
490 fn parse_wattage_limit(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
491 data.extract_map::<f64, _>(DataField::WattageLimit, Power::from_watts)
492 }
493}
494
495impl GetLightFlashing for AvalonQMiner {
496 fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
497 data.extract::<bool>(DataField::LightFlashing)
498 }
499}
500
501impl GetMessages for AvalonQMiner {}
502
503impl GetUptime for AvalonQMiner {
504 fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
505 data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
506 }
507}
508
509impl GetFluidTemperature for AvalonQMiner {}
510impl GetIsMining for AvalonQMiner {}
511
512impl GetPools for AvalonQMiner {
513 fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
514 data.get(&DataField::Pools)
515 .and_then(|v| v.as_array())
516 .map(|slice| slice.to_vec())
517 .unwrap_or_default()
518 .into_iter()
519 .enumerate()
520 .map(|(idx, pool)| PoolData {
521 url: pool
522 .get("URL")
523 .and_then(|v| v.as_str())
524 .map(|x| PoolURL::from(x.to_owned())),
525 user: pool.get("User").and_then(|v| v.as_str()).map(|s| s.into()),
526 position: Some(idx as u16),
527 alive: pool
528 .get("Status")
529 .and_then(|v| v.as_str())
530 .map(|s| s == "Alive"),
531 active: pool.get("Stratum Active").and_then(|v| v.as_bool()),
532 accepted_shares: pool.get("Accepted").and_then(|v| v.as_u64()),
533 rejected_shares: pool.get("Rejected").and_then(|v| v.as_u64()),
534 })
535 .collect()
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542 use crate::data::device::models::avalon::AvalonMinerModel::AvalonHomeQ;
543 use crate::test::api::MockAPIClient;
544 use crate::test::json::cgminer::avalon::{
545 DEVS_COMMAND, PARSED_STATS_COMMAND, POOLS_COMMAND, VERSION_COMMAND,
546 };
547
548 #[tokio::test]
549
550 async fn test_avalon_home_q() -> Result<()> {
551 let miner = AvalonQMiner::new(
552 IpAddr::from([127, 0, 0, 1]),
553 MinerModel::Avalon(AvalonHomeQ),
554 MinerFirmware::Stock,
555 );
556
557 let mut results = HashMap::new();
558 let version_cmd: MinerCommand = MinerCommand::RPC {
559 command: "version",
560 parameters: None,
561 };
562 let stats_cmd: MinerCommand = MinerCommand::RPC {
563 command: "stats",
564 parameters: None,
565 };
566 let devs_cmd: MinerCommand = MinerCommand::RPC {
567 command: "devs",
568 parameters: None,
569 };
570 let pools_cmd: MinerCommand = MinerCommand::RPC {
571 command: "pools",
572 parameters: None,
573 };
574
575 results.insert(stats_cmd, Value::from_str(PARSED_STATS_COMMAND)?);
576 results.insert(devs_cmd, Value::from_str(DEVS_COMMAND)?);
577 results.insert(pools_cmd, Value::from_str(POOLS_COMMAND)?);
578 results.insert(version_cmd, Value::from_str(VERSION_COMMAND)?);
579
580 let mock_api = MockAPIClient::new(results);
581 let mut collector = DataCollector::new(&miner, &mock_api);
582 let data = collector.collect_all().await;
583
584 let miner_data = miner.parse_data(data);
585
586 assert_eq!(miner_data.uptime, Some(Duration::from_secs(37819)));
587 assert_eq!(miner_data.wattage_limit, Some(Power::from_watts(800.0)));
588 assert_eq!(miner_data.fans.len(), 4);
589 assert_eq!(miner_data.hashboards[0].chips.len(), 160);
590
591 Ok(())
592 }
593}