v0.8.2
This commit is contained in:
@@ -1,3 +1,42 @@
|
||||
# GREE Controller v0.8.2 - build and validation report
|
||||
|
||||
## 0.8.2 UI/i18n + Home Assistant sensor freshness
|
||||
|
||||
Source baseline: v0.8.1.
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed the 0.8.x ownership translations: `zones.controlOwner`, owner labels and owner timing labels are now inside each language pack's `translations` object, so the Web UI receives translated values instead of raw translation keys.
|
||||
- Simplified the Polish owner caption from `Właściciel sterowania` to `Sterowanie` and allowed the owner/timing line to wrap instead of truncating.
|
||||
- Added `home_assistant.sensor_stale_after_seconds` with a serde-compatible 300 s default. The Home Assistant / Sensors page exposes it as minutes (1-1440 min); the backend clamps API/env values to 30-86400 s.
|
||||
- The configured freshness limit is used by room HA sensors, the outdoor HA sensor and the HA connection test. Existing non-default `zone.sensor_stale_after_seconds` values remain per-zone API overrides; the historical hidden/default 300 s value inherits the global HA setting.
|
||||
- Added `HA_SENSOR_STALE_AFTER_SECONDS` as an optional environment override.
|
||||
- Quick Thermostat and Direct Device dashboard grids now use `align-items:start`, so a taller manual/ownership panel no longer visually stretches neighbouring cards in the same row.
|
||||
- `build.rs` now rejects unexpected top-level language-pack keys, preventing translation keys from being accidentally stored outside `translations` again.
|
||||
- Added a Rust regression test for global/per-zone stale-timeout resolution.
|
||||
- Bumped controller and Home Assistant integration version to 0.8.2 and changed the service-worker cache key.
|
||||
|
||||
### Validation executed in this environment
|
||||
|
||||
- `node --check web/app.js`
|
||||
- `node --check web/theme-init.js`
|
||||
- `node --check web/sw.js`
|
||||
- parsed all JSON files
|
||||
- verified both language packs contain only `meta` + `translations`
|
||||
- verified all 564 statically referenced UI translation keys exist in both English and Polish packs
|
||||
- parsed `web/index.html`
|
||||
- `python3 -m compileall -q home-assistant scripts make_zip.py`
|
||||
- `bash -n scripts/*.sh`
|
||||
- checked version consistency across Cargo.toml, Cargo.lock, README and Home Assistant manifest
|
||||
- checked that no HA temperature read still uses a hardcoded `Some(300)` freshness limit
|
||||
- reviewed the complete 0.8.1 -> 0.8.2 source diff for unintended file changes
|
||||
|
||||
### Rust toolchain note
|
||||
|
||||
This execution environment does not contain `cargo`, `rustc` or `rustfmt`, so the Rust regression test and full compilation cannot be executed here. The existing installer/updater still runs `cargo test --all-targets` before touching the running service and then performs the release build. If Rust compilation or tests fail on the target host, the running service is not replaced.
|
||||
|
||||
---
|
||||
|
||||
# GREE Controller v0.8.1 - build and validation report
|
||||
|
||||
## 0.8.1 Rust borrow-checker hotfix
|
||||
|
||||
Generated
+1
-1
@@ -633,7 +633,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+19
-19
@@ -1,12 +1,12 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
b1347a7cbb561b78c52a6045b162bf8af46b841abdca3b1e25a01225eb34c189 ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
4b11d1d60ebbdbfc91c6caef4907559d29a096e7a000f75a74b9db58ee56ff44 ./BUILD_REPORT.md
|
||||
13e555cc191550f5d09e87dc0f041bbbae55c4fc70afe98e1398c44b99e1a3d0 ./Cargo.lock
|
||||
c1cb0011d206c38dd84d7d3668c58998ecb3eb95afd7ae64a62d4b1ddd5bf614 ./Cargo.toml
|
||||
f3b1bbc242d26f4079394b893b2ab3c106920a7d6822fd92f7788cd6a2e1ad16 ./BUILD_REPORT.md
|
||||
029e59328b60468a5da40bd17aaae6a4a0082eab85b2033ad375d0c31baca967 ./Cargo.lock
|
||||
a815c1e8134a4d3f9afee6ab53911a5be6b0ce746435722787e150ab219c45e0 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
14fa3424666b9805ee65b1744e9ac0cef449339adec17e451bd68182d8ea74d2 ./README.md
|
||||
f2c8eb28fedb26bb8f5636e1805e7a782cc472b87d882f6752d520f16216a248 ./build.rs
|
||||
3f774fbdc32de8f437d7cf9c1ceb2ae195c674d0f48b8891b25cf36d152ee2c4 ./docs/API.md
|
||||
05b5ebed3bb136b648e5a1d97ca6b268cef898d4bf8ab8d92abfcf66bbf5242e ./README.md
|
||||
91c826e2f8c974bdb7b15e01e9b20487f06ff887247ecd9600f1263c11a61b96 ./build.rs
|
||||
4fe34356d313ab624b4bb0de09c271999262cbd363cc1ede15af89a93a82b3ea ./docs/API.md
|
||||
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
|
||||
7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md
|
||||
10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md
|
||||
@@ -24,7 +24,7 @@ ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assista
|
||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
|
||||
559f47833e772277016976da5e1b717468083203f6e15a83b6c66f6d52992fbc ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
d54d23f1f1dc633f26c493b138d061deb97a063b3d79dd91f85aaddf126bc427 ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
|
||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
|
||||
1cae12876eb81085910907cf1fcf340a18dcba4f8c967ef222c9496325057849 ./home-assistant/custom_components/gree_controller/sensor.py
|
||||
@@ -33,8 +33,8 @@ c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assista
|
||||
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
|
||||
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
2bb4ce4b0eff314bd20288a0d93b0e4dd38fe22a2367bf912434c9b7e658b88f ./lang/en.json
|
||||
11e23a02a9c979860f086d50f71591084629ae198df08ab3065fdb3e2236d6ee ./lang/pl.json
|
||||
9f5a96bc8837aa5ea303b8b0dc10fcb629e084f64dfbb8f3b427705897e5ecdb ./lang/en.json
|
||||
e0099f407b5ba49a8fe21bfc0fc8f7c0a092806c42bcc28061e6306f23b0ea8d ./lang/pl.json
|
||||
028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py
|
||||
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
@@ -47,15 +47,15 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
723c2c18245079c44b24e66d738dd77a3e6b4d2ac934f2329b7cc7ae62f7a2e3 ./src/api.rs
|
||||
32b1d0a7ac2060fd18fe7f1ca1d110ee45beaf67e81514d24d6bde3d36301387 ./src/config.rs
|
||||
0bc3da5b1148aa54d2b234592d740bc262738d386f27b2f316465651ebedf891 ./src/api.rs
|
||||
9040e8cb6647c76a875148b7591abcccfe4e2d4708ef462a1e5d25dd4ccac911 ./src/config.rs
|
||||
5dfda2f4dc540c502885b0cd7017dc77768684588acf528f01d1fd88f1af4aec ./src/db.rs
|
||||
3f70c6cf618c8dbf5744876a611e2fd8d9be6362c25e680dee55a23eaef93b08 ./src/engine.rs
|
||||
83f1a0bd19bc00b9f13e5207e11b6e21ca7d2b385136330f273ca502b292cecd ./src/engine.rs
|
||||
4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs
|
||||
fbe5b18bfe1a8298c99850bfab54c2bf13c1824c0df40669ad52eff125aab964 ./src/home_assistant.rs
|
||||
c6ff66da9ad08506f839ec56a869ead3abc824b5311e0ea9244517d11f7f4207 ./src/home_assistant.rs
|
||||
190b0a33431539676e5dd7796698077f16c179d42eae4501ca96a91bf797cbf8 ./src/influxdb.rs
|
||||
c67212da6a2bd5c2933ece31586ca4048b484fda3105f23a6e06e881b079d129 ./src/main.rs
|
||||
dcd73be37622f06de921175ab4f9be8b7d22f23994c32496b7b0ccd39db7560f ./src/models.rs
|
||||
f74f5ac1940cd1c9cd8ee610c56f68b9fe17646c97736e951cd1c42408bd5b89 ./src/models.rs
|
||||
ea4bd7f20759101c2525600a1f342d2fc0946e2f0b3208b03cc5a415b6947c10 ./src/notifications.rs
|
||||
7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs
|
||||
bc03d88e5476386747ff5e32f1788cdc1fcd4bef238ab66df2dada282c348c8f ./src/protocol/gree.rs
|
||||
@@ -63,10 +63,10 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol
|
||||
6a1c0cab3eab80ecd254c5e486b1fba068523edb8d699c64054c17f67f4a31d0 ./src/queries.rs
|
||||
2d69811db832c90ce06035ee29665205a04e4c514a21ecae9623991a9b14d825 ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
591b0153e307d025281cb72c8355d84cf8caab0db4633aca9cbec08676f25d2a ./web/app.js
|
||||
cc9f2b846fb66fcb540851bdbe4d751fd297f04dac3d6a076fe1597281059992 ./web/app.js
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
e97db3feed31236152da42726e50455dcac08620a0da81cc4208c325b986fde5 ./web/index.html
|
||||
a4127eb010479bf50a940edbddd068ab3afb6e5386e4ddc383950112fe499b6a ./web/index.html
|
||||
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
|
||||
1bccea6bfdbd0d5fec82f777886ce4704823952012f6f5625c0d98bca74d2dc4 ./web/styles.css
|
||||
f44697b9306c14224bed65b084cdcd2a453e876e1fc7e3db8d3d153a94c76ff3 ./web/sw.js
|
||||
1404cd00f52605c347b86ef84d95c74c47f48f5fa82e763b0ae99c62e9800850 ./web/styles.css
|
||||
06d7293a1e9b2022503e58d06a43b27dadb8cc10acfe3dbb3bec2325b33b2b04 ./web/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Standalone local GREE air-conditioner controller written in Rust. It runs on a regular Linux host or an LXC container and provides a mobile-first web interface without depending on the vendor cloud.
|
||||
|
||||
Current version: **0.8.1**.
|
||||
Current version: **0.8.2**.
|
||||
|
||||
## Highlights
|
||||
|
||||
@@ -47,7 +47,7 @@ See [`BUILD_REPORT.md`](BUILD_REPORT.md) for package validation details and [`do
|
||||
On Debian, Ubuntu or an LXC container:
|
||||
|
||||
```bash
|
||||
unzip gree-controller-v0.8.1.zip
|
||||
unzip gree-controller-v0.8.2.zip
|
||||
cd gree-controller
|
||||
chmod +x scripts/*.sh
|
||||
./scripts/dev.sh
|
||||
@@ -479,6 +479,15 @@ Zones now use setpoint modulation, global seasonal house mode, cooling/heating C
|
||||
|
||||
|
||||
|
||||
|
||||
## 0.8.2 UI/i18n and Home Assistant sensor freshness
|
||||
|
||||
- fixes ownership translations that were accidentally stored outside the language-pack `translations` object,
|
||||
- adds a configurable Home Assistant sensor freshness limit in the Home Assistant / Sensors settings page (default 5 minutes),
|
||||
- keeps legacy non-default per-zone stale limits as API-level overrides,
|
||||
- prevents taller Quick Thermostat / Direct Device cards from stretching neighbouring dashboard cards,
|
||||
- adds build-time validation for misplaced language-pack keys.
|
||||
|
||||
## 0.8.0 Control ownership and thermostat safety
|
||||
|
||||
- Adds an explicit zone control-ownership model (`control_owner`, command source, since/resume/reason) shared by the engine, API, Web UI and Home Assistant control plan.
|
||||
|
||||
@@ -41,6 +41,14 @@ fn main() {
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
|
||||
let document: Value = serde_json::from_str(&source)
|
||||
.unwrap_or_else(|error| panic!("{filename}: invalid JSON: {error}"));
|
||||
let root_object = document.as_object().unwrap_or_else(|| panic!("{filename}: language pack root must be an object"));
|
||||
let unexpected: Vec<&str> = root_object.keys()
|
||||
.filter(|key| key.as_str() != "meta" && key.as_str() != "translations")
|
||||
.map(|key| key.as_str())
|
||||
.collect();
|
||||
if !unexpected.is_empty() {
|
||||
panic!("{filename}: unexpected top-level keys {:?}; translation keys must be inside 'translations'", unexpected);
|
||||
}
|
||||
let meta = document.get("meta").unwrap_or_else(|| panic!("{filename}: missing meta object"));
|
||||
let code = required_string(meta, "code", filename);
|
||||
let name = required_string(meta, "name", filename);
|
||||
|
||||
@@ -198,6 +198,8 @@ For non-`custom` schedules, `setpoint` is only a compatibility value; the actual
|
||||
|
||||
Runtime settings can specify `home_assistant.outdoor_entity_id` and `outdoor_assist_enabled`. Outdoor temperature never replaces the zone room temperature. It is only used to make active setpoints/fan speed slightly more assertive in extreme weather.
|
||||
|
||||
`home_assistant.sensor_stale_after_seconds` controls the maximum accepted age of Home Assistant temperature samples. The default is 300 seconds (5 minutes) and the backend clamps the value to 30-86400 seconds. The Web UI exposes this value in minutes under **Home Assistant / Sensors**. Existing zones keep compatibility with the historical `zone.sensor_stale_after_seconds` field: the old hidden/default value of 300 seconds inherits the global HA setting, while a non-default value supplied through the zone API remains a per-zone override.
|
||||
|
||||
For trusted local Home Assistant servers with self-signed/invalid HTTPS certificates, set:
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.2",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
+14
-12
@@ -611,16 +611,18 @@
|
||||
"zones.noMeasurement": "No measurement",
|
||||
"zones.localThermostatOffDescriptionTimed": "This zone is locally off. Automation takes control again in {time}.",
|
||||
"zones.localThermostatResuming": "Timer elapsed — automation is taking control…",
|
||||
"zones.resumeNow": "Resume now"
|
||||
},
|
||||
"zones.controlOwner": "Control owner",
|
||||
"zones.ownerAutomation": "Automation",
|
||||
"zones.ownerLocalThermostat": "Local thermostat",
|
||||
"zones.ownerExternal": "Remote / external control",
|
||||
"zones.ownerHaDirect": "HA — direct control",
|
||||
"zones.ownerWebDirect": "Web — direct control",
|
||||
"zones.ownerGlobalOff": "Globally off",
|
||||
"zones.ownerSince": "since",
|
||||
"zones.ownerResume": "until",
|
||||
"zones.lockoutUntil": "lockout until"
|
||||
"zones.resumeNow": "Resume now",
|
||||
"zones.controlOwner": "Control",
|
||||
"zones.ownerAutomation": "Automation",
|
||||
"zones.ownerLocalThermostat": "Local thermostat",
|
||||
"zones.ownerExternal": "Remote / external control",
|
||||
"zones.ownerHaDirect": "HA — direct control",
|
||||
"zones.ownerWebDirect": "Web — direct control",
|
||||
"zones.ownerGlobalOff": "Globally off",
|
||||
"zones.ownerSince": "since",
|
||||
"zones.ownerResume": "until",
|
||||
"zones.lockoutUntil": "lockout until",
|
||||
"settings.haSensorStaleAfterMinutes": "Maximum HA sensor reading age (min)",
|
||||
"settings.haSensorStaleAfterHint": "If the HA sensor is not updated within this time, the reading is treated as stale. The thermostat falls back to the GREE sensor when available."
|
||||
}
|
||||
}
|
||||
|
||||
+14
-12
@@ -611,16 +611,18 @@
|
||||
"zones.noMeasurement": "Brak pomiaru",
|
||||
"zones.localThermostatOffDescriptionTimed": "Strefa jest lokalnie wyłączona. Automatyka przejmie sterowanie za {time}.",
|
||||
"zones.localThermostatResuming": "Czas minął — automatyka przejmuje sterowanie…",
|
||||
"zones.resumeNow": "Wznów teraz"
|
||||
},
|
||||
"zones.controlOwner": "Właściciel sterowania",
|
||||
"zones.ownerAutomation": "Automatyka",
|
||||
"zones.ownerLocalThermostat": "Termostat lokalny",
|
||||
"zones.ownerExternal": "Pilot / sterowanie zewnętrzne",
|
||||
"zones.ownerHaDirect": "HA — sterowanie bezpośrednie",
|
||||
"zones.ownerWebDirect": "Web — sterowanie bezpośrednie",
|
||||
"zones.ownerGlobalOff": "Globalnie wyłączone",
|
||||
"zones.ownerSince": "od",
|
||||
"zones.ownerResume": "do",
|
||||
"zones.lockoutUntil": "blokada do"
|
||||
"zones.resumeNow": "Wznów teraz",
|
||||
"zones.controlOwner": "Sterowanie",
|
||||
"zones.ownerAutomation": "Automatyka",
|
||||
"zones.ownerLocalThermostat": "Termostat lokalny",
|
||||
"zones.ownerExternal": "Pilot / sterowanie zewnętrzne",
|
||||
"zones.ownerHaDirect": "HA — sterowanie bezpośrednie",
|
||||
"zones.ownerWebDirect": "Web — sterowanie bezpośrednie",
|
||||
"zones.ownerGlobalOff": "Globalnie wyłączone",
|
||||
"zones.ownerSince": "od",
|
||||
"zones.ownerResume": "do",
|
||||
"zones.lockoutUntil": "blokada do",
|
||||
"settings.haSensorStaleAfterMinutes": "Maksymalny wiek odczytu sensora HA (min)",
|
||||
"settings.haSensorStaleAfterHint": "Jeśli sensor HA nie zaktualizuje się przez ten czas, odczyt zostanie uznany za nieaktualny. Termostat użyje czujnika GREE jako awaryjnego źródła, jeśli jest dostępny."
|
||||
}
|
||||
}
|
||||
|
||||
+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,
|
||||
|
||||
@@ -1065,6 +1065,7 @@ function renderHomeAssistantSettings() {
|
||||
form.ha_token.placeholder = app.settings.home_assistant?.token_configured ? tr('settings.haTokenSaved') : tr('settings.haLongLivedToken');
|
||||
form.ha_entity_id.value = app.settings.home_assistant?.default_entity_id || '';
|
||||
form.ha_outdoor_entity_id.value = app.settings.home_assistant?.outdoor_entity_id || '';
|
||||
form.ha_sensor_stale_after_minutes.value = String(Math.max(1, Math.round(Number(app.settings.home_assistant?.sensor_stale_after_seconds || 300) / 60)));
|
||||
form.ha_allow_invalid_tls.checked = !!app.settings.home_assistant?.allow_invalid_tls;
|
||||
form.outdoor_assist_enabled.checked = !!app.settings.outdoor_assist_enabled;
|
||||
renderSensorAliases();
|
||||
@@ -2067,6 +2068,7 @@ function currentSettingsBody() {
|
||||
token: '',
|
||||
default_entity_id: ha.default_entity_id || '',
|
||||
outdoor_entity_id: ha.outdoor_entity_id || '',
|
||||
sensor_stale_after_seconds: Number(ha.sensor_stale_after_seconds || 300),
|
||||
allow_invalid_tls: !!ha.allow_invalid_tls,
|
||||
sensor_aliases: {...(ha.sensor_aliases || {})},
|
||||
},
|
||||
@@ -2127,6 +2129,7 @@ function homeAssistantSettingsBodyFromForm(form) {
|
||||
token: raw.ha_token,
|
||||
default_entity_id: raw.ha_entity_id,
|
||||
outdoor_entity_id: raw.ha_outdoor_entity_id,
|
||||
sensor_stale_after_seconds: Math.max(60, Math.min(86400, Math.round(Number(raw.ha_sensor_stale_after_minutes || 5) * 60))),
|
||||
allow_invalid_tls: form.ha_allow_invalid_tls.checked,
|
||||
sensor_aliases: {...(app.sensorAliases || {})},
|
||||
};
|
||||
|
||||
@@ -187,6 +187,8 @@
|
||||
<label class="wide"><span data-i18n="settings.haLongLivedToken">Long-Lived Access Token</span><input type="password" name="ha_token" autocomplete="new-password" data-i18n-placeholder="settings.haTokenKeep" placeholder="Leave empty to keep the saved token"></label>
|
||||
<label class="wide"><span data-i18n="settings.defaultEntity">Default room entity_id</span><input name="ha_entity_id" placeholder="sensor.living_room_temperature"></label>
|
||||
<label class="wide"><span data-i18n="settings.outdoorEntity">Outdoor temperature entity_id</span><input name="ha_outdoor_entity_id" placeholder="sensor.outdoor_temperature"></label>
|
||||
<label class="wide"><span data-i18n="settings.haSensorStaleAfterMinutes">Maximum HA sensor reading age (min)</span><input type="number" name="ha_sensor_stale_after_minutes" min="1" max="1440" step="1" value="5"></label>
|
||||
<p class="field-note wide" data-i18n="settings.haSensorStaleAfterHint">If the HA sensor is not updated within this time, the reading is treated as stale. The thermostat falls back to the GREE sensor when available.</p>
|
||||
<label class="check wide"><input type="checkbox" name="outdoor_assist_enabled"> <span data-i18n="settings.outdoorAssist">Use outdoor temperature as smart-control assist</span></label>
|
||||
<label class="check wide"><input type="checkbox" name="ha_allow_invalid_tls"> <span data-i18n="settings.allowInvalidTls">Allow invalid/self-signed HTTPS certificate</span></label>
|
||||
<p class="field-note wide warning-note" data-i18n="settings.allowInvalidTlsHint">Use only for a trusted local Home Assistant server, for example https://192.168.50.25.</p>
|
||||
|
||||
@@ -831,6 +831,7 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
|
||||
|
||||
/* Quick thermostat: target control gets its own row, readings no longer collide with it. */
|
||||
#dashboardZones { grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr)); }
|
||||
#dashboardZones, #dashboardDevices { align-items:start; }
|
||||
.quick-thermostat-control { min-width:0; }
|
||||
.quick-thermostat-control .thermostat-main {
|
||||
grid-template-columns:minmax(0,1fr) minmax(0,1fr);
|
||||
@@ -851,6 +852,8 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
|
||||
.quick-thermostat-control .zone-enable-toggle { flex:0 0 auto; }
|
||||
.quick-thermostat-control .zone-state-line { gap:8px; font-size:10px; }
|
||||
.quick-thermostat-control .zone-state-line span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.quick-thermostat-control .control-owner-line { align-items:flex-start; flex-wrap:wrap; }
|
||||
.quick-thermostat-control .control-owner-line span { overflow:visible; text-overflow:clip; white-space:normal; }
|
||||
|
||||
/* Smaller Node-RED/simulator-like plan blocks with explicit group context. */
|
||||
#controlPlan.automation-plan-grid { grid-template-columns:repeat(auto-fit,minmax(min(100%,210px),1fr)); gap:8px; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'gree-controller-v080-control-ownership';
|
||||
const CACHE = 'gree-controller-v082-ui-i18n-ha-freshness';
|
||||
const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
|
||||
const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/';
|
||||
const ASSETS = [path('/'), path('/styles.css'), path('/app.js'), path('/theme-init.js'), path('/favicon.svg'), path('/manifest.webmanifest'), path('/lang/index.json'), path('/lang/en.json')];
|
||||
|
||||
Reference in New Issue
Block a user