1use anyhow::{Result, anyhow};
2use async_trait::async_trait;
3use macaddr::MacAddr;
4use measurements::{AngularVelocity, Frequency, Power, Temperature, Voltage};
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, ChipData};
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::pool::{PoolData, PoolURL};
17use crate::miners::backends::traits::*;
18use crate::miners::commands::MinerCommand;
19use crate::miners::data::{
20 DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer,
21};
22
23use web::VnishWebAPI;
24
25mod web;
26
27#[derive(Debug)]
28pub struct VnishV120 {
29 ip: IpAddr,
30 web: VnishWebAPI,
31 device_info: DeviceInfo,
32}
33
34impl VnishV120 {
35 pub fn new(ip: IpAddr, model: MinerModel) -> Self {
36 VnishV120 {
37 ip,
38 web: VnishWebAPI::new(ip, 80),
39 device_info: DeviceInfo::new(
40 MinerMake::from(model),
41 model,
42 MinerFirmware::VNish,
43 HashAlgorithm::SHA256,
44 ),
45 }
46 }
47}
48
49#[async_trait]
50impl APIClient for VnishV120 {
51 async fn get_api_result(&self, command: &MinerCommand) -> Result<Value> {
52 match command {
53 MinerCommand::WebAPI { .. } => self.web.get_api_result(command).await,
54 _ => Err(anyhow!("Unsupported command type for Vnish API")),
55 }
56 }
57}
58
59impl GetDataLocations for VnishV120 {
60 fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
61 fn cmd(endpoint: &'static str) -> MinerCommand {
62 MinerCommand::WebAPI {
63 command: endpoint,
64 parameters: None,
65 }
66 }
67
68 let info_cmd = cmd("info");
69 let status_cmd = cmd("status");
70 let summary_cmd = cmd("summary");
71 let chains_cmd = cmd("chains");
72 let factory_info_cmd = cmd("chains/factory-info");
73
74 match data_field {
75 DataField::Mac => vec![(
76 info_cmd,
77 DataExtractor {
78 func: get_by_pointer,
79 key: Some("/system/network_status/mac"),
80 tag: None,
81 },
82 )],
83 DataField::SerialNumber => vec![
84 (
85 factory_info_cmd,
86 DataExtractor {
87 func: get_by_pointer,
88 key: Some("/psu_serial"),
89 tag: None,
90 },
91 ),
92 (
93 info_cmd,
94 DataExtractor {
95 func: get_by_pointer,
96 key: Some("/serial"),
97 tag: None,
98 },
99 ),
100 ],
101 DataField::Hostname => vec![(
102 info_cmd,
103 DataExtractor {
104 func: get_by_pointer,
105 key: Some("/system/network_status/hostname"),
106 tag: None,
107 },
108 )],
109 DataField::ApiVersion => vec![(
110 info_cmd,
111 DataExtractor {
112 func: get_by_pointer,
113 key: Some("/fw_version"),
114 tag: None,
115 },
116 )],
117 DataField::FirmwareVersion => vec![(
118 info_cmd,
119 DataExtractor {
120 func: get_by_pointer,
121 key: Some("/fw_version"),
122 tag: None,
123 },
124 )],
125 DataField::ControlBoardVersion => vec![(
126 info_cmd,
127 DataExtractor {
128 func: get_by_pointer,
129 key: Some("/platform"),
130 tag: None,
131 },
132 )],
133 DataField::Uptime => vec![(
134 info_cmd,
135 DataExtractor {
136 func: get_by_pointer,
137 key: Some("/system/uptime"),
138 tag: None,
139 },
140 )],
141 DataField::Hashrate => vec![(
142 summary_cmd,
143 DataExtractor {
144 func: get_by_pointer,
145 key: Some("/miner/hr_realtime"),
146 tag: None,
147 },
148 )],
149 DataField::ExpectedHashrate => vec![
150 (
151 factory_info_cmd,
152 DataExtractor {
153 func: get_by_pointer,
154 key: Some("/hr_stock"),
155 tag: None,
156 },
157 ),
158 (
159 summary_cmd,
160 DataExtractor {
161 func: get_by_pointer,
162 key: Some("/miner/hr_stock"),
163 tag: None,
164 },
165 ),
166 ],
167 DataField::Wattage => vec![(
168 summary_cmd,
169 DataExtractor {
170 func: get_by_pointer,
171 key: Some("/miner/power_consumption"),
172 tag: None,
173 },
174 )],
175 DataField::Fans => vec![(
176 summary_cmd,
177 DataExtractor {
178 func: get_by_pointer,
179 key: Some("/miner/cooling/fans"),
180 tag: None,
181 },
182 )],
183 DataField::Hashboards => vec![
184 (
185 summary_cmd,
186 DataExtractor {
187 func: get_by_pointer,
188 key: Some("/miner/chains"),
189 tag: None,
190 },
191 ),
192 (
193 chains_cmd,
194 DataExtractor {
195 func: get_by_pointer,
196 key: Some(""),
197 tag: None,
198 },
199 ),
200 ],
201 DataField::Pools => vec![(
202 summary_cmd,
203 DataExtractor {
204 func: get_by_pointer,
205 key: Some("/miner/pools"),
206 tag: None,
207 },
208 )],
209 DataField::IsMining => vec![(
210 status_cmd,
211 DataExtractor {
212 func: get_by_pointer,
213 key: Some("/miner_state"),
214 tag: None,
215 },
216 )],
217 DataField::LightFlashing => vec![(
218 status_cmd,
219 DataExtractor {
220 func: get_by_pointer,
221 key: Some("/find_miner"),
222 tag: None,
223 },
224 )],
225 _ => vec![],
226 }
227 }
228}
229
230impl GetIP for VnishV120 {
231 fn get_ip(&self) -> IpAddr {
232 self.ip
233 }
234}
235
236impl GetDeviceInfo for VnishV120 {
237 fn get_device_info(&self) -> DeviceInfo {
238 self.device_info
239 }
240}
241
242impl CollectData for VnishV120 {
243 fn get_collector(&self) -> DataCollector<'_> {
244 DataCollector::new(self)
245 }
246}
247
248impl GetMAC for VnishV120 {
249 fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
250 data.extract::<String>(DataField::Mac)
251 .and_then(|s| MacAddr::from_str(&s).ok())
252 }
253}
254
255impl GetSerialNumber for VnishV120 {
256 fn parse_serial_number(&self, data: &HashMap<DataField, Value>) -> Option<String> {
257 data.extract::<String>(DataField::SerialNumber)
258 }
259}
260
261impl GetHostname for VnishV120 {
262 fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
263 data.extract::<String>(DataField::Hostname)
264 }
265}
266
267impl GetApiVersion for VnishV120 {
268 fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
269 data.extract::<String>(DataField::ApiVersion)
270 }
271}
272
273impl GetFirmwareVersion for VnishV120 {
274 fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
275 data.extract::<String>(DataField::FirmwareVersion)
276 }
277}
278
279impl GetControlBoardVersion for VnishV120 {
280 fn parse_control_board_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
281 data.extract::<String>(DataField::ControlBoardVersion)
282 }
283}
284
285impl GetHashboards for VnishV120 {
286 fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
287 let mut hashboards: Vec<BoardData> = Vec::new();
288
289 let chains_data = data.get(&DataField::Hashboards).and_then(|v| v.as_array());
290
291 if let Some(chains_array) = chains_data {
292 for (idx, chain) in chains_array.iter().enumerate() {
293 let hashrate = Self::extract_hashrate(chain, &["/hashrate_rt", "/hr_realtime"]);
294 let expected_hashrate =
295 Self::extract_hashrate(chain, &["/hashrate_ideal", "/hr_nominal"]);
296
297 let frequency = Self::extract_frequency(chain);
298 let voltage = Self::extract_voltage(chain);
299 let (board_temperature, chip_temperature) = Self::extract_temperatures(chain);
300
301 let working_chips = Self::extract_working_chips(chain);
302 let active = Self::extract_chain_active_status(chain, &hashrate);
303 let serial_number = Self::extract_chain_serial(chain, data);
304 let tuned = Self::extract_tuned_status(chain, data);
305 let chips = Self::extract_chips(chain);
306
307 hashboards.push(BoardData {
308 position: chain
309 .pointer("/id")
310 .and_then(|v| v.as_u64())
311 .unwrap_or(idx as u64) as u8,
312 hashrate,
313 expected_hashrate,
314 board_temperature,
315 intake_temperature: chip_temperature,
316 outlet_temperature: chip_temperature,
317 expected_chips: self.device_info.hardware.chips,
318 working_chips,
319 serial_number,
320 chips,
321 voltage,
322 frequency,
323 tuned,
324 active,
325 });
326 }
327 }
328
329 hashboards
330 }
331}
332
333impl GetHashrate for VnishV120 {
334 fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
335 data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
336 value: f,
337 unit: HashRateUnit::GigaHash,
338 algo: String::from("SHA256"),
339 })
340 }
341}
342
343impl GetExpectedHashrate for VnishV120 {
344 fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
345 data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| HashRate {
346 value: f,
347 unit: HashRateUnit::GigaHash,
348 algo: String::from("SHA256"),
349 })
350 }
351}
352
353impl GetFans for VnishV120 {
354 fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
355 let mut fans: Vec<FanData> = Vec::new();
356
357 if let Some(fans_data) = data.get(&DataField::Fans)
358 && let Some(fans_array) = fans_data.as_array()
359 {
360 for (idx, fan) in fans_array.iter().enumerate() {
361 if let Some(rpm) = fan.pointer("/rpm").and_then(|v| v.as_i64()) {
362 fans.push(FanData {
363 position: idx as i16,
364 rpm: Some(AngularVelocity::from_rpm(rpm as f64)),
365 });
366 }
367 }
368 }
369
370 fans
371 }
372}
373
374impl GetPsuFans for VnishV120 {}
375
376impl GetFluidTemperature for VnishV120 {}
377
378impl GetWattage for VnishV120 {
379 fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
380 data.extract_map::<i64, _>(DataField::Wattage, |w| Power::from_watts(w as f64))
381 }
382}
383
384impl GetWattageLimit for VnishV120 {}
385
386impl GetLightFlashing for VnishV120 {
387 fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
388 data.extract::<bool>(DataField::LightFlashing)
389 }
390}
391
392impl GetMessages for VnishV120 {}
393
394impl GetUptime for VnishV120 {
395 fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
396 data.extract::<String>(DataField::Uptime)
397 .and_then(|uptime_str| {
398 let trimmed = uptime_str.trim();
400
401 if trimmed.contains("days") {
403 let mut total_seconds = 0u64;
404
405 if let Some(days_part) = trimmed.split("days").next()
407 && let Ok(days) = days_part.trim().parse::<u64>()
408 {
409 total_seconds += days * 24 * 60 * 60;
410 }
411
412 if let Some(time_part) = trimmed.split(',').nth(1) {
414 let time_part = time_part.trim();
415 if let Some((hours_str, minutes_str)) = time_part.split_once(':')
416 && let (Ok(hours), Ok(minutes)) = (
417 hours_str.trim().parse::<u64>(),
418 minutes_str.trim().parse::<u64>(),
419 )
420 {
421 total_seconds += hours * 60 * 60 + minutes * 60;
422 }
423 }
424
425 return Some(Duration::from_secs(total_seconds));
426 }
427
428 None
429 })
430 }
431}
432
433impl GetIsMining for VnishV120 {
434 fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
435 data.extract::<String>(DataField::IsMining)
436 .map(|state| state == "mining")
437 .unwrap_or(false)
438 }
439}
440
441impl GetPools for VnishV120 {
442 fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
443 let mut pools: Vec<PoolData> = Vec::new();
444
445 if let Some(pools_data) = data.get(&DataField::Pools)
446 && let Some(pools_array) = pools_data.as_array()
447 {
448 for (idx, pool) in pools_array.iter().enumerate() {
449 let url = pool
450 .pointer("/url")
451 .and_then(|v| v.as_str())
452 .map(String::from)
453 .map(PoolURL::from);
454
455 let user = pool
456 .pointer("/user")
457 .and_then(|v| v.as_str())
458 .map(String::from);
459
460 let accepted_shares = pool.pointer("/accepted").and_then(|v| v.as_u64());
461 let rejected_shares = pool.pointer("/rejected").and_then(|v| v.as_u64());
462 let pool_status = pool.pointer("/status").and_then(|v| v.as_str());
463 let (active, alive) = Self::parse_pool_status(pool_status);
464
465 pools.push(PoolData {
466 position: Some(idx as u16),
467 url,
468 accepted_shares,
469 rejected_shares,
470 active,
471 alive,
472 user,
473 });
474 }
475 }
476
477 pools
478 }
479}
480
481impl VnishV120 {
483 fn extract_hashrate(chain: &Value, paths: &[&str]) -> Option<HashRate> {
484 paths
485 .iter()
486 .find_map(|&path| chain.pointer(path).and_then(|v| v.as_f64()))
487 .map(|f| HashRate {
488 value: f,
489 unit: HashRateUnit::GigaHash,
490 algo: String::from("SHA256"),
491 })
492 }
493
494 fn extract_frequency(chain: &Value) -> Option<Frequency> {
495 chain
496 .pointer("/frequency")
497 .or_else(|| chain.pointer("/freq"))
498 .and_then(|v| v.as_f64())
499 .map(Frequency::from_megahertz)
500 }
501
502 fn extract_voltage(chain: &Value) -> Option<Voltage> {
503 chain
504 .pointer("/voltage")
505 .and_then(|v| v.as_i64())
506 .map(|v| Voltage::from_millivolts(v as f64))
507 }
508
509 fn extract_temperatures(chain: &Value) -> (Option<Temperature>, Option<Temperature>) {
510 let board_temp = chain
511 .pointer("/pcb_temp/max")
512 .and_then(|v| v.as_i64())
513 .map(|t| Temperature::from_celsius(t as f64));
514
515 let chip_temp = chain
516 .pointer("/chip_temp/max")
517 .and_then(|v| v.as_i64())
518 .map(|t| Temperature::from_celsius(t as f64));
519
520 (board_temp, chip_temp)
521 }
522
523 fn extract_working_chips(chain: &Value) -> Option<u16> {
524 chain
525 .pointer("/chip_statuses")
526 .map(|statuses| {
527 let red = statuses
528 .pointer("/red")
529 .and_then(|v| v.as_u64())
530 .unwrap_or(0);
531 let orange = statuses
532 .pointer("/orange")
533 .and_then(|v| v.as_u64())
534 .unwrap_or(0);
535 (red + orange) as u16
536 })
537 .or_else(|| {
538 chain
539 .pointer("/chips")
540 .and_then(|v| v.as_array())
541 .map(|chips| chips.len() as u16)
542 })
543 }
544
545 fn extract_chain_active_status(chain: &Value, hashrate: &Option<HashRate>) -> Option<bool> {
546 chain
547 .pointer("/status/state")
548 .and_then(|v| v.as_str())
549 .map(|s| s == "mining")
550 .or_else(|| hashrate.as_ref().map(|h| h.value > 0.0))
551 }
552
553 fn parse_pool_status(status: Option<&str>) -> (Option<bool>, Option<bool>) {
554 match status {
555 Some("active" | "working") => (Some(true), Some(true)),
556 Some("offline" | "disabled") => (Some(false), Some(false)),
557 Some("rejecting") => (Some(false), Some(true)),
558 _ => (None, None),
559 }
560 }
561
562 fn extract_chain_serial(chain: &Value, data: &HashMap<DataField, Value>) -> Option<String> {
563 chain
565 .pointer("/serial")
566 .and_then(|v| v.as_str())
567 .map(String::from)
568 .or_else(|| {
569 data.extract::<String>(DataField::SerialNumber)
571 })
572 }
573
574 fn extract_tuned_status(_chain: &Value, data: &HashMap<DataField, Value>) -> Option<bool> {
575 if let Some(miner_state) = data.extract::<String>(DataField::IsMining) {
577 match miner_state.as_str() {
578 "auto-tuning" => Some(false), "mining" => Some(true), _ => None,
581 }
582 } else {
583 None
584 }
585 }
586
587 fn extract_chips(chain: &Value) -> Vec<ChipData> {
588 let mut chips: Vec<ChipData> = Vec::new();
589
590 if let Some(chips_array) = chain.pointer("/chips").and_then(|v| v.as_array()) {
591 for (idx, chip) in chips_array.iter().enumerate() {
592 let hashrate = chip
593 .pointer("/hr")
594 .and_then(|v| v.as_f64())
595 .map(|f| HashRate {
596 value: f,
597 unit: HashRateUnit::GigaHash,
598 algo: String::from("SHA256"),
599 });
600
601 let temperature = chip
602 .pointer("/temp")
603 .and_then(|v| v.as_f64())
604 .map(Temperature::from_celsius);
605
606 let voltage = chip
607 .pointer("/volt")
608 .and_then(|v| v.as_i64())
609 .map(|v| Voltage::from_millivolts(v as f64));
610
611 let frequency = chip
612 .pointer("/freq")
613 .and_then(|v| v.as_i64())
614 .map(|f| Frequency::from_megahertz(f as f64));
615
616 let working = hashrate.as_ref().map(|hr| hr.value > 0.0);
617
618 chips.push(ChipData {
619 position: chip
620 .pointer("/id")
621 .and_then(|v| v.as_u64())
622 .unwrap_or(idx as u64) as u16,
623 hashrate,
624 temperature,
625 voltage,
626 frequency,
627 tuned: None,
628 working,
629 });
630 }
631 }
632
633 chips
634 }
635}