fn installation_outdoor_temperature(state: &AppState, device_id: &str) -> Option { 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, 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) -> Option { 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 { 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 { 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) }