Files
gree-controller/src/engine/history.rs
T
2026-09-15 09:04:29 +02:00

187 lines
6.3 KiB
Rust

fn installation_outdoor_temperature(state: &AppState, device_id: &str) -> Option<f64> {
let group = state.db.device_group_for_device(device_id).ok().flatten()?;
let source_id = group.outdoor_temperature_device_id.as_deref()?;
state.db.get_device(source_id).ok().flatten()?.outdoor_temperature
}
fn record_zone_history(
state: &AppState,
zone: &Zone,
outdoor_temperature: Option<f64>,
poll_interval_seconds: u64,
) {
let device = match state.db.get_device(&zone.device_id) {
Ok(Some(device)) => device,
Ok(None) => return,
Err(err) => {
tracing::warn!(error=?err, zone_id=%zone.id, "cannot load device for zone history");
return;
}
};
let reading = ZoneReading {
id: 0,
zone_id: zone.id.clone(),
device_id: zone.device_id.clone(),
timestamp: Utc::now(),
gree_temperature: zone.device_temperature.or(device.current_temperature),
external_temperature: zone.external_temperature,
control_temperature: zone
.current_temperature
.or(zone.device_temperature)
.or(device.current_temperature),
target_temperature: zone
.effective_setpoint
.or(zone.manual_setpoint)
.or(Some(zone.setpoint)),
device_setpoint: zone.device_setpoint.or(Some(device.target_temperature)),
outdoor_temperature: installation_outdoor_temperature(state, &device.id)
.or(outdoor_temperature)
.or(device.outdoor_temperature),
power: device.power,
mode: if zone.effective_mode.is_empty() {
device.mode.clone()
} else {
zone.effective_mode.clone()
},
fan_speed: device.fan_speed,
demand: zone.demand,
control_source: zone.control_temperature_source.clone(),
active_preset: zone.active_preset.clone(),
};
let interval = poll_interval_seconds.max(15) as i64;
match state.db.add_zone_reading_if_due(&reading, interval) {
Ok(true) => queue_influx_zone(state, reading),
Ok(false) => {}
Err(err) => tracing::warn!(error=?err, zone_id=%zone.id, "cannot save zone history sample"),
}
}
fn record_ha_history(
state: &AppState,
entity_id: &str,
zone_id: Option<&str>,
kind: &str,
temperature: f64,
poll_interval_seconds: u64,
) {
let reading = HaReading {
id: 0,
entity_id: entity_id.to_string(),
zone_id: zone_id.map(str::to_string),
kind: kind.to_string(),
timestamp: Utc::now(),
temperature,
};
let interval = poll_interval_seconds.max(15) as i64;
match state.db.add_ha_reading_if_due(&reading, interval) {
Ok(true) => queue_influx_ha(state, reading),
Ok(false) => {}
Err(err) => {
tracing::warn!(error=?err, entity_id=%entity_id, "cannot save Home Assistant history sample")
}
}
}
fn queue_influx_device(state: &AppState, reading: Reading) {
let state = state.clone();
tokio::spawn(async move {
let settings = state.settings.read().await.influxdb.clone();
if !settings.enabled {
return;
}
if let Err(err) = influxdb::write_device(&state.http, &settings, &reading).await {
tracing::warn!(error=?err, device_id=%reading.device_id, "cannot write device metric to InfluxDB");
}
});
}
fn queue_influx_zone(state: &AppState, reading: ZoneReading) {
let state = state.clone();
tokio::spawn(async move {
let settings = state.settings.read().await.influxdb.clone();
if !settings.enabled {
return;
}
if let Err(err) = influxdb::write_zone(&state.http, &settings, &reading).await {
tracing::warn!(error=?err, zone_id=%reading.zone_id, "cannot write zone metric to InfluxDB");
}
});
}
fn queue_influx_ha(state: &AppState, reading: HaReading) {
let state = state.clone();
tokio::spawn(async move {
let settings = state.settings.read().await.influxdb.clone();
if !settings.enabled {
return;
}
if let Err(err) = influxdb::write_ha(&state.http, &settings, &reading).await {
tracing::warn!(error=?err, entity_id=%reading.entity_id, "cannot write HA metric to InfluxDB");
}
});
}
fn median_outdoor_temperature(mut values: Vec<f64>) -> Option<f64> {
if values.is_empty() {
return None;
}
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let middle = values.len() / 2;
let value = if values.len() % 2 == 0 {
(values[middle - 1] + values[middle]) / 2.0
} else {
values[middle]
};
Some((value * 10.0).round() / 10.0)
}
fn valid_outdoor_temperature(device: &Device) -> Option<f64> {
if !device.enabled || !device.online || device.communication_failures != 0 {
return None;
}
device
.outdoor_temperature
.filter(|value| value.is_finite() && (-60.0..=70.0).contains(value))
}
fn gree_outdoor_temperature(devices: &[Device], device_groups: &[DeviceGroup]) -> Option<f64> {
let by_id: HashMap<&str, &Device> = devices
.iter()
.map(|device| (device.id.as_str(), device))
.collect();
let mut grouped_ids = HashSet::new();
let mut values = Vec::new();
for group in device_groups {
for device_id in &group.device_ids {
grouped_ids.insert(device_id.as_str());
}
let group_value = group
.outdoor_temperature_device_id
.as_deref()
.and_then(|source_id| by_id.get(source_id).copied())
.and_then(valid_outdoor_temperature)
.or_else(|| {
median_outdoor_temperature(
group
.device_ids
.iter()
.filter_map(|id| by_id.get(id.as_str()).copied())
.filter_map(valid_outdoor_temperature)
.collect(),
)
});
if let Some(value) = group_value {
values.push(value);
}
}
values.extend(
devices
.iter()
.filter(|device| !grouped_ids.contains(device.id.as_str()))
.filter_map(valid_outdoor_temperature),
);
median_outdoor_temperature(values)
}