v0.8.2
This commit is contained in:
+3
-1
@@ -1897,6 +1897,7 @@ async fn update_settings(State(state): State<AppState>, Json(mut input): Json<Ru
|
||||
}
|
||||
if input.controller_id.trim().is_empty() { input.controller_id = old.controller_id; }
|
||||
if input.home_assistant.token.trim().is_empty() { input.home_assistant.token = old.home_assistant.token; }
|
||||
input.home_assistant.sensor_stale_after_seconds = input.home_assistant.sensor_stale_after_seconds.clamp(30, 86_400);
|
||||
if input.notifications.pushover_app_token.trim().is_empty() { input.notifications.pushover_app_token = old.notifications.pushover_app_token; }
|
||||
if input.notifications.pushover_user_key.trim().is_empty() { input.notifications.pushover_user_key = old.notifications.pushover_user_key; }
|
||||
if input.notifications.slack_webhook_url.trim().is_empty() { input.notifications.slack_webhook_url = old.notifications.slack_webhook_url; }
|
||||
@@ -2189,7 +2190,7 @@ struct HaTestRequest { entity_id: Option<String> }
|
||||
async fn test_home_assistant(State(state): State<AppState>, Json(input): Json<HaTestRequest>) -> Result<Json<Value>, AppError> {
|
||||
let settings = state.settings.read().await.clone();
|
||||
let resolved_entity_id = home_assistant::resolve_entity_id(&settings.home_assistant, input.entity_id.as_deref());
|
||||
let temperature = home_assistant::read_temperature(&state.http, &settings.home_assistant, resolved_entity_id.as_deref(), Some(300))
|
||||
let temperature = home_assistant::read_temperature(&state.http, &settings.home_assistant, resolved_entity_id.as_deref(), Some(settings.home_assistant.sensor_stale_after_seconds))
|
||||
.await.map_err(|e| AppError::Device(e.to_string()))?;
|
||||
Ok(Json(json!({"ok": true, "temperature_c": temperature, "entity_id": resolved_entity_id})))
|
||||
}
|
||||
@@ -2270,6 +2271,7 @@ fn public_settings(settings: &RuntimeSettings) -> Value {
|
||||
"token_configured": !settings.home_assistant.token.trim().is_empty(),
|
||||
"default_entity_id": settings.home_assistant.default_entity_id,
|
||||
"outdoor_entity_id": settings.home_assistant.outdoor_entity_id,
|
||||
"sensor_stale_after_seconds": settings.home_assistant.sensor_stale_after_seconds,
|
||||
"allow_invalid_tls": settings.home_assistant.allow_invalid_tls,
|
||||
"sensor_aliases": settings.home_assistant.sensor_aliases,
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ impl Config {
|
||||
token: env::var("HA_TOKEN").unwrap_or_default(),
|
||||
default_entity_id: env::var("HA_ENTITY_ID").unwrap_or_default(),
|
||||
outdoor_entity_id: env::var("HA_OUTDOOR_ENTITY_ID").unwrap_or_default(),
|
||||
sensor_stale_after_seconds: env_u64("HA_SENSOR_STALE_AFTER_SECONDS").unwrap_or(300).clamp(30, 86_400),
|
||||
allow_invalid_tls: env::var("HA_ALLOW_INVALID_TLS")
|
||||
.map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
|
||||
.unwrap_or(false),
|
||||
@@ -105,6 +106,7 @@ impl Config {
|
||||
if let Some(value) = env_u8("GREE_CONTROLLER_NIGHT_MODE_MAX_FAN_SPEED") { settings.night_mode.max_fan_speed = value.clamp(1, 5); }
|
||||
if let Some(value) = env_bool("GREE_CONTROLLER_NIGHT_MODE_FORCE_QUIET") { settings.night_mode.force_quiet = value; }
|
||||
if let Some(value) = env_bool("GREE_CONTROLLER_NIGHT_MODE_NATIVE_SLEEP") { settings.night_mode.use_native_sleep = value; }
|
||||
if let Some(value) = env_u64("HA_SENSOR_STALE_AFTER_SECONDS") { settings.home_assistant.sensor_stale_after_seconds = value.clamp(30, 86_400); }
|
||||
|
||||
let influx_env_present = [
|
||||
"GREE_CONTROLLER_INFLUX_ENABLED", "GREE_CONTROLLER_INFLUX_VERSION", "GREE_CONTROLLER_INFLUX_URL",
|
||||
@@ -147,6 +149,7 @@ fn env_bool(name: &str) -> Option<bool> {
|
||||
}
|
||||
|
||||
fn env_u32(name: &str) -> Option<u32> { env::var(name).ok()?.parse().ok() }
|
||||
fn env_u64(name: &str) -> Option<u64> { env::var(name).ok()?.parse().ok() }
|
||||
fn env_u8(name: &str) -> Option<u8> { env::var(name).ok()?.parse().ok() }
|
||||
|
||||
fn first_env(names: &[&str]) -> Option<String> {
|
||||
|
||||
+18
-2
@@ -1158,7 +1158,7 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
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), Some(300)).await {
|
||||
match home_assistant::read_temperature(&state.http, &settings.home_assistant, Some(entity_id), Some(settings.home_assistant.sensor_stale_after_seconds)).await {
|
||||
Ok(value) => {
|
||||
record_ha_history(
|
||||
state,
|
||||
@@ -1204,8 +1204,9 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
let resolved_entity = home_assistant::resolve_entity_id(&settings.home_assistant, zone.ha_entity_id.as_deref());
|
||||
let http = &state.http;
|
||||
let ha_settings = &settings.home_assistant;
|
||||
let stale_after_seconds = effective_sensor_stale_after_seconds(zone.sensor_stale_after_seconds, ha_settings.sensor_stale_after_seconds);
|
||||
Some(async move {
|
||||
let result = home_assistant::read_temperature(http, ha_settings, resolved_entity.as_deref(), Some(zone.sensor_stale_after_seconds)).await
|
||||
let result = home_assistant::read_temperature(http, ha_settings, resolved_entity.as_deref(), Some(stale_after_seconds)).await
|
||||
.map_err(|err| err.to_string());
|
||||
(zone_id, resolved_entity, result)
|
||||
})
|
||||
@@ -1773,6 +1774,13 @@ fn external_room_sensor_cooling_assist(mode: &str, control_source: &str) -> f64
|
||||
if mode == "cool" && matches!(control_source, "external" | "combined") { 0.5 } else { 0.0 }
|
||||
}
|
||||
|
||||
fn effective_sensor_stale_after_seconds(zone_value: u64, global_value: u64) -> u64 {
|
||||
let global = global_value.clamp(30, 86_400);
|
||||
// 0 and the historical hidden default (300 s) mean "inherit the HA setting".
|
||||
// A non-default value supplied through the existing zone API remains a per-zone override.
|
||||
if zone_value == 0 || zone_value == 300 { global } else { zone_value.clamp(30, 86_400) }
|
||||
}
|
||||
|
||||
fn round_device_setpoint(mode: &str, demand: bool, value: f64) -> f64 {
|
||||
let value = value.clamp(16.0, 30.0);
|
||||
match (mode, demand) {
|
||||
@@ -2407,6 +2415,14 @@ mod tests {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
|
||||
#[test]
|
||||
fn global_ha_sensor_stale_timeout_is_used_for_default_zone_value() {
|
||||
assert_eq!(effective_sensor_stale_after_seconds(300, 600), 600);
|
||||
assert_eq!(effective_sensor_stale_after_seconds(0, 900), 900);
|
||||
assert_eq!(effective_sensor_stale_after_seconds(120, 600), 120);
|
||||
assert_eq!(effective_sensor_stale_after_seconds(120_000, 600), 86_400);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overnight_schedule_works() {
|
||||
let now = Utc.with_ymd_and_hms(2025, 1, 7, 1, 0, 0).unwrap().with_timezone(&Local); // Tuesday
|
||||
|
||||
@@ -93,6 +93,7 @@ mod tests {
|
||||
token: "token".into(),
|
||||
default_entity_id: "sensor.salon_temperature".into(),
|
||||
outdoor_entity_id: "sensor.zewnatrz_temperature".into(),
|
||||
sensor_stale_after_seconds: 300,
|
||||
allow_invalid_tls: false,
|
||||
sensor_aliases,
|
||||
}
|
||||
|
||||
@@ -582,6 +582,9 @@ pub struct HomeAssistantSettings {
|
||||
/// Optional outdoor temperature sensor used only as an assist signal.
|
||||
#[serde(default)]
|
||||
pub outdoor_entity_id: String,
|
||||
/// Maximum accepted age of Home Assistant sensor samples.
|
||||
#[serde(default = "default_sensor_stale_after")]
|
||||
pub sensor_stale_after_seconds: u64,
|
||||
/// Accept self-signed/expired certificates for local Home Assistant HTTPS.
|
||||
#[serde(default)]
|
||||
pub allow_invalid_tls: bool,
|
||||
|
||||
Reference in New Issue
Block a user