187 lines
6.3 KiB
Rust
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)
|
|
}
|