Skip to main content

stratum_apps/monitoring/
miner_telemetry.rs

1//! Miner telemetry monitoring types and helpers.
2
3use serde::{Deserialize, Serialize};
4use utoipa::ToSchema;
5
6use asic_rs::{
7    core::data::{collector::DataField, hashrate::HashRateUnit, miner::MinerData},
8    MinerFactory,
9};
10use std::net::IpAddr;
11use tracing::debug;
12
13/// Telemetry reported by the miner's management interface.
14#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
15pub struct MinerTelemetry {
16    /// Miner manufacturer or brand reported by the management interface.
17    pub make: Option<String>,
18    /// Miner model reported by the management interface.
19    pub model: Option<String>,
20    /// Firmware version reported by the miner, when exposed.
21    pub firmware_version: Option<String>,
22    /// Miner-reported hashrate in hashes per second.
23    pub reported_hashrate_hs: Option<f64>,
24    /// Current miner power consumption in watts.
25    pub power_consumption_w: Option<f64>,
26    /// Miner efficiency in joules per terahash.
27    pub efficiency_j_per_th: Option<f64>,
28    /// Average miner temperature in degrees Celsius.
29    pub average_temperature_c: Option<f64>,
30    /// Total miner system uptime in seconds.
31    pub uptime_secs: Option<u64>,
32    /// Whether the miner reports that hashing is currently running.
33    pub is_mining: Option<bool>,
34}
35
36impl From<MinerData> for MinerTelemetry {
37    fn from(data: MinerData) -> Self {
38        Self {
39            make: Some(data.device_info.make),
40            model: Some(data.device_info.model),
41            firmware_version: data.firmware_version,
42            reported_hashrate_hs: data
43                .hashrate
44                .map(|hashrate| hashrate.as_unit(HashRateUnit::Hash).value),
45            power_consumption_w: data.wattage.map(|power| power.as_watts()),
46            efficiency_j_per_th: data.efficiency,
47            average_temperature_c: data
48                .average_temperature
49                .map(|temperature| temperature.as_celsius()),
50            uptime_secs: data.uptime.map(|uptime| uptime.as_secs()),
51            is_mining: Some(data.is_mining),
52        }
53    }
54}
55
56const MINER_TELEMETRY_EXCLUDED_FIELDS: &[DataField] = &[
57    DataField::Mac,
58    DataField::SerialNumber,
59    DataField::Hostname,
60    DataField::ApiVersion,
61    DataField::ControlBoardVersion,
62    DataField::Chips,
63    DataField::ExpectedHashrate,
64    DataField::Fans,
65    DataField::PsuFans,
66    DataField::FluidTemperature,
67    DataField::TuningTarget,
68    DataField::LightFlashing,
69    DataField::Messages,
70    DataField::Pools,
71];
72
73pub struct MinerTelemetryCollector {
74    factory: MinerFactory,
75}
76
77impl MinerTelemetryCollector {
78    pub fn new() -> Self {
79        Self {
80            factory: MinerFactory::new(),
81        }
82    }
83
84    pub async fn fetch(&self, ip: IpAddr) -> Option<MinerTelemetry> {
85        let miner = match self.factory.get_miner(ip).await {
86            Ok(Some(miner)) => miner,
87            Ok(None) => {
88                debug!("No miner management interface found at {ip}");
89                return None;
90            }
91            Err(error) => {
92                debug!("Failed to get miner management interface at {ip}: {error}");
93                return None;
94            }
95        };
96
97        Some(MinerTelemetry::from(
98            miner
99                .get_data_filtered(MINER_TELEMETRY_EXCLUDED_FIELDS.to_vec())
100                .await,
101        ))
102    }
103}
104
105impl Default for MinerTelemetryCollector {
106    fn default() -> Self {
107        Self::new()
108    }
109}