use crate::observation::source;
use crate::observation::{
BindContext, Perturbation, Sensor, SensorDescriptor, SensorId, SensorOutcome, StateWriter,
Uncertainty,
};
use corescout_core::error::{Error, Result};
use corescout_mirror::entity::{keys, Entity, EntityClass};
use corescout_mirror::relation::{Relation, RelationKind};
use corescout_mirror::state::{ChannelId, Semantics, Unit};
use std::path::PathBuf;
const MILLIDEGREES: f64 = 1000.0;
struct Target {
row: u32,
input: PathBuf,
}
pub struct ThermalSensor {
targets: Vec<Target>,
channel: Option<ChannelId>,
}
impl ThermalSensor {
pub fn new() -> ThermalSensor {
ThermalSensor {
targets: Vec::new(),
channel: None,
}
}
}
impl Default for ThermalSensor {
fn default() -> Self {
Self::new()
}
}
impl Sensor for ThermalSensor {
fn descriptor(&self) -> SensorDescriptor {
SensorDescriptor {
id: SensorId(3),
key: "thermal",
physical_fact: "temperature at each exposed thermal sensor, in degrees Celsius",
source: "/sys/class/thermal and /sys/class/hwmon",
max_rate_hz: 10.0,
perturbation: Perturbation::Low,
uncertainty: Uncertainty::absolute(
1.0,
"on-die thermal diodes are specified to about 1 C and are filtered, so the \
reading lags the junction temperature it describes",
),
requires_privilege: false,
}
}
fn bind(&mut self, ctx: &mut BindContext<'_>) -> Result<()> {
let sys = ctx.substrate().roots.sys.clone();
let mut targets = Vec::new();
for (index, path) in source::numbered_children(sys.join("class/thermal"), "thermal_zone") {
let Some(temp) = path.join("temp").exists().then(|| path.join("temp")) else {
continue;
};
let zone_type = source::string(path.join("type")).unwrap_or_else(|| "zone".to_string());
let row = ctx.declare_entity(Entity::new(
keys::thermal_zone(&zone_type, index),
EntityClass::ThermalZone,
Some(index),
));
if let Some(machine) = ctx.row_of("machine") {
ctx.declare_relation(Relation::new(row, machine, RelationKind::ThermalDomain));
}
targets.push(Target { row, input: temp });
}
for (_, path) in source::numbered_children(sys.join("class/hwmon"), "hwmon") {
let chip = source::string(path.join("name")).unwrap_or_else(|| "hwmon".to_string());
for index in 1..=32u32 {
let input = path.join(format!("temp{index}_input"));
if !input.exists() {
continue;
}
let label = source::string(path.join(format!("temp{index}_label")))
.unwrap_or_else(|| format!("temp{index}"));
let row = ctx.declare_entity(Entity::new(
keys::thermal_zone(&format!("{chip}/{label}"), index),
EntityClass::ThermalZone,
Some(index),
));
let linked = label
.strip_prefix("Core ")
.and_then(|n| n.trim().parse::<u32>().ok())
.and_then(|core_id| {
ctx.substrate()
.topology
.physical_cores
.iter()
.find(|c| c.core_id == core_id)
.map(|c| (c.package_id, c.core_id))
})
.and_then(|(package, core)| ctx.row_of(&keys::physical_core(package, core)));
let target_row = linked.or_else(|| ctx.row_of("machine"));
if let Some(target) = target_row {
ctx.declare_relation(Relation::new(row, target, RelationKind::ThermalDomain));
}
targets.push(Target { row, input });
}
}
if targets.is_empty() {
return Err(Error::unsupported(
"no thermal sensors are exposed (common in VMs and containers)",
));
}
self.targets = targets;
self.channel =
Some(ctx.declare_channel("thermal.temperature", Unit::Celsius, Semantics::Instant));
Ok(())
}
fn observe(&mut self, out: &mut StateWriter<'_>) -> SensorOutcome {
let mut outcome = SensorOutcome::default();
let Some(channel) = self.channel else {
return outcome;
};
for target in &self.targets {
match source::f64(&target.input) {
Some(millidegrees) => {
out.set(target.row, channel, millidegrees / MILLIDEGREES);
outcome.sample();
}
None => outcome.error(),
}
}
outcome
}
}