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