asic_rs/miners/
data.rs

1use crate::miners::{
2    backends::traits::{APIClient, MinerInterface},
3    commands::MinerCommand,
4};
5use serde_json::{Value, json};
6use std::collections::{HashMap, HashSet};
7use strum::{EnumIter, IntoEnumIterator};
8
9/// Represents the individual pieces of data that can be queried from a miner device.
10#[derive(Debug, Clone, Hash, Eq, PartialEq, Copy, EnumIter)]
11pub enum DataField {
12    /// Schema version of the miner data.
13    SchemaVersion,
14    /// Timestamp of when the data was collected.
15    Timestamp,
16    /// IP address of the miner.
17    Ip,
18    /// MAC address of the miner.
19    Mac,
20    /// Information about the miner's device.
21    DeviceInfo,
22    /// Serial number of the miner.
23    SerialNumber,
24    /// Hostname assigned to the miner.
25    Hostname,
26    /// Version of the miner's API.
27    ApiVersion,
28    /// Firmware version of the miner.
29    FirmwareVersion,
30    /// Control board version of the miner.
31    ControlBoardVersion,
32    /// Details about the hashboards (e.g., temperatures, chips, etc.).
33    Hashboards,
34    /// Current hashrate reported by the miner.
35    Hashrate,
36    /// Expected hashrate for the miner.
37    ExpectedHashrate,
38    /// Fan speed or fan configuration.
39    Fans,
40    /// PSU fan speed or configuration.
41    PsuFans,
42    /// Average temperature reported by the miner.
43    AverageTemperature,
44    /// Fluid temperature reported by the miner.
45    FluidTemperature,
46    /// Current power consumption in watts.
47    Wattage,
48    /// Configured power limit in watts.
49    WattageLimit,
50    /// Efficiency of the miner (e.g., J/TH).
51    Efficiency,
52    /// Whether the fault or alert light is flashing.
53    LightFlashing,
54    /// Messages reported by the miner (e.g., errors or warnings).
55    Messages,
56    /// Uptime in seconds.
57    Uptime,
58    /// Whether the miner is currently hashing.
59    IsMining,
60    /// Pool configuration (addresses, statuses, etc.).
61    Pools,
62}
63
64/// A function pointer type that takes a JSON `Value` and an optional key,
65/// returning the extracted value if found.
66type ExtractorFn = for<'a> fn(&'a Value, Option<&'static str>) -> Option<&'a Value>;
67
68/// Describes how to extract a specific value from a command's response.
69///
70/// Created by a backend and used to locate a field within a JSON structure.
71#[derive(Clone, Copy)]
72pub struct DataExtractor {
73    /// Function used to extract data from a JSON response.
74    pub func: ExtractorFn,
75    /// Optional key or pointer within the response to extract.
76    pub key: Option<&'static str>,
77    /// Optional tag to move the extracted value to
78    pub tag: Option<&'static str>,
79}
80
81/// Alias for a tuple describing the API command and the extractor used to parse its result.
82pub type DataLocation = (MinerCommand, DataExtractor);
83
84/// Extracts a value from a JSON object using a key (flat lookup).
85///
86/// Returns `None` if the key is `None` or not found in the object.
87pub fn get_by_key<'a>(data: &'a Value, key: Option<&str>) -> Option<&'a Value> {
88    data.get(key?.to_string())
89}
90
91/// Extracts a value from a JSON object using a JSON pointer path.
92///
93/// Returns `None` if the pointer is `None` or the path doesn't exist.
94pub fn get_by_pointer<'a>(data: &'a Value, pointer: Option<&str>) -> Option<&'a Value> {
95    data.pointer(pointer?)
96}
97
98/// A trait for types that can be extracted from a JSON Value.
99pub trait FromValue: Sized {
100    /// Attempts to convert a JSON Value to Self.
101    fn from_value(value: &Value) -> Option<Self>;
102}
103
104// Implement FromValue for common types
105impl FromValue for String {
106    fn from_value(value: &Value) -> Option<Self> {
107        value.as_str().map(String::from)
108    }
109}
110
111impl FromValue for f64 {
112    fn from_value(value: &Value) -> Option<Self> {
113        value.as_f64()
114    }
115}
116
117impl FromValue for u64 {
118    fn from_value(value: &Value) -> Option<Self> {
119        value.as_u64()
120    }
121}
122
123impl FromValue for i64 {
124    fn from_value(value: &Value) -> Option<Self> {
125        value.as_i64()
126    }
127}
128
129impl FromValue for bool {
130    fn from_value(value: &Value) -> Option<Self> {
131        // Try to get as bool first
132        value.as_bool().or_else(|| {
133            // If not a bool, try to interpret as a number (0 = false, non-zero = true)
134            value
135                .as_u64()
136                .map(|n| n != 0)
137                .or_else(|| value.as_i64().map(|n| n != 0))
138        })
139    }
140}
141
142impl<T: FromValue> FromValue for Vec<T> {
143    fn from_value(value: &Value) -> Option<Self> {
144        value.as_array()?.iter().map(|v| T::from_value(v)).collect()
145    }
146}
147
148/// Extension trait for HashMap<DataField, &Value> to provide cleaner value extraction.
149pub trait DataExtensions {
150    /// Extract a value of type T from the data map for the given field.
151    fn extract<T: FromValue>(&self, field: DataField) -> Option<T>;
152
153    /// Extract a value of type T from the data map for the given field, with a default value.
154    fn extract_or<T: FromValue>(&self, field: DataField, default: T) -> T;
155
156    /// Extract a nested value of type T from the data map for the given field and nested key.
157    fn extract_nested<T: FromValue>(&self, field: DataField, nested_key: &str) -> Option<T>;
158
159    /// Extract a nested value of type T from the data map for the given field and nested key, with a default value.
160    fn extract_nested_or<T: FromValue>(&self, field: DataField, nested_key: &str, default: T) -> T;
161
162    /// Extract a value and map it to another type using the provided function.
163    fn extract_map<T: FromValue, U>(&self, field: DataField, f: impl FnOnce(T) -> U) -> Option<U>;
164
165    /// Extract a value, map it to another type, or use a default value.
166    fn extract_map_or<T: FromValue, U>(
167        &self,
168        field: DataField,
169        default: U,
170        f: impl FnOnce(T) -> U,
171    ) -> U;
172
173    /// Extract a nested value and map it to another type using the provided function.
174    fn extract_nested_map<T: FromValue, U>(
175        &self,
176        field: DataField,
177        nested_key: &str,
178        f: impl FnOnce(T) -> U,
179    ) -> Option<U>;
180
181    /// Extract a nested value, map it to another type, or use a default value.
182    fn extract_nested_map_or<T: FromValue, U>(
183        &self,
184        field: DataField,
185        nested_key: &str,
186        default: U,
187        f: impl FnOnce(T) -> U,
188    ) -> U;
189}
190
191impl DataExtensions for HashMap<DataField, Value> {
192    fn extract<T: FromValue>(&self, field: DataField) -> Option<T> {
193        self.get(&field).and_then(|v| T::from_value(v))
194    }
195
196    fn extract_or<T: FromValue>(&self, field: DataField, default: T) -> T {
197        self.extract(field).unwrap_or(default)
198    }
199
200    fn extract_nested<T: FromValue>(&self, field: DataField, nested_key: &str) -> Option<T> {
201        self.get(&field)
202            .and_then(|v| v.get(nested_key))
203            .and_then(|v| T::from_value(v))
204    }
205
206    fn extract_nested_or<T: FromValue>(&self, field: DataField, nested_key: &str, default: T) -> T {
207        self.extract_nested(field, nested_key).unwrap_or(default)
208    }
209
210    fn extract_map<T: FromValue, U>(&self, field: DataField, f: impl FnOnce(T) -> U) -> Option<U> {
211        self.extract(field).map(f)
212    }
213
214    fn extract_map_or<T: FromValue, U>(
215        &self,
216        field: DataField,
217        default: U,
218        f: impl FnOnce(T) -> U,
219    ) -> U {
220        self.extract(field).map(f).unwrap_or(default)
221    }
222
223    fn extract_nested_map<T: FromValue, U>(
224        &self,
225        field: DataField,
226        nested_key: &str,
227        f: impl FnOnce(T) -> U,
228    ) -> Option<U> {
229        self.extract_nested(field, nested_key).map(f)
230    }
231
232    fn extract_nested_map_or<T: FromValue, U>(
233        &self,
234        field: DataField,
235        nested_key: &str,
236        default: U,
237        f: impl FnOnce(T) -> U,
238    ) -> U {
239        self.extract_nested(field, nested_key)
240            .map(f)
241            .unwrap_or(default)
242    }
243}
244
245/// A utility for collecting structured miner data from an API backend.
246pub struct DataCollector<'a> {
247    /// Backend-specific data mapping logic.
248    miner: &'a dyn MinerInterface,
249    client: &'a dyn APIClient,
250    /// Cache of command responses keyed by command string.
251    cache: HashMap<MinerCommand, Value>,
252}
253
254impl<'a> DataCollector<'a> {
255    /// Constructs a new `DataCollector` with the given backend and API client.
256    pub fn new(miner: &'a dyn MinerInterface) -> Self {
257        Self {
258            miner,
259            client: miner,
260            cache: HashMap::new(),
261        }
262    }
263
264    #[allow(dead_code)]
265    pub(crate) fn new_with_client(
266        miner: &'a dyn MinerInterface,
267        client: &'a dyn APIClient,
268    ) -> Self {
269        Self {
270            miner,
271            client,
272            cache: HashMap::new(),
273        }
274    }
275
276    /// Collects **all** available fields from the miner and returns a map of results.
277    pub async fn collect_all(&mut self) -> HashMap<DataField, Value> {
278        self.collect(DataField::iter().collect::<Vec<_>>().as_slice())
279            .await
280    }
281
282    /// Collects only the specified fields from the miner and returns a map of results.
283    ///
284    /// This method sends only the minimum required set of API commands.
285    pub async fn collect(&mut self, fields: &[DataField]) -> HashMap<DataField, Value> {
286        let mut results = HashMap::new();
287        let required_commands = self.get_required_commands(fields);
288
289        for command in required_commands {
290            if let Ok(response) = self.client.get_api_result(&command).await {
291                self.cache.insert(command, response);
292            }
293        }
294
295        // Extract the data for each field using the cached responses.
296        for &field in fields {
297            if let Some(value) = self.extract_field(field) {
298                results.insert(field, value);
299            }
300        }
301
302        results
303    }
304
305    fn merge(&self, a: &mut Value, b: Value) {
306        Self::merge_values(a, b);
307    }
308
309    fn merge_values(a: &mut Value, b: Value) {
310        match (a, b) {
311            (Value::Object(a_map), Value::Object(b_map)) => {
312                for (k, v) in b_map {
313                    Self::merge_values(a_map.entry(k).or_insert(Value::Null), v);
314                }
315            }
316            (Value::Array(a_array), Value::Array(b_array)) => {
317                // Combine arrays by extending
318                a_array.extend(b_array);
319            }
320            (a_slot, b_val) => {
321                // For everything else (including mismatched types), overwrite
322                *a_slot = b_val;
323            }
324        }
325    }
326
327    /// Determines the unique set of API commands needed for the requested fields.
328    ///
329    /// Uses the backend's location mappings to identify required commands.
330    fn get_required_commands(&self, fields: &[DataField]) -> HashSet<MinerCommand> {
331        fields
332            .iter()
333            .flat_map(|&field| self.miner.get_locations(field))
334            .map(|(cmd, _)| cmd.clone())
335            .collect()
336    }
337
338    /// Attempts to extract the value for a specific field from the cached command responses.
339    ///
340    /// Uses the extractor function and key associated with the field for parsing.
341    fn extract_field(&self, field: DataField) -> Option<Value> {
342        let mut success: Vec<Value> = Vec::new();
343        for (command, extractor) in self.miner.get_locations(field) {
344            if let Some(response_data) = self.cache.get(&command)
345                && let Some(value) = (extractor.func)(response_data, extractor.key)
346            {
347                match extractor.tag {
348                    Some(tag) => {
349                        let tag = tag.to_string();
350                        success.push(json!({ tag: value.clone() }).clone());
351                    }
352                    None => {
353                        success.push(value.clone());
354                    }
355                }
356            }
357        }
358        if success.is_empty() {
359            None
360        } else {
361            let mut response = json!({});
362            for value in success {
363                self.merge(&mut response, value)
364            }
365            Some(response)
366        }
367    }
368}