v0.5.4
This commit is contained in:
+85
-15
@@ -154,6 +154,8 @@ pub async fn send_command(state: &AppState, device_id: &str, command: DeviceComm
|
||||
}
|
||||
}
|
||||
}
|
||||
if command.quiet.is_some() && applied_command.quiet.is_none() { device.supports_quiet = Some(false); }
|
||||
if command.sleep.is_some() && applied_command.sleep.is_none() { device.supports_sleep = Some(false); }
|
||||
applied_command.apply(&mut device);
|
||||
device.online = true;
|
||||
device.communication_failures = 0;
|
||||
@@ -309,18 +311,22 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
let schedules = state.db.list_schedules()?;
|
||||
let settings = state.settings.read().await.clone();
|
||||
|
||||
// Outdoor temperature is deliberately optional. It never replaces the room sensor;
|
||||
// it only makes the active setpoint/fan a little more assertive in extreme weather.
|
||||
let outdoor_temperature = if !settings.home_assistant.outdoor_entity_id.trim().is_empty() {
|
||||
match home_assistant::read_temperature(
|
||||
&state.http,
|
||||
&settings.home_assistant,
|
||||
Some(settings.home_assistant.outdoor_entity_id.trim()),
|
||||
).await {
|
||||
// Outdoor temperature is deliberately optional. Prefer the configured Home
|
||||
// Assistant entity, but keep the dashboard/assist useful by falling back to the
|
||||
// outdoor sensors reported by GREE units when HA is temporarily unavailable.
|
||||
let device_snapshot = state.db.list_devices()?;
|
||||
let configured_outdoor = settings.home_assistant.outdoor_entity_id.trim();
|
||||
let resolved_outdoor = if configured_outdoor.is_empty() {
|
||||
None
|
||||
} else {
|
||||
home_assistant::resolve_entity_id(&settings.home_assistant, Some(configured_outdoor))
|
||||
};
|
||||
let ha_outdoor_temperature = if let Some(entity_id) = resolved_outdoor.as_deref() {
|
||||
match home_assistant::read_temperature(&state.http, &settings.home_assistant, Some(entity_id)).await {
|
||||
Ok(value) => {
|
||||
record_ha_history(
|
||||
state,
|
||||
settings.home_assistant.outdoor_entity_id.trim(),
|
||||
entity_id,
|
||||
None,
|
||||
"outdoor",
|
||||
value,
|
||||
@@ -329,13 +335,14 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
Some(value)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::debug!(error=?err, "outdoor Home Assistant sensor unavailable");
|
||||
tracing::debug!(configured_entity=%configured_outdoor, resolved_entity=%entity_id, error=?err, "outdoor Home Assistant sensor unavailable; trying GREE fallback");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let outdoor_temperature = ha_outdoor_temperature.or_else(|| gree_outdoor_temperature(&device_snapshot));
|
||||
{
|
||||
let mut current = state.outdoor_temperature.write().await;
|
||||
if *current != outdoor_temperature {
|
||||
@@ -372,16 +379,21 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
let previous_source = zone.control_temperature_source.clone();
|
||||
let device_temperature = device.current_temperature;
|
||||
let external_temperature = if matches!(zone.sensor_source.as_str(), "home_assistant" | "combined") {
|
||||
match home_assistant::read_temperature(&state.http, &settings.home_assistant, zone.ha_entity_id.as_deref()).await {
|
||||
let resolved_entity = home_assistant::resolve_entity_id(&settings.home_assistant, zone.ha_entity_id.as_deref());
|
||||
match home_assistant::read_temperature(&state.http, &settings.home_assistant, resolved_entity.as_deref()).await {
|
||||
Ok(value) => {
|
||||
if let Some(entity_id) = zone.ha_entity_id.as_deref().filter(|value| !value.trim().is_empty()) {
|
||||
if let Some(entity_id) = resolved_entity.as_deref() {
|
||||
record_ha_history(state, entity_id, Some(&zone.id), "room", value, settings.poll_interval_seconds);
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
Err(err) => {
|
||||
if !matches!(previous_source.as_str(), "device_fallback" | "device_discrepancy_fallback") {
|
||||
state.log("warn", "ha.sensor_error", &err.to_string(), json!({"zone_id": zone.id, "entity_id": zone.ha_entity_id.as_deref()}));
|
||||
state.log("warn", "ha.sensor_error", &err.to_string(), json!({
|
||||
"zone_id": zone.id,
|
||||
"configured_entity_id": zone.ha_entity_id.as_deref(),
|
||||
"resolved_entity_id": resolved_entity,
|
||||
}));
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -497,12 +509,20 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
night_active,
|
||||
settings.night_mode.force_quiet,
|
||||
);
|
||||
let desired_sleep = native_sleep_command(
|
||||
settings.night_mode.enabled,
|
||||
night_active,
|
||||
settings.night_mode.use_native_sleep,
|
||||
device.supports_sleep == Some(true) && state.gree.sleep_command_supported(&device.id),
|
||||
device.sleep,
|
||||
);
|
||||
|
||||
let needs_command = !device.power
|
||||
|| device.mode != effective_mode
|
||||
|| (device.target_temperature - desired_device_target).abs() >= 0.5
|
||||
|| desired_fan.map(|fan| fan != device.fan_speed).unwrap_or(false)
|
||||
|| desired_quiet.map(|quiet| quiet != device.quiet).unwrap_or(false);
|
||||
|| desired_quiet.map(|quiet| quiet != device.quiet).unwrap_or(false)
|
||||
|| desired_sleep.map(|sleep| sleep != device.sleep).unwrap_or(false);
|
||||
|
||||
let urgent_mode_change = !device.power || device.mode != effective_mode;
|
||||
if needs_command && (urgent_mode_change || adjustment_allowed(&zone)) {
|
||||
@@ -512,6 +532,7 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
target_temperature: Some(desired_device_target),
|
||||
fan_speed: desired_fan,
|
||||
quiet: desired_quiet,
|
||||
sleep: desired_sleep,
|
||||
..Default::default()
|
||||
};
|
||||
match send_command(state, &zone.device_id, command).await {
|
||||
@@ -527,6 +548,7 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
"outdoor_temperature": outdoor_temperature,
|
||||
"fan_speed": updated_device.fan_speed,
|
||||
"quiet": updated_device.quiet,
|
||||
"sleep": updated_device.sleep,
|
||||
"night_mode": night_active,
|
||||
}));
|
||||
}
|
||||
@@ -633,6 +655,23 @@ fn queue_influx_ha(state: &AppState, reading: HaReading) {
|
||||
});
|
||||
}
|
||||
|
||||
fn gree_outdoor_temperature(devices: &[Device]) -> Option<f64> {
|
||||
let mut values: Vec<f64> = devices.iter()
|
||||
.filter(|device| device.enabled && device.online)
|
||||
.filter_map(|device| device.outdoor_temperature)
|
||||
.filter(|value| value.is_finite() && (-60.0..=70.0).contains(value))
|
||||
.collect();
|
||||
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 select_zone_temperature(zone: &Zone, device_temperature: Option<f64>, external_temperature: Option<f64>) -> (Option<f64>, String, bool) {
|
||||
match zone.sensor_source.as_str() {
|
||||
"home_assistant" => match (external_temperature, device_temperature) {
|
||||
@@ -707,6 +746,19 @@ fn smart_quiet_command(
|
||||
None
|
||||
}
|
||||
|
||||
fn native_sleep_command(
|
||||
night_enabled: bool,
|
||||
night_active: bool,
|
||||
use_native_sleep: bool,
|
||||
sleep_supported: bool,
|
||||
device_sleep: bool,
|
||||
) -> Option<bool> {
|
||||
if !night_enabled || !use_native_sleep || !sleep_supported { return None; }
|
||||
if night_active { return Some(true); }
|
||||
if device_sleep { return Some(false); }
|
||||
None
|
||||
}
|
||||
|
||||
fn night_limited_fan_speed(requested: u8, max_fan: u8) -> u8 {
|
||||
let max_fan = max_fan.clamp(1, 5);
|
||||
if requested == 0 { 1 } else { requested.min(max_fan) }
|
||||
@@ -854,6 +906,8 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
device_name: device.map(|item| item.name.clone()).unwrap_or_else(|| zone.device_id.clone()),
|
||||
enabled: zone.enabled,
|
||||
mode: effective_mode.to_string(),
|
||||
configured_mode: zone.mode.clone(),
|
||||
inherit_house_mode: zone.inherit_house_mode,
|
||||
preset: if effective_mode == "off" { "off".into() } else if zone.active_preset.is_empty() { preset } else { zone.active_preset.clone() },
|
||||
current_temperature: zone.current_temperature,
|
||||
target_temperature: if effective_mode == "off" { None } else { zone.effective_setpoint.or(target) },
|
||||
@@ -1156,7 +1210,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn night_mode_handles_midnight_and_limits_auto_fan() {
|
||||
let settings = NightModeSettings { enabled: true, start_time: "22:00".into(), end_time: "06:00".into(), max_fan_speed: 1, force_quiet: true };
|
||||
let settings = NightModeSettings { enabled: true, start_time: "22:00".into(), end_time: "06:00".into(), max_fan_speed: 1, force_quiet: true, use_native_sleep: true };
|
||||
assert!(night_mode_active(&settings, NaiveTime::from_hms_opt(23, 30, 0).unwrap()));
|
||||
assert!(night_mode_active(&settings, NaiveTime::from_hms_opt(5, 59, 0).unwrap()));
|
||||
assert!(!night_mode_active(&settings, NaiveTime::from_hms_opt(12, 0, 0).unwrap()));
|
||||
@@ -1164,6 +1218,22 @@ mod tests {
|
||||
assert_eq!(night_limited_fan_speed(3, 1), 1);
|
||||
assert_eq!(smart_quiet_command(false, true, true, true, false, true, true, true), Some(true));
|
||||
assert_eq!(smart_quiet_command(false, true, true, true, true, true, false, true), Some(false));
|
||||
assert_eq!(native_sleep_command(true, true, true, true, false), Some(true));
|
||||
assert_eq!(native_sleep_command(true, false, true, true, true), Some(false));
|
||||
assert_eq!(native_sleep_command(true, true, true, false, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gree_outdoor_fallback_uses_median_of_online_units() {
|
||||
let mut a = Device::simulated_default();
|
||||
a.outdoor_temperature = Some(10.0);
|
||||
let mut b = Device::simulated_default();
|
||||
b.id = "sim-b".into();
|
||||
b.outdoor_temperature = Some(12.0);
|
||||
let mut c = Device::simulated_default();
|
||||
c.id = "sim-c".into();
|
||||
c.outdoor_temperature = Some(40.0);
|
||||
assert_eq!(gree_outdoor_temperature(&[a, b, c]), Some(12.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user