blob: a2e0b060b0218c08341b8e0aa489184780b773a4 [file] [log] [blame]
//! This module provides functionality for extracting sensor configurations from OpenBMC Entity
//! Manager's configuration files
use lazy_static::lazy_static;
use log::debug;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
/// Represents the type of reading a sensor provides.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub enum ReadingType {
Voltage,
Current,
Power,
Temperature,
Fan,
#[default]
Unknown,
}
impl std::fmt::Display for ReadingType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReadingType::Voltage => write!(f, "Voltage"),
ReadingType::Current => write!(f, "Current"),
ReadingType::Power => write!(f, "Power"),
ReadingType::Temperature => write!(f, "Temperature"),
ReadingType::Fan => write!(f, "Fan"),
ReadingType::Unknown => write!(f, "Unknown"),
}
}
}
/// Represents properties of a Power Supply Unit (PSU) sensor.
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct PSUProperty {
pub name: String,
pub max: i32,
pub min: i32,
pub scale: i32,
pub offset: i32,
}
/// Represents a sensor configuration.
#[allow(non_snake_case)]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Sensor {
#[serde(default)]
pub Name: String,
#[serde(rename = "Type")]
pub sensor_type: String,
#[serde(default)]
pub Bus: Option<String>,
#[serde(default)]
pub Address: Option<String>,
#[serde(default)]
/// Use Labels[0] to select from this for the threshold for this sensor
pub Thresholds: Option<Vec<Threshold>>,
#[serde(default)]
pub Index: Option<u32>,
#[serde(default)]
pub Labels: Option<Vec<String>>,
#[serde(default)]
pub extra: HashMap<String, Value>,
#[serde(default)]
pub reading_type: (PSUProperty, ReadingType),
}
/// Represents a threshold configuration for a sensor.
#[allow(non_snake_case)]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Threshold {
pub Direction: String,
pub Name: String,
pub Severity: u32,
pub Value: f64,
#[serde(default)]
pub Index: Option<u32>,
#[serde(default)]
pub Label: Option<String>,
}
#[rustfmt::skip]
lazy_static! {
static ref LABEL_MATCH: HashMap<&'static str, (PSUProperty, ReadingType)> = {
let mut m = HashMap::new();
m.insert("pin", (PSUProperty { name: "Input Power".to_string(), max: 3000, min: 0, scale: 6, offset: 0 }, ReadingType::Power));
m.insert("pin1", (PSUProperty { name: "Input Power".to_string(), max: 3000, min: 0, scale: 6, offset: 0 }, ReadingType::Power));
m.insert("pin2", (PSUProperty { name: "Input Power".to_string(), max: 3000, min: 0, scale: 6, offset: 0 }, ReadingType::Power));
m.insert("pout1", (PSUProperty { name: "Output Power".to_string(), max: 3000, min: 0, scale: 6, offset: 0 }, ReadingType::Power));
m.insert("pout2", (PSUProperty { name: "Output Power".to_string(), max: 3000, min: 0, scale: 6, offset: 0 }, ReadingType::Power));
m.insert("pout3", (PSUProperty { name: "Output Power".to_string(), max: 3000, min: 0, scale: 6, offset: 0 }, ReadingType::Power));
m.insert("power1", (PSUProperty { name: "Output Power".to_string(), max: 3000, min: 0, scale: 6, offset: 0 }, ReadingType::Power));
m.insert("power2", (PSUProperty { name: "Output Power".to_string(), max: 3000, min: 0, scale: 6, offset: 0 }, ReadingType::Power));
m.insert("power3", (PSUProperty { name: "Output Power".to_string(), max: 3000, min: 0, scale: 6, offset: 0 }, ReadingType::Power));
m.insert("power4", (PSUProperty { name: "Output Power".to_string(), max: 3000, min: 0, scale: 6, offset: 0 }, ReadingType::Power));
m.insert("maxpin", (PSUProperty { name: "Max Input Power".to_string(), max: 3000, min: 0, scale: 6, offset: 0 }, ReadingType::Power));
m.insert("vin", (PSUProperty { name: "Input Voltage".to_string(), max: 300, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vin1", (PSUProperty { name: "Input Voltage".to_string(), max: 300, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vin2", (PSUProperty { name: "Input Voltage".to_string(), max: 300, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("maxvin", (PSUProperty { name: "Max Input Voltage".to_string(), max: 300, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("in_voltage0", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("in_voltage1", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("in_voltage2", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("in_voltage3", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout1", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout2", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout3", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout4", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout5", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout6", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout7", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout8", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout9", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout10", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout11", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout12", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout13", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout14", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout15", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout16", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout17", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout18", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout19", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout20", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout21", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout22", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout23", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout24", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout25", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout26", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout27", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout28", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout29", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout30", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout31", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vout32", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("vmon", (PSUProperty { name: "Auxiliary Input Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("in0", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("in1", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("in2", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("in3", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("in4", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("in5", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("in6", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("in7", (PSUProperty { name: "Output Voltage".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Voltage));
m.insert("iin", (PSUProperty { name: "Input Current".to_string(), max: 20, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iin1", (PSUProperty { name: "Input Current".to_string(), max: 20, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iin2", (PSUProperty { name: "Input Current".to_string(), max: 20, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout1", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout2", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout3", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout4", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout5", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout6", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout7", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout8", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout9", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout10", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout11", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout12", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout13", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("iout14", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("curr1", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("curr2", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("curr3", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("curr4", (PSUProperty { name: "Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("maxiout1", (PSUProperty { name: "Max Output Current".to_string(), max: 255, min: 0, scale: 3, offset: 0 }, ReadingType::Current));
m.insert("temp1", (PSUProperty { name: "Temperature".to_string(), max: 127, min: -128, scale: 3, offset: 0 }, ReadingType::Temperature));
m.insert("temp2", (PSUProperty { name: "Temperature".to_string(), max: 127, min: -128, scale: 3, offset: 0 }, ReadingType::Temperature));
m.insert("temp3", (PSUProperty { name: "Temperature".to_string(), max: 127, min: -128, scale: 3, offset: 0 }, ReadingType::Temperature));
m.insert("temp4", (PSUProperty { name: "Temperature".to_string(), max: 127, min: -128, scale: 3, offset: 0 }, ReadingType::Temperature));
m.insert("temp5", (PSUProperty { name: "Temperature".to_string(), max: 127, min: -128, scale: 3, offset: 0 }, ReadingType::Temperature));
m.insert("temp6", (PSUProperty { name: "Temperature".to_string(), max: 127, min: -128, scale: 3, offset: 0 }, ReadingType::Temperature));
m.insert("maxtemp1", (PSUProperty { name: "Max Temperature".to_string(), max: 127, min: -128, scale: 3, offset: 0 }, ReadingType::Temperature));
m.insert("fan1", (PSUProperty { name: "Fan Speed 1".to_string(), max: 30000, min: 0, scale: 0, offset: 0 }, ReadingType::Fan));
m.insert("fan2", (PSUProperty { name: "Fan Speed 2".to_string(), max: 30000, min: 0, scale: 0, offset: 0 }, ReadingType::Fan));
m.insert("fan3", (PSUProperty { name: "Fan Speed 3".to_string(), max: 30000, min: 0, scale: 0, offset: 0 }, ReadingType::Fan));
m.insert("fan4", (PSUProperty { name: "Fan Speed 4".to_string(), max: 30000, min: 0, scale: 0, offset: 0 }, ReadingType::Fan));
m
};
}
/// Infers the reading type of a sensor based on its configuration.
///
/// # Arguments
///
/// * `sensor` - The sensor configuration to infer the reading type from.
///
/// # Returns
///
/// A tuple containing the PSUProperty and ReadingType for the sensor.
fn infer_reading_type(sensor: &Sensor) -> (PSUProperty, ReadingType) {
// First, try to match using the Labels field
if let Some(labels) = &sensor.Labels {
if let Some(first_label) = labels.first() {
if let Some((psu_property, reading_type)) = LABEL_MATCH.get(first_label.as_str()) {
return (psu_property.clone(), *reading_type);
}
// If no exact match, try to match prefixes
for (label, (psu_property, reading_type)) in LABEL_MATCH.iter() {
if first_label.starts_with(label) {
return (psu_property.clone(), *reading_type);
}
}
}
}
// If Labels matching fails, fall back to matching based on the sensor Name
let name_lower = sensor.Name.to_lowercase();
// Fallback logic
if name_lower.contains("temp") || name_lower.ends_with('t') {
if let Some((psu_property, reading_type)) = LABEL_MATCH.get("temp1") {
return (psu_property.clone(), *reading_type);
}
} else if name_lower.contains("curr")
|| name_lower.contains("iout")
|| name_lower.ends_with('i')
{
if let Some((psu_property, reading_type)) = LABEL_MATCH.get("curr1") {
return (psu_property.clone(), *reading_type);
}
} else if name_lower.contains("power")
|| name_lower.contains("pout")
|| name_lower.contains("pin")
{
if let Some((psu_property, reading_type)) = LABEL_MATCH.get("power1") {
return (psu_property.clone(), *reading_type);
}
} else if name_lower.contains("volt")
|| name_lower.contains("vin")
|| name_lower.contains("vout")
|| name_lower.ends_with('v')
{
if let Some((psu_property, reading_type)) = LABEL_MATCH.get("vin") {
return (psu_property.clone(), *reading_type);
}
} else if name_lower.contains("fan") || name_lower.contains("tach") {
if let Some((psu_property, reading_type)) = LABEL_MATCH.get("fan1") {
return (psu_property.clone(), *reading_type);
}
}
(PSUProperty::default(), ReadingType::Unknown)
}
/// Extracts sensor configurations from the 'Exposes' field of a configuration object.
///
/// # Arguments
///
/// * `exposes` - A slice of JSON Value objects containing sensor configurations.
///
/// # Returns
///
/// A vector of Sensor structs extracted from the configuration.
#[rustfmt::skip]
fn extract_sensors_from_exposes(exposes: &[Value]) -> Vec<Sensor> {
let mut sensors = Vec::new();
for expose in exposes {
debug!("Captured expose: {:?}", expose);
// Handle cases where there are multiple sensors defined within a single expose object, each corresponding to a label, like "P0_ADM1266".
if let Some(labels) = expose.get("Labels").and_then(|v| v.as_array()) {
for label in labels {
debug!("Captured expose by label: {:?}", label);
if let Some(label_name) = label.as_str() {
let name_key = format!("{}_Name", label_name);
let sensor_name = expose
.get(&name_key)
.and_then(|v| v.as_str())
.unwrap_or(label_name)
.to_string();
let mut sensor = Sensor {
Name: sensor_name.clone(),
sensor_type: expose
.get("Type")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
Bus: expose.get("Bus").and_then(|v| match v {
Value::String(s) => Some(s.clone()),
Value::Number(n) => n.as_u64().map(|n| n.to_string()),
_ => None,
}),
Address: expose
.get("Address")
.and_then(|v| v.as_str().map(String::from)),
Thresholds: expose
.get("Thresholds")
.and_then(|v| serde_json::from_value(v.clone()).ok()),
Index: None,
Labels: Some(vec![label_name.to_string()]),
extra: HashMap::new(),
reading_type: Default::default(),
};
sensor.reading_type = infer_reading_type(&sensor);
debug!("Add captured expose by labels: {:?}", sensor);
sensors.push(sensor);
}
}
}
// Try to deserialize the entire expose object into a Sensor struct, like "fan2_tach".
else if let Ok(mut sensor) = serde_json::from_value::<Sensor>(expose.clone()) {
debug!("Captured expose by Sensor type: {:?}", expose);
// Filter for valid sensor types, some entry in config file are not sensors, like
// MAX31790, use this to filter them out
let valid_hwmon_types = [
"ADC", "MAX31725", "MAX31730", "TMP75", "TMP421", "TMP441", "TMP112", "EMC1412",
"EMC1413", "ADM1032", "ADT7470", "G781", "MAX6581", "MAX6654", "LM75", "LM95245",
"ADT7460", "ADT7490", "W83795G", "SI7020", "NCT7802", "pmbus", "ADM1266", "ADM1272",
"ADM1275", "ADM1278", "ADM1293", "ADM1294", "IR35221", "IR38062", "IR38164", "IR38263",
"ISL68137", "ISL68220", "ISL69225", "ISL69227", "ISL69228", "ISL69234", "ISL69236",
"ISL69239", "ISL69243", "ISL69247", "ISL69254", "ISL69259", "ISL69260", "LM25066",
"RAA228000", "RAA228004", "RAA228006", "RAA228228", "RAA229001", "RAA229004",
"TPS53679", "TPS53688", "TPS53681", "TPS53622", "TPS53819", "TPS53915", "TPS53916",
"TPS53647", "TPS53667", "TPS53676", "TPS53661", "TPS53641", "TPS53631", "TPS53625",
"TPS53M30", "UCD90120", "UCD90124", "UCD90160", "UCD90320", "UCD9090", "UCD90910",
"XDPE11280", "XDPE12284", "XDPE132G5C", "MAX20730", "MAX20734", "MAX20743",
"MAX34451", "MAX34460", "MAX34461", "MAX77836", "HwmonTempSensor", "VirtualSensor",
"ADCSensor", "FanSensor", "NVMe", "TMP431", "I2CFan"
];
let valid_pldm_types = ["PLDM"];
let valid_other_types = [
"ExternalSensor", "Fan", "System", "DownstreamPort",
"MyMachine CPLD Upstream Port", "MyMachine 12 Upstream Port",
"MyMachine 13 Upstream Port", "MyMachine 8 Upstream Port",
"MyMachine 11 Upstream Port"
];
if valid_hwmon_types.contains(&sensor.sensor_type.as_str()) {
// Handle I2CFan type specifically
if sensor.sensor_type == "I2CFan" {
if let Some(connector) = expose.get("Connector") {
if let Some(pwm_name) = connector.get("PwmName").and_then(|v| v.as_str()) {
let mut pwm_sensor = sensor.clone();
pwm_sensor.Name = pwm_name.to_string();
pwm_sensor.sensor_type = "PWMSensor".to_string();
pwm_sensor.reading_type = infer_reading_type(&pwm_sensor);
debug!("Add PWM sensor: {:?}", pwm_sensor);
sensors.push(pwm_sensor);
}
}
}
sensor.reading_type = infer_reading_type(&sensor);
debug!("Add captured expose {:?} by Sensor type, sensor: {:?}", expose, sensor);
} else if valid_pldm_types.contains(&sensor.sensor_type.as_str()) {
sensor.sensor_type = "PLDM".to_string();
sensor.reading_type = infer_reading_type(&sensor);
} else if valid_other_types.contains(&sensor.sensor_type.as_str()) {
// FIXME: hardcode sensor NAme for PWMSensor from config
// Add captured expose by Sensor type: Object {"HotPluggable": Bool(false), "LocationType": String("Connector"), "Name": String("Fan3"), "PWMSensor": String("fan3_pwm"), "ServiceLabel": String("FAN3"), "TachSensor": String("fan3_tach"), "Type": String("Fan")}
// extract_sensors, config /usr/share/entity-manager/configurations/heizo.json, all sensors from config: Sensor { Name: "Fan0", sensor_type: "Fan", Bus: None, Address: None, Thresholds: None, Index: None, Labels: None, extra: {}, reading_type: (PSUProperty { name: "Fan Speed 1", max: 30000, min: 0, scale: 0, offset: 0 }, Fan) }
if sensor.sensor_type == "Fan" {
let pwm_sensor = expose.get("PWMSensor").and_then(|p| p.as_str()).unwrap_or("").to_owned();
sensor.Name = pwm_sensor;
sensor.sensor_type = "PWMSensor".to_string();
}
} else {
debug!("Skip captured expose: {:?}", expose);
continue; // Skip sensors with unrecognized types
}
sensor.reading_type = infer_reading_type(&sensor);
debug!("Add captured expose {:?} by Sensor type, sensor: {:?}", expose, sensor);
sensors.push(sensor);
}
// A catch-all for sensors that don't fit into the first two categories.
else if let Some(sensor_type) = expose.get("Type").and_then(|t| t.as_str()) {
debug!("Captured expose by Type: {:?}", expose);
let labels = expose.get("Labels").and_then(|v| v.as_array()).map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect::<Vec<String>>()
});
let mut label_map = HashMap::new();
if let Some(labels) = &labels {
for label in labels {
let name_key = format!("{}_Name", label);
if let Some(sensor_name) = expose.get(&name_key).and_then(|v| v.as_str()) {
label_map.insert(label.clone(), sensor_name.to_string());
}
}
}
let bus = expose.get("Bus").and_then(|b| match b {
Value::Number(n) => Some(n.to_string()),
Value::String(s) => Some(s.clone()),
_ => None,
});
let address = expose
.get("Address")
.and_then(|a| a.as_str().map(String::from));
// Create individual sensors for each label
for (label, sensor_name) in label_map {
let mut thresholds = Vec::new();
if let Some(thresh_array) = expose.get("Thresholds").and_then(|t| t.as_array()) {
for thresh in thresh_array {
if let Ok(t) = serde_json::from_value::<Threshold>(thresh.clone()) {
if t.Label.as_ref().map(|l| l == &label).unwrap_or(false) {
thresholds.push(t);
}
}
}
}
let mut sensor = Sensor {
reading_type: Default::default(),
Name: sensor_name.clone(),
sensor_type: sensor_type.to_string(),
Bus: bus.clone(),
Address: address.clone(),
Thresholds: if thresholds.is_empty() {
None
} else {
Some(thresholds)
},
Index: None,
Labels: Some(vec![label]),
extra: HashMap::new(),
};
sensor.reading_type = infer_reading_type(&sensor);
debug!("Add captured expose by Type and label: {:?}", sensor);
sensors.push(sensor);
}
}
}
sensors
}
/// Recursively extracts sensor configurations from a JSON Value.
///
/// # Arguments
///
/// * `value` - A JSON Value that may contain sensor configurations.
///
/// # Returns
///
/// A vector of Sensor structs extracted from the Value.
fn extract_sensors_from_value(value: &Value) -> Vec<Sensor> {
match value {
Value::Array(arr) => arr.iter().flat_map(extract_sensors_from_value).collect(),
Value::Object(_) => extract_sensors_from_exposes(&[value.clone()]),
_ => Vec::new(),
}
}
/// Extracts sensor configurations from a JSON configuration file.
///
/// # Arguments
///
/// * `config_file` - The path to the JSON configuration file.
///
/// # Returns
///
/// A Result containing a vector of Sensor structs or an error.
pub async fn extract_sensors(config_file: &str) -> Result<Vec<Sensor>, Box<dyn std::error::Error>> {
let file_content = tokio::fs::read_to_string(config_file).await?;
let config: Value = serde_json::from_str(&file_content)?;
let sensors = match config {
Value::Array(configs) => {
let mut all_sensors = Vec::new();
for c in configs {
if let Some(exposes) = c.get("Exposes") {
all_sensors.extend(extract_sensors_from_value(exposes));
}
}
all_sensors
}
Value::Object(config) => {
if let Some(exposes) = config.get("Exposes") {
extract_sensors_from_value(exposes)
} else {
Vec::new()
}
}
_ => Vec::new(),
};
Ok(sensors)
}
/// Finds the sysfs file path for a given sensor.
///
/// # Arguments
///
/// * `sensor` - The Sensor configuration to find the file for.
///
/// # Returns
///
/// A Result containing the sysfs file path as a String or an error.
pub fn find_sensor_file(sensor: &Sensor) -> io::Result<String> {
debug!("find_sensor_file for sensor {:?}", sensor);
if let (Some(bus), Some(address)) = (&sensor.Bus, &sensor.Address) {
// Strip "0x" prefix if present
let address_stripped = address.trim_start_matches("0x");
let base_path = Path::new("/sys/class/hwmon");
let result = search_hwmon(base_path, bus, address_stripped, sensor);
if result.is_ok() {
debug!(
"find_sensor_file in hwmon for sensor {:?}, found result {:?}",
sensor, result
);
return result;
}
// For /sys/bus/i2c/devices
let base_path = Path::new("/sys/bus/i2c/devices");
let result = search_i2c(base_path, bus, address_stripped, sensor);
if result.is_ok() {
debug!(
"find_sensor_file in i2c for sensor {:?}, found result {:?}",
sensor, result
);
return result;
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
"Sensor file not found",
))
}
/// Searches for a sensor file in the hwmon directory.
///
/// # Arguments
///
/// * `base_path` - The base path to start the search from.
/// * `bus` - The bus identifier.
/// * `address_stripped` - The stripped address of the sensor.
/// * `sensor` - The Sensor configuration to search for.
///
/// # Returns
///
/// A Result containing the sensor file path as a String or an error.
fn search_hwmon(
base_path: &Path,
bus: &str,
address_stripped: &str,
sensor: &Sensor,
) -> io::Result<String> {
let entries = fs::read_dir(base_path)?;
for entry in entries {
let entry = entry?;
let symlink_path = entry.path();
let real_path = fs::read_link(&symlink_path)?;
let full_real_path = base_path.join(&real_path);
if let Some(path_str) = full_real_path.to_str() {
if path_str.contains(bus) && path_str.contains(address_stripped) {
let sensor_file = find_specific_sensor_file(&full_real_path, sensor)?;
return Ok(sensor_file.to_string_lossy().into_owned());
}
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
"Sensor file not found in hwmon",
))
}
/// Searches for a sensor file in the i2c devices directory.
///
/// # Arguments
///
/// * `base_path` - The base path to start the search from.
/// * `bus` - The bus identifier.
/// * `address` - The address of the sensor.
/// * `sensor` - The Sensor configuration to search for.
///
/// # Returns
///
/// A Result containing the sensor file path as a String or an error.
fn search_i2c(base_path: &Path, bus: &str, address: &str, sensor: &Sensor) -> io::Result<String> {
let i2c_path = base_path.join(format!("i2c-{}", bus));
let device_path = i2c_path.join(format!("{}-00{}", bus, address));
debug!(
"search_i2c: hwmon_dir {:?} for sensor {:?}",
device_path.join("hwmon"),
sensor.Name
);
let hwmon_dir = fs::read_dir(device_path.join("hwmon"))?
.filter_map(Result::ok)
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Hwmon directory not found"))?;
let sensor_file = find_specific_sensor_file(&hwmon_dir.path(), sensor)?;
Ok(sensor_file.to_string_lossy().into_owned())
}
/// Finds the specific sensor file within a hwmon directory.
///
/// # Arguments
///
/// * `hwmon_path` - The path to the hwmon directory.
/// * `sensor` - The Sensor configuration to search for.
///
/// # Returns
///
/// A Result containing the PathBuf of the sensor file or an error.
fn find_specific_sensor_file(hwmon_path: &Path, sensor: &Sensor) -> io::Result<PathBuf> {
let hwmon_dir = fs::read_dir(hwmon_path)?;
let file_pattern = get_file_pattern(sensor);
debug!(
"Matching file_pattern {:?} under hwmon_dir {:?} for sensor name {}",
file_pattern, hwmon_dir, sensor.Name
);
for file in hwmon_dir {
let file = file?;
if file.file_name().to_string_lossy().contains(&file_pattern) {
debug!(
"Matching file {:?} under hwmon_path: {:?} for sensor name {}",
file, hwmon_path, sensor.Name
);
return Ok(file.path());
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
"Specific sensor file not found",
))
}
/// Gets the file pattern for a normal (non-pmbus) sensor.
///
/// # Arguments
///
/// * `sensor` - The Sensor configuration to get the pattern for.
///
/// # Returns
///
/// A String containing the file pattern for the sensor.
fn get_file_pattern_normal(sensor: &Sensor) -> String {
let mut index = 1; // Default index if not specified
if let Some(sensor_index) = sensor.Index {
index = sensor_index;
} else if let Some(labels) = &sensor.Labels {
// Assert that there's only one label
// SAFEFY: this should only be called at server initialization, so it can unwrap
assert!(
labels.len() == 1,
"Expected exactly one label, found {}",
labels.len()
);
// Extract the number suffix from the label
if let Some(label) = labels.first() {
if let Ok(number_str) = label
.chars()
.skip_while(|c| !c.is_numeric())
.collect::<String>()
.parse::<u32>()
{
index = number_str;
}
}
}
debug!("sensor {} has type {}", sensor.Name, sensor.sensor_type);
match sensor.reading_type.1 {
ReadingType::Voltage => format!("in{}_input", index),
ReadingType::Current => format!("curr{}_input", index),
ReadingType::Power => format!("power{}_input", index),
ReadingType::Temperature => format!("temp{}_input", index),
ReadingType::Fan => format!("fan{}_input", index + 1),
_ => format!("in{}_input", index), // Default to voltage for unknown types
}
}
/// Gets the file pattern for a sensor.
///
/// # Arguments
///
/// * `sensor` - The Sensor configuration to get the pattern for.
///
/// # Returns
///
/// A String containing the file pattern for the sensor.
fn get_file_pattern(sensor: &Sensor) -> String {
match sensor.sensor_type.as_str() {
// pmbus has special mapping rule, not worthy to include them in the normal
// handling logic
"pmbus" => {
if let Some(labels) = &sensor.Labels {
if let Some(label) = labels.first() {
match label.as_str() {
"vout1" => "in2_input".to_string(),
"iout1" => "curr2_input".to_string(),
"vin" => "in1_input".to_string(),
"pin" | "pout1" => "power2_input".to_string(),
"temp1" => "temp1_input".to_string(),
_ => "in1_input".to_string(),
}
} else {
"in1_input".to_string()
}
} else {
"in1_input".to_string()
}
}
_ =>
// logic for normal sensor types
{
get_file_pattern_normal(sensor)
}
}
}
/// Represents Field Replaceable Unit (FRU) information for a sensor.
#[derive(Debug, Clone)]
pub struct FruInfo {
/// FRU name for this device
pub service_label: Option<String>,
/// FRU name of its parent device, if this is an embedded device
pub location_code: Option<String>,
/// The Concatenation of all Parent FRU's ServiceLabels until root
#[allow(dead_code)]
pub part_location_context: Option<String>,
}
/// Extracts FRU information for sensors from a configuration file.
///
/// TODO: not fully verified yet
///
/// # Arguments
///
/// * `config_file` - The path to the configuration file.
///
/// # Returns
///
/// A Result containing a HashMap of sensor names to FruInfo or an error.
pub fn extract_sensor_frus(
config_file: &str,
) -> Result<HashMap<String, FruInfo>, Box<dyn std::error::Error>> {
let file_content = std::fs::read_to_string(config_file)?;
let config: Value = serde_json::from_str(&file_content)?;
let mut sensor_frus = HashMap::new();
let mut sensor_to_fan_map = HashMap::new();
if let Value::Array(configs) = config {
for c in configs {
process_component(&c, &mut sensor_frus, &mut sensor_to_fan_map)?;
}
}
// Update sensor FRU info with fan service labels
let fan_service_labels: HashMap<_, _> = sensor_frus
.iter()
.filter_map(|(name, info)| {
info.service_label
.as_ref()
.map(|label| (name.clone(), label.clone()))
})
.collect();
for (sensor_name, fan_name) in sensor_to_fan_map {
if let Some(fru_info) = sensor_frus.get_mut(&sensor_name) {
if let Some(service_label) = fan_service_labels.get(&fan_name) {
fru_info.service_label = Some(service_label.clone());
}
}
}
Ok(sensor_frus)
}
/// Processes a component from the configuration and extracts sensor FRU information.
///
/// TODO: not fully verified yet
///
/// # Arguments
///
/// * `component` - The JSON Value representing the component.
/// * `sensor_frus` - A mutable reference to the HashMap storing sensor FRU information.
/// * `sensor_to_fan_map` - A mutable reference to the HashMap mapping sensors to fans.
///
/// # Returns
///
/// A Result indicating success or an error.
fn process_component(
component: &Value,
sensor_frus: &mut HashMap<String, FruInfo>,
sensor_to_fan_map: &mut HashMap<String, String>,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(location_code) = component
.get("xyz.openbmc_project.Inventory.Decorator.LocationCode")
.and_then(|l| l.get("LocationCode"))
.and_then(|l| l.as_str())
{
let fru_info = FruInfo {
location_code: Some(location_code.to_string()),
service_label: None,
part_location_context: None,
};
if let Some(exposes) = component.get("Exposes").and_then(|e| e.as_array()) {
for expose in exposes {
if let Some(expose_type) = expose.get("Type").and_then(|t| t.as_str()) {
match expose_type {
"Fan" => {
let fan_name =
expose.get("Name").and_then(|n| n.as_str()).unwrap_or("");
let service_label = expose.get("ServiceLabel").and_then(|s| s.as_str());
let tach_sensor = expose.get("TachSensor").and_then(|t| t.as_str());
let pwm_sensor = expose.get("PWMSensor").and_then(|p| p.as_str());
let mut fan_fru_info = fru_info.clone();
fan_fru_info.service_label = service_label.map(String::from);
sensor_frus.insert(fan_name.to_string(), fan_fru_info);
if let Some(tach) = tach_sensor {
sensor_to_fan_map.insert(tach.to_string(), fan_name.to_string());
}
if let Some(pwm) = pwm_sensor {
sensor_to_fan_map.insert(pwm.to_string(), fan_name.to_string());
}
}
_ => {
extract_sensors_from_expose(expose, sensor_frus, &fru_info);
}
}
}
}
}
}
Ok(())
}
/// Extracts sensor information from an 'Expose' object in the configuration.
///
/// TODO: not fully verified yet
///
/// # Arguments
///
/// * `expose` - The JSON Value representing the 'Expose' object.
/// * `sensor_frus` - A mutable reference to the HashMap storing sensor FRU information.
/// * `fru_info` - The FruInfo for the current component.
fn extract_sensors_from_expose(
expose: &Value,
sensor_frus: &mut HashMap<String, FruInfo>,
fru_info: &FruInfo,
) {
if let Some(sensor_type) = expose.get("Type").and_then(|t| t.as_str()) {
match sensor_type {
"ADM1266" | "pmbus" | "ADM1272" => {
if let Some(labels) = expose.get("Labels").and_then(|l| l.as_array()) {
for label in labels {
if let Some(label_str) = label.as_str() {
let name_key = format!("{}_Name", label_str);
if let Some(sensor_name) =
expose.get(&name_key).and_then(|n| n.as_str())
{
sensor_frus.insert(sensor_name.to_string(), fru_info.clone());
}
}
}
}
}
"PLDM" | "MAX31725" | "TMP431" | "I2CFan" | "ADC" | "ExternalSensor" => {
if let Some(sensor_name) = expose.get("Name").and_then(|n| n.as_str()) {
sensor_frus.insert(sensor_name.to_string(), fru_info.clone());
}
}
_ => {}
}
}
}