This commit is contained in:
Mateusz Gruszczyński
2026-08-25 09:43:28 +02:00
parent fd26d6877f
commit 440c7c3a8d
15 changed files with 118 additions and 55 deletions
+11 -2
View File
@@ -1,8 +1,8 @@
# GREE Controller v0.6.5 - build and validation report # GREE Controller v0.6.6 - build and validation report
## Scope ## Scope
Version 0.6.5 includes the 0.6.4 changes described below plus the whole-house thermostat semantics, master power control, Home Assistant house controls and LAN response-time reporting documented in this report. Version 0.6.6 includes the 0.6.5 changes described below plus synchronized whole-house activation: selecting a global operating mode or work profile now re-enables master power and powers on all enabled units. The global power panel is also placed above the house climate controls.
## Implemented changes ## Implemented changes
@@ -130,3 +130,12 @@ Validation performed in the build environment:
- Home Assistant now exposes controller-level **Thermostat mode** and **Work profile** select entities plus an **All air conditioners** master-power switch, backed by dedicated restricted HA endpoints. - Home Assistant now exposes controller-level **Thermostat mode** and **Work profile** select entities plus an **All air conditioners** master-power switch, backed by dedicated restricted HA endpoints.
- Device state stores the latest successful GREE communication round-trip time. **LAN network -> Devices** displays it in milliseconds next to the IP address and protocol. - Device state stores the latest successful GREE communication round-trip time. **LAN network -> Devices** displays it in milliseconds next to the IP address and protocol.
- PWA cache key advanced to `gree-controller-v065`. - PWA cache key advanced to `gree-controller-v065`.
## 0.6.6 synchronized whole-house activation
- The global power panel is displayed above **House climate mode**.
- Selecting whole-house Cooling or Heating clears a previous global power-off and powers on all enabled units.
- Selecting Auto, Comfort, Sleep or Away as a whole-house profile does the same.
- House mode `off` / **Do not control** remains non-invasive and does not alter master power.
- The behavior is implemented in the shared backend endpoints, so Home Assistant house-mode/profile controls follow the same rules.
Generated
+1 -1
View File
@@ -633,7 +633,7 @@ dependencies = [
[[package]] [[package]]
name = "gree-controller" name = "gree-controller"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"aes", "aes",
"aes-gcm", "aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "gree-controller" name = "gree-controller"
version = "0.6.5" version = "0.6.6"
edition = "2021" edition = "2021"
authors = ["GREE Controller contributors"] authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support" description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+14 -14
View File
@@ -1,19 +1,19 @@
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
c6ea33a11a287f159c6e4c5f19967d8599146692cd576cfe6b8ccacddadd558a ./BUILD_REPORT.md c5c9d9dad288bd9ec4695db7e4c1003ddd39a6cdf70073362dd84c7a87da57f8 ./BUILD_REPORT.md
c9673a32218f054f3d3d3b7f89cba3f0aa75f7078558e43ed3adff064e65d209 ./Cargo.lock dae36dbeeb9f26dce98eefc232c47b2fcbd5b75c9b896943990fa65b642c3638 ./Cargo.lock
735f566fe24949e8a4fae1bebd42d12e91a91cb99a36b114803f0cf0b93212a6 ./Cargo.toml 2a20c8523878f420c0f3d4df542b60220d7b645da8ed2acdc71d7328d721013e ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
90fe7b3c18f2ce0d244b8134b73841d3b416d33f7c13d3da107517a124c58d7e ./README.md 0e702e55efa6445d959c03c54dff71b1e56317b6fa9de28c52ef34a38c65e9fc ./README.md
f2c8eb28fedb26bb8f5636e1805e7a782cc472b87d882f6752d520f16216a248 ./build.rs f2c8eb28fedb26bb8f5636e1805e7a782cc472b87d882f6752d520f16216a248 ./build.rs
6e0e5eb103c4acdaeb04c224fdd49e7ec051c667732d131439a165ac1739b713 ./docs/API.md 1aeca9e23d3a4fcbe3214acafdc2a33cdffa0ad46efb5ead4477a2ebc3a4286c ./docs/API.md
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md 234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md 7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md
10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md 10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md
19e6532f21c59b93c77863e8a8ac3a7d9abaf869f18e30401b7d0975b404b06e ./docs/PROJECT_SPEC.md 19e6532f21c59b93c77863e8a8ac3a7d9abaf869f18e30401b7d0975b404b06e ./docs/PROJECT_SPEC.md
7748b1230029439a405139750b2e0607b8e463bfd56ad86982394105069697c5 ./docs/REVERSE_PROXY.md 7748b1230029439a405139750b2e0607b8e463bfd56ad86982394105069697c5 ./docs/REVERSE_PROXY.md
28f1403d84b01b37bebb72548aa39a75bb14bdbb611a43bfa761319957cd6f39 ./docs/SECURITY.md 28f1403d84b01b37bebb72548aa39a75bb14bdbb611a43bfa761319957cd6f39 ./docs/SECURITY.md
248570ceff0e020c9d81e8b9e1622ce33f3350f51a04371a9f4fc3f8fa13f62c ./home-assistant/README.md 8942e83eabe4edc9728594236b61e3c2be39b8bb46b66c19c4eff7ec7fe80716 ./home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
001215412d77abd393f34c414ff6d0ebbaf419f140d76037b7f114a7955df6c3 ./home-assistant/custom_components/gree_controller/api.py 001215412d77abd393f34c414ff6d0ebbaf419f140d76037b7f114a7955df6c3 ./home-assistant/custom_components/gree_controller/api.py
abcca32b090e5a5d02574a3a340e97321f9879ae6c94ef12eb1f42d967eae2d5 ./home-assistant/custom_components/gree_controller/climate.py abcca32b090e5a5d02574a3a340e97321f9879ae6c94ef12eb1f42d967eae2d5 ./home-assistant/custom_components/gree_controller/climate.py
@@ -23,7 +23,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./home-assista
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py 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/icon.png
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
8666eebd03276d6649104dcf538b79073f4f50a51b1440bcbad0a23c09b0f78b ./home-assistant/custom_components/gree_controller/manifest.json 52c67fb50b8e9fec5c60af20df6db3385cc40afe08e57b9ea1776ab5fefdaf70 ./home-assistant/custom_components/gree_controller/manifest.json
d0206766d76f70ea1c75a618e41bf1e8e8c0429f42ee97f40daac1ea12f16658 ./home-assistant/custom_components/gree_controller/number.py d0206766d76f70ea1c75a618e41bf1e8e8c0429f42ee97f40daac1ea12f16658 ./home-assistant/custom_components/gree_controller/number.py
459233ccefa7827fc1c6887146d8a69e01149a497635fd5e1802c220f418eb28 ./home-assistant/custom_components/gree_controller/select.py 459233ccefa7827fc1c6887146d8a69e01149a497635fd5e1802c220f418eb28 ./home-assistant/custom_components/gree_controller/select.py
ec35191e27a018a72cb341914e2abd7fecd25f6b641ea25bebb9e809dd8ecaff ./home-assistant/custom_components/gree_controller/sensor.py ec35191e27a018a72cb341914e2abd7fecd25f6b641ea25bebb9e809dd8ecaff ./home-assistant/custom_components/gree_controller/sensor.py
@@ -32,8 +32,8 @@ ec35191e27a018a72cb341914e2abd7fecd25f6b641ea25bebb9e809dd8ecaff ./home-assista
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json 13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json 4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
997c7a91017b5d51b3e44016d21252b37d302f95ba0fb5802f3d8fd116687d6f ./lang/en.json 3b147813b7ad9d44c99ec4b2c062de77f914ae04538abc6cfaa9e5451fc6a83c ./lang/en.json
33f6935ad1fe72ea0e6893e230ff95565388410fb0fbfe974b34610d789c5d2b ./lang/pl.json c301de561c5999031b7a09101a67ab5c7ee49118fe181ad152741c6c2ea01272 ./lang/pl.json
028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py 028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
@@ -46,7 +46,7 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
f8d1349236819fe0f0253df5269353245f0cacf5b29614ac79c2317c16943636 ./src/api.rs f3d38b84a241cf0dd14ae8f8db7f1ef972734c1f2acaeb8d0bd7997c6e8f05bd ./src/api.rs
32b1d0a7ac2060fd18fe7f1ca1d110ee45beaf67e81514d24d6bde3d36301387 ./src/config.rs 32b1d0a7ac2060fd18fe7f1ca1d110ee45beaf67e81514d24d6bde3d36301387 ./src/config.rs
a89bc49e359715109149130a10ac60abd26bfd2bc18c5ee5142e01696d9f9cdd ./src/db.rs a89bc49e359715109149130a10ac60abd26bfd2bc18c5ee5142e01696d9f9cdd ./src/db.rs
8ec14c01036c73f28cdb330d696f0d3573198e7ea4d03ba6bf9cc1d34ec6c7a8 ./src/engine.rs 8ec14c01036c73f28cdb330d696f0d3573198e7ea4d03ba6bf9cc1d34ec6c7a8 ./src/engine.rs
@@ -62,10 +62,10 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol
d1cba5e55c77d44c75a9c9897ae76c327fd96bdf9c01ad27e34322b689348487 ./src/queries.rs d1cba5e55c77d44c75a9c9897ae76c327fd96bdf9c01ad27e34322b689348487 ./src/queries.rs
f74ac398c6f2e64ebb965900330324f3ec7403e70339638a408229f29fbd07ae ./src/state.rs f74ac398c6f2e64ebb965900330324f3ec7403e70339638a408229f29fbd07ae ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
53b94be090889a8f709b16b0f88e9e9c732427151de3cb740c1f16144dec37b4 ./web/app.js 8a5471859605115780e1a1cc3d07345698c846c642a9a604977ae2ca73cec0ec ./web/app.js
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
59e85721942f533243d12491e201dbfedef859dc752b7f0c7c37405705832d2d ./web/index.html 3d47ede16e8652f2b36223ca26271ca302d8a1c62030f18a47b461da9c1bf47f ./web/index.html
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
a9643913bf5ceba3b0940fff583350f5bb853e7d0421296a9661a4e3e256bde1 ./web/styles.css 3796167c55146cf549a6df8b7057072ba4731ed667f9691a35a21832139169cd ./web/styles.css
e804540ecc928c3f29a8e71fdae68a6e0a36eb0c0698b1ca22cf8f42a888d61c ./web/sw.js aba62a4b8535e59fd78650142e96c7234ef744e02f28178923b40b5188cb0265 ./web/sw.js
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
+2 -2
View File
@@ -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. 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.6.5**. Current version: **0.6.6**.
## Highlights ## Highlights
@@ -46,7 +46,7 @@ See [`BUILD_REPORT.md`](BUILD_REPORT.md) for package validation details and [`do
On Debian, Ubuntu or an LXC container: On Debian, Ubuntu or an LXC container:
```bash ```bash
unzip gree-controller-v0.6.5.zip unzip gree-controller-v0.6.6.zip
cd gree-controller cd gree-controller
chmod +x scripts/*.sh chmod +x scripts/*.sh
./scripts/dev.sh ./scripts/dev.sh
+1 -1
View File
@@ -79,7 +79,7 @@ curl -X POST "$BASE/api/house/preset" -H "$AUTH" -H 'Content-Type: application/j
-d '{"preset":"sleep"}' -d '{"preset":"sleep"}'
``` ```
House modes: `cool`, `heat`, `off` (`off` = no house-level thermostat control). House presets: `auto`, `comfort`, `sleep`, `away`. A non-`auto` house preset creates temporary per-zone overrides that expire at each zone's next schedule boundary. `POST /api/house/power` controls a separate whole-house master power state. With master power off, zones and controller automations cannot restart units; the selected house thermostat mode is preserved. This is independent from house mode `off`, which only means no house-level thermostat control and allows direct/manual device operation. House modes: `cool`, `heat`, `off` (`off` = no house-level thermostat control). House presets: `auto`, `comfort`, `sleep`, `away`. A non-`auto` house preset creates temporary per-zone overrides that expire at each zone's next schedule boundary. `POST /api/house/power` controls a separate whole-house master power state. With master power off, zones and controller automations cannot restart units; the selected house thermostat mode is preserved. This is independent from house mode `off`, which only means no house-level thermostat control and allows direct/manual device operation. Choosing whole-house `cool`/`heat` or any whole-house preset is treated as an explicit whole-house activation: master power is set back to on and all enabled units are powered on. Choosing house mode `off` does not change master power.
A zone stores separate profile temperatures for both seasons: A zone stores separate profile temperatures for both seasons:
+1
View File
@@ -132,6 +132,7 @@ The integration also exposes three controller-level entities on the **GREE Contr
- **Thermostat mode** (`select`) — Cooling, Heating or **Do not control**. Do not control pauses house-level thermostat commands but leaves direct device control and explicit per-zone Heat/Cool overrides untouched. - **Thermostat mode** (`select`) — Cooling, Heating or **Do not control**. Do not control pauses house-level thermostat commands but leaves direct device control and explicit per-zone Heat/Cool overrides untouched.
- **Work profile** (`select`) — Auto schedule, Comfort, Sleep or Away. If zones have mixed manual profiles, the select has no single current option until a whole-house profile is chosen again. - **Work profile** (`select`) — Auto schedule, Comfort, Sleep or Away. If zones have mixed manual profiles, the select has no single current option until a whole-house profile is chosen again.
- Choosing **Cooling/Heating** in the thermostat-mode select or any whole-house **Work profile** is treated as explicit whole-house activation: it switches **All air conditioners** back on. **Do not control** does not change master power.
- **All air conditioners** (`switch`) — separate master power. Turning it off powers every enabled unit down and prevents zones/controller automations from restarting them. Turning it on powers all enabled units on again without changing the selected thermostat mode or profile. - **All air conditioners** (`switch`) — separate master power. Turning it off powers every enabled unit down and prevents zones/controller automations from restarting them. Turning it on powers all enabled units on again without changing the selected thermostat mode or profile.
These controls use the Home Assistant integration token and the dedicated `/api/integrations/home-assistant/house/*` endpoints. These controls use the Home Assistant integration token and the dedicated `/api/integrations/home-assistant/house/*` endpoints.
@@ -1,7 +1,7 @@
{ {
"domain": "gree_controller", "domain": "gree_controller",
"name": "GREE Controller", "name": "GREE Controller",
"version": "0.6.5", "version": "0.6.6",
"config_flow": true, "config_flow": true,
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
+1 -1
View File
@@ -534,7 +534,7 @@
"house.noControl": "Do not control", "house.noControl": "Do not control",
"house.powerAllEyebrow": "Global power", "house.powerAllEyebrow": "Global power",
"house.powerAllTitle": "All air conditioners", "house.powerAllTitle": "All air conditioners",
"house.powerAllHint": "Separate master power for all enabled units. “Turn all off” prevents zones and automations from restarting them without changing the selected thermostat mode.", "house.powerAllHint": "Master power for all enabled units. “Turn all off” prevents them from restarting. Choosing whole-house Cooling/Heating or an Auto/Comfort/Sleep/Away profile automatically switches master power back on.",
"house.powerOnAll": "Turn all on", "house.powerOnAll": "Turn all on",
"house.powerOffAll": "Turn all off", "house.powerOffAll": "Turn all off",
"house.powerOnDone": "All air conditioners turned on", "house.powerOnDone": "All air conditioners turned on",
+1 -1
View File
@@ -534,7 +534,7 @@
"house.noControl": "Nie steruj", "house.noControl": "Nie steruj",
"house.powerAllEyebrow": "Zasilanie globalne", "house.powerAllEyebrow": "Zasilanie globalne",
"house.powerAllTitle": "Wszystkie klimatyzatory", "house.powerAllTitle": "Wszystkie klimatyzatory",
"house.powerAllHint": "Osobne nadrzędne zasilanie wszystkich aktywnych jednostek. „Wyłącz wszystkie” blokuje ponowne uruchamianie przez strefy i automatyzacje, ale nie zmienia wybranego trybu termostatu.", "house.powerAllHint": "Nadrzędne zasilanie wszystkich aktywnych jednostek. „Wyłącz wszystkie” blokuje ich ponowne uruchamianie. Globalny wybór Chłodzenia/Grzania albo profilu Auto/Komfort/Sen/Poza domem automatycznie ustawia „Włącz wszystkie”.",
"house.powerOnAll": "Włącz wszystkie", "house.powerOnAll": "Włącz wszystkie",
"house.powerOffAll": "Wyłącz wszystkie", "house.powerOffAll": "Wyłącz wszystkie",
"house.powerOnDone": "Włączono wszystkie klimatyzatory", "house.powerOnDone": "Włączono wszystkie klimatyzatory",
+69 -22
View File
@@ -594,16 +594,55 @@ async fn update_zone_control(State(state): State<AppState>, Path(id): Path<Strin
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct HouseControlPatch { mode: String } struct HouseControlPatch { mode: String }
async fn command_all_enabled_devices_power(state: &AppState, power: bool, source: &str) -> Result<Vec<Value>, AppError> {
let mut failed = Vec::new();
for device in state.db.list_devices()? {
if !device.enabled || device.power == power { continue; }
let command = DeviceCommand { power: Some(power), ..Default::default() };
if let Err(err) = engine::send_command(state, &device.id, command).await {
state.log("error", "house.power_all_error", &err.to_string(), json!({
"device_id": device.id,
"device_name": device.name,
"power": power,
"source": source,
}));
failed.push(json!({
"device_id": device.id,
"device_name": device.name,
"error": err.to_string(),
}));
}
}
Ok(failed)
}
async fn update_house_control(State(state): State<AppState>, Json(input): Json<HouseControlPatch>) -> Result<Json<Value>, AppError> { async fn update_house_control(State(state): State<AppState>, Json(input): Json<HouseControlPatch>) -> Result<Json<Value>, AppError> {
if !matches!(input.mode.as_str(), "cool" | "heat" | "off") { if !matches!(input.mode.as_str(), "cool" | "heat" | "off") {
return Err(AppError::BadRequest("house mode must be cool, heat or off".into())); return Err(AppError::BadRequest("house mode must be cool, heat or off".into()));
} }
let mode = input.mode;
let activate_all = mode != "off";
let payload = {
let mut settings = state.settings.write().await; let mut settings = state.settings.write().await;
settings.house_mode = input.mode; settings.house_mode = mode.clone();
// Choosing a real whole-house operating mode is an explicit request to run the
// house climate. It therefore clears a previous global power-off. "off" keeps
// its separate meaning: do not control, without changing master power.
if activate_all { settings.house_power_enabled = true; }
state.db.save_runtime_settings(&settings)?; state.db.save_runtime_settings(&settings)?;
let payload = public_settings(&settings); public_settings(&settings)
};
state.broadcast("settings.updated", payload.clone()); state.broadcast("settings.updated", payload.clone());
state.log("info", "house.mode", &format!("House mode set to {}", settings.house_mode), json!({"mode": settings.house_mode})); if activate_all {
let failed = command_all_enabled_devices_power(&state, true, "house_mode").await?;
if !failed.is_empty() {
state.log("warn", "house.mode_power_partial", "House mode enabled master power, but some devices could not be powered on", json!({
"mode": mode,
"failed": failed.len(),
}));
}
}
state.log("info", "house.mode", &format!("House mode set to {}", mode), json!({"mode": mode, "master_power_enabled": activate_all}));
Ok(Json(payload)) Ok(Json(payload))
} }
@@ -623,23 +662,7 @@ async fn update_house_power(State(state): State<AppState>, Json(input): Json<Hou
} }
} }
let mut failed = Vec::new(); let failed = command_all_enabled_devices_power(&state, input.power, "house_power").await?;
for device in state.db.list_devices()? {
if !device.enabled || device.power == input.power { continue; }
let command = DeviceCommand { power: Some(input.power), ..Default::default() };
if let Err(err) = engine::send_command(&state, &device.id, command).await {
state.log("error", "house.power_all_error", &err.to_string(), json!({
"device_id": device.id,
"device_name": device.name,
"power": input.power,
}));
failed.push(json!({
"device_id": device.id,
"device_name": device.name,
"error": err.to_string(),
}));
}
}
let devices = state.db.list_devices()?; let devices = state.db.list_devices()?;
let settings = state.settings.read().await; let settings = state.settings.read().await;
@@ -664,6 +687,17 @@ async fn update_house_preset(State(state): State<AppState>, Json(input): Json<Ho
if !matches!(input.preset.as_str(), "auto" | "comfort" | "sleep" | "away") { if !matches!(input.preset.as_str(), "auto" | "comfort" | "sleep" | "away") {
return Err(AppError::BadRequest("house preset must be auto, comfort, sleep or away".into())); return Err(AppError::BadRequest("house preset must be auto, comfort, sleep or away".into()));
} }
// A whole-house profile is also an explicit whole-house activation. This mirrors
// selecting cooling/heating and makes the separate master-power control intuitive.
let settings_payload = {
let mut settings = state.settings.write().await;
settings.house_power_enabled = true;
state.db.save_runtime_settings(&settings)?;
public_settings(&settings)
};
state.broadcast("settings.updated", settings_payload.clone());
let schedules = state.db.list_schedules()?; let schedules = state.db.list_schedules()?;
let mut zones = state.db.list_zones()?; let mut zones = state.db.list_zones()?;
for zone in &mut zones { for zone in &mut zones {
@@ -680,8 +714,21 @@ async fn update_house_preset(State(state): State<AppState>, Json(input): Json<Ho
state.db.save_zone(zone)?; state.db.save_zone(zone)?;
state.broadcast("zone.updated", serde_json::to_value(&*zone)?); state.broadcast("zone.updated", serde_json::to_value(&*zone)?);
} }
state.log("info", "house.preset", &format!("House preset set to {}", input.preset), json!({"preset": input.preset}));
Ok(Json(json!({"preset": input.preset, "zones": zones}))) let failed = command_all_enabled_devices_power(&state, true, "house_preset").await?;
let devices = state.db.list_devices()?;
state.log("info", "house.preset", &format!("House preset set to {}", input.preset), json!({
"preset": input.preset,
"master_power_enabled": true,
"failed": failed.len(),
}));
Ok(Json(json!({
"preset": input.preset,
"zones": zones,
"devices": devices,
"settings": settings_payload,
"failed": failed,
})))
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
+7 -1
View File
@@ -1439,7 +1439,13 @@ document.addEventListener('click', async event => {
return; return;
} }
if (action === 'house-preset') { if (action === 'house-preset') {
try { const result=await api('/api/house/preset',{method:'POST',body:{preset:button.dataset.value}}); app.zones=result.zones||app.zones; renderHouseClimate(); renderZones(); scheduleControlPlanLoad(); toast(tr('house.presetUpdated')); } try {
const result=await api('/api/house/preset',{method:'POST',body:{preset:button.dataset.value}});
app.settings=result.settings||app.settings; app.zones=result.zones||app.zones; app.devices=result.devices||app.devices;
renderAll(); scheduleControlPlanLoad();
const failed=Array.isArray(result.failed)?result.failed.length:0;
if(failed) toast(tr('house.powerPartial',{count:failed}),true); else toast(tr('house.presetUpdated'));
}
catch(error){ toast(error.message,true); } return; catch(error){ toast(error.message,true); } return;
} }
if (action === 'zone-temperature') { const zone=app.zones.find(v=>v.id===button.dataset.id); if(zone) { const base=Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint); queueZoneTemperature(zone, base+Number(button.dataset.delta)); } return; } if (action === 'zone-temperature') { const zone=app.zones.find(v=>v.id===button.dataset.id); if(zone) { const base=Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint); queueZoneTemperature(zone, base+Number(button.dataset.delta)); } return; }
+2 -2
View File
@@ -37,11 +37,11 @@
<div class="hero-temp" id="heroTemperature">--<small>°C</small></div> <div class="hero-temp" id="heroTemperature">--<small>°C</small></div>
</div> </div>
<div class="metrics" id="metrics"></div> <div class="metrics" id="metrics"></div>
<div class="panel house-climate" id="houseClimate"></div>
<div class="panel house-power-panel"> <div class="panel house-power-panel">
<div><span class="eyebrow" data-i18n="house.powerAllEyebrow">Global power</span><h3 data-i18n="house.powerAllTitle">All air conditioners</h3><p data-i18n="house.powerAllHint">Direct power control for all enabled units, independent from thermostat presets.</p></div> <div><span class="eyebrow" data-i18n="house.powerAllEyebrow">Global power</span><h3 data-i18n="house.powerAllTitle">All air conditioners</h3><p data-i18n="house.powerAllHint">Master power for all enabled units. Whole-house mode or profile selection automatically switches it back on.</p></div>
<div class="house-power-actions"><button id="housePowerOn" type="button" class="secondary" data-action="house-power" data-value="true" data-i18n="house.powerOnAll">Turn all on</button><button id="housePowerOff" type="button" class="secondary" data-action="house-power" data-value="false" data-i18n="house.powerOffAll">Turn all off</button></div> <div class="house-power-actions"><button id="housePowerOn" type="button" class="secondary" data-action="house-power" data-value="true" data-i18n="house.powerOnAll">Turn all on</button><button id="housePowerOff" type="button" class="secondary" data-action="house-power" data-value="false" data-i18n="house.powerOffAll">Turn all off</button></div>
</div> </div>
<div class="panel house-climate" id="houseClimate"></div>
<div class="section-heading"><div><span class="eyebrow" data-i18n="plan.eyebrow">Control plan</span><h2 data-i18n="plan.title">What happens next</h2></div></div> <div class="section-heading"><div><span class="eyebrow" data-i18n="plan.eyebrow">Control plan</span><h2 data-i18n="plan.title">What happens next</h2></div></div>
<div class="automation-plan-grid" id="controlPlan"></div> <div class="automation-plan-grid" id="controlPlan"></div>
<div class="section-heading"><div><span class="eyebrow" data-i18n="nav.zones">Zones</span><h2 data-i18n="dashboard.quickThermostats">Quick thermostats</h2></div></div> <div class="section-heading"><div><span class="eyebrow" data-i18n="nav.zones">Zones</span><h2 data-i18n="dashboard.quickThermostats">Quick thermostats</h2></div></div>
+3 -3
View File
@@ -298,7 +298,7 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; }
.mode-row, .fan-row, .preset-row { scrollbar-width: none; } .mode-row, .fan-row, .preset-row { scrollbar-width: none; }
.mode-row::-webkit-scrollbar, .fan-row::-webkit-scrollbar, .preset-row::-webkit-scrollbar { display: none; } .mode-row::-webkit-scrollbar, .fan-row::-webkit-scrollbar, .preset-row::-webkit-scrollbar { display: none; }
.house-climate { display: grid; gap: 16px; margin: -15px 0 12px; } .house-climate { display: grid; gap: 16px; margin: 0 0 12px; }
.house-climate-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; } .house-climate-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.house-climate-head h3 { margin: 2px 0 4px; font-size: 20px; } .house-climate-head h3 { margin: 2px 0 4px; font-size: 20px; }
.house-climate-head p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; } .house-climate-head p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; }
@@ -325,7 +325,7 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; }
.plan-loading { grid-column: 1/-1; padding: 18px; color: var(--muted); } .plan-loading { grid-column: 1/-1; padding: 18px; color: var(--muted); }
.house-mode-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .house-mode-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.house-mode-row button { min-height: 44px; color: var(--muted); background: var(--surface-muted); } .house-mode-row button { min-height: 44px; color: var(--muted); background: var(--surface-muted); }
.house-power-panel { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin: 0 0 34px; } .house-power-panel { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin: -15px 0 12px; }
.house-power-panel h3 { margin: 2px 0 4px; font-size: 18px; } .house-power-panel h3 { margin: 2px 0 4px; font-size: 18px; }
.house-power-panel p { margin: 0; max-width: 650px; color: var(--muted); font-size: 12px; line-height: 1.45; } .house-power-panel p { margin: 0; max-width: 650px; color: var(--muted); font-size: 12px; line-height: 1.45; }
.house-power-actions { display: flex; flex: 0 0 auto; gap: 8px; } .house-power-actions { display: flex; flex: 0 0 auto; gap: 8px; }
@@ -360,7 +360,7 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; }
.warning-note { color: var(--warning); } .warning-note { color: var(--warning); }
@media (max-width: 620px) { @media (max-width: 620px) {
.house-climate { margin-top: -20px; } .house-power-panel { margin-top: -20px; }
.house-climate-head { align-items: flex-start; } .house-climate-head { align-items: flex-start; }
.house-climate-head p { max-width: 220px; } .house-climate-head p { max-width: 220px; }
.house-power-panel { align-items: stretch; flex-direction: column; } .house-power-panel { align-items: stretch; flex-direction: column; }
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'gree-controller-v065'; const CACHE = 'gree-controller-v066';
const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, ''); const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/'; 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')]; 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')];