This commit is contained in:
Mateusz Gruszczyński
2026-08-24 16:47:23 +02:00
parent 66a5d6e5e9
commit 4ab878d587
15 changed files with 284 additions and 101 deletions
+7 -2
View File
@@ -1,11 +1,16 @@
# GREE Controller v0.5.4 - build and validation report # GREE Controller v0.5.5 - build and validation report
## Scope ## Scope
Version 0.5.4 builds on 0.5.3 with Home Assistant sensor-read fixes, full zone climate mode control, GREE capability discovery and native Sleep support during night mode. Version 0.5.5 builds on 0.5.4 with dedicated Night mode and Home Assistant/Sensors pages, compact unit-feature controls, and room-temperature refresh for disabled zones.
## Implemented changes ## Implemented changes
- Night-mode settings moved out of application Settings into a dedicated **Night mode** page.
- Home Assistant connection, sensor aliases and integration access tokens moved to a dedicated **Home Assistant / Sensors** page.
- Settings now contains application/technical settings only.
- Unit-feature controls on device cards are smaller, centered and visually aligned with the other compact device controls.
- Disabled thermostat zones continue refreshing and showing room temperature while control commands remain disabled.
- Home Assistant sensor aliases are now presentation-only end-to-end: backend reads resolve aliases back to the original `entity_id`, settings are canonicalized, and zone/outdoor polling records the real entity ID. - Home Assistant sensor aliases are now presentation-only end-to-end: backend reads resolve aliases back to the original `entity_id`, settings are canonicalized, and zone/outdoor polling records the real entity ID.
- Outdoor temperature now falls back to the median outdoor reading from online GREE units when the configured Home Assistant outdoor sensor is unavailable. - Outdoor temperature now falls back to the median outdoor reading from online GREE units when the configured Home Assistant outdoor sensor is unavailable.
- GREE optional capabilities are discovered from status properties (`Lig`, `Quiet`, `Tur`, `Air`, `Blo`, `Health`, `SwhSlp`) and persisted with each device. - GREE optional capabilities are discovered from status properties (`Lig`, `Quiet`, `Tur`, `Air`, `Blo`, `Health`, `SwhSlp`) and persisted with each device.
Generated
+1 -1
View File
@@ -633,7 +633,7 @@ dependencies = [
[[package]] [[package]]
name = "gree-controller" name = "gree-controller"
version = "0.5.4" version = "0.5.5"
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.5.4" version = "0.5.5"
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,17 +1,17 @@
e027a3bf37a7917e249ebffb980d43b5e18c70e5931113271975cbeda81a88bd ./.env.example e027a3bf37a7917e249ebffb980d43b5e18c70e5931113271975cbeda81a88bd ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
7e0bbddcccea0726bdbf7c93a0fa3a892c1d60279b6da8d992a55a7183522b01 ./BUILD_REPORT.md 52c2c492893ce42e68dac763533a5070170c21b18d67b302a4e1380a4d4cc4ab ./BUILD_REPORT.md
ce1d4a2d05be90ffecc39af85cdf5383b08484c3a27482e02f387030075db17e ./Cargo.lock 4202057467ffdc3f53ef250c3e7852a929be788bb0af03fbd2366a57ce42b79e ./Cargo.lock
d9e0c071ee11f26bd8e804e6cff976339982ebca45dad2f03614e147c35df327 ./Cargo.toml 9edc867d716d9d8ce2c54cdf17e16628f9c2b338ee36f88e0bb0fcb4f7884b57 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
e7b879527c9d1eb7d6bbd054813e709026777e4a79e62b4c8912d2d5aa5e7ff3 ./README.md 44675b0d5d9e6e89dd0bae8eff3ab76a190a3b1bdd6507e7402957bbd92cac36 ./README.md
a4fa9bfee9735ed8ed95ea31456e0cce503d82502ae3f550108ffca51b0f0c3d ./build.rs a4fa9bfee9735ed8ed95ea31456e0cce503d82502ae3f550108ffca51b0f0c3d ./build.rs
f00f7475d690694c837e29a618c7737199515bd7a64a5a3be5f44291fe73df0c ./docs/API.md 674f8cf1ab9f1a49188e20b8e3977f1e2b7e9b8ef975315266730c44a55bbb48 ./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
47e18e6b1723f1f6eb2516be6d83bbebd31b8155d9d3cdfccc0973177f3d8441 ./docs/PROJECT_SPEC.md 47e18e6b1723f1f6eb2516be6d83bbebd31b8155d9d3cdfccc0973177f3d8441 ./docs/PROJECT_SPEC.md
cf889651576a0860c3dc383c255aa011306f82d6287533617c523bcc41caca21 ./home-assistant/README.md 925ffa324b1b2cfad7ca40354e8255548f70627f95c4bb345b957d3f7e53694c ./home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
b8e7c0dd722bfac1d1d8d5c2a4b5ff6b2d6fa89525b9ff5c121ee1d09623a7fd ./home-assistant/custom_components/gree_controller/api.py b8e7c0dd722bfac1d1d8d5c2a4b5ff6b2d6fa89525b9ff5c121ee1d09623a7fd ./home-assistant/custom_components/gree_controller/api.py
b0314469ddb7c69ee4a96c677815af30cb97713e93911a05597f2fdc3d10f489 ./home-assistant/custom_components/gree_controller/climate.py b0314469ddb7c69ee4a96c677815af30cb97713e93911a05597f2fdc3d10f489 ./home-assistant/custom_components/gree_controller/climate.py
@@ -19,7 +19,7 @@ b0314469ddb7c69ee4a96c677815af30cb97713e93911a05597f2fdc3d10f489 ./home-assista
071f721b75af2ed68812c9be3a62b7ae63771e56e567a569f59ca976f8ff42f1 ./home-assistant/custom_components/gree_controller/const.py 071f721b75af2ed68812c9be3a62b7ae63771e56e567a569f59ca976f8ff42f1 ./home-assistant/custom_components/gree_controller/const.py
8dd51322798cad28d4d19938fe459aa2a4d3b64b075532149f3c8b62049a6c8b ./home-assistant/custom_components/gree_controller/coordinator.py 8dd51322798cad28d4d19938fe459aa2a4d3b64b075532149f3c8b62049a6c8b ./home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
b3ac1d74c2f8decdc0dcb9689d3a62bea803a9533164929c09d97922c08c605a ./home-assistant/custom_components/gree_controller/manifest.json f79cc14c65131fa86a5bf41d55f49b60a3aa71a6d15de98027de830e3b11b2a1 ./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
5c914c2faa18a9cb9eb442dc111c9416f89a630578f820beb021d5381aa1b2d0 ./home-assistant/custom_components/gree_controller/sensor.py 5c914c2faa18a9cb9eb442dc111c9416f89a630578f820beb021d5381aa1b2d0 ./home-assistant/custom_components/gree_controller/sensor.py
7c23374d1c82e4b361089386ff89265322b80def0f4db9d3fd2c039867033105 ./home-assistant/custom_components/gree_controller/switch.py 7c23374d1c82e4b361089386ff89265322b80def0f4db9d3fd2c039867033105 ./home-assistant/custom_components/gree_controller/switch.py
@@ -27,8 +27,8 @@ d0206766d76f70ea1c75a618e41bf1e8e8c0429f42ee97f40daac1ea12f16658 ./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
e824a016cff1fa0411ab456b74f3cfe597546072c9e334ef92de69f86a868f48 ./lang/en.json eb5370fbee1a8edc11a536ff0e68dba2152767734feb2976fef3cb7cc8c885f4 ./lang/en.json
93d26be77deb820dbce29484db8f2ef6c56b5b05a451c4e9d57013e43aa88395 ./lang/pl.json 9e4f43dd809f2d92aca8ce6813e843dbf7e2f38adcfd9cdae584a94e22197fa3 ./lang/pl.json
028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py 028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
3640e34a1ed5a93be3e96610848b16b8e748d5ca95975ac7e95d38cbf32695a8 ./scripts/common.sh 3640e34a1ed5a93be3e96610848b16b8e748d5ca95975ac7e95d38cbf32695a8 ./scripts/common.sh
@@ -44,7 +44,7 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda
ca63498714f3e154fd98c0c2157ab355bf36fc2208d7b3f82c201e153749fb04 ./src/api.rs ca63498714f3e154fd98c0c2157ab355bf36fc2208d7b3f82c201e153749fb04 ./src/api.rs
31ef3ddbc096e580e9212486683e4db86998322ee87190c61b0cab5ae6bc8fc9 ./src/config.rs 31ef3ddbc096e580e9212486683e4db86998322ee87190c61b0cab5ae6bc8fc9 ./src/config.rs
a89bc49e359715109149130a10ac60abd26bfd2bc18c5ee5142e01696d9f9cdd ./src/db.rs a89bc49e359715109149130a10ac60abd26bfd2bc18c5ee5142e01696d9f9cdd ./src/db.rs
c2ac8e8b4acc2bd15e9c41af5a003b7ced4424346617946e387ec57ce9cb2583 ./src/engine.rs 9dfea316b39da6ded85cb9c7ec6f6a61e0cd9a51f40469145782d5766c9cf7a4 ./src/engine.rs
ae3b496749a3fd723b243d9bea92e5d76249f52814c80359ad9bac53abacb074 ./src/error.rs ae3b496749a3fd723b243d9bea92e5d76249f52814c80359ad9bac53abacb074 ./src/error.rs
081f6909e95839c7da8c40b866af9de237e0540f3e65e1ce739ce70a6fa94846 ./src/home_assistant.rs 081f6909e95839c7da8c40b866af9de237e0540f3e65e1ce739ce70a6fa94846 ./src/home_assistant.rs
190b0a33431539676e5dd7796698077f16c179d42eae4501ca96a91bf797cbf8 ./src/influxdb.rs 190b0a33431539676e5dd7796698077f16c179d42eae4501ca96a91bf797cbf8 ./src/influxdb.rs
@@ -56,9 +56,9 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol
d1cba5e55c77d44c75a9c9897ae76c327fd96bdf9c01ad27e34322b689348487 ./src/queries.rs d1cba5e55c77d44c75a9c9897ae76c327fd96bdf9c01ad27e34322b689348487 ./src/queries.rs
e3c02b232bd9d632ca1d3b6ad69a7cc61dc2e51954765d9e963d713d135f7dca ./src/state.rs e3c02b232bd9d632ca1d3b6ad69a7cc61dc2e51954765d9e963d713d135f7dca ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
e82ff244991b968a3dd69f117ce9feeeee6e21c00a257d1d42273283b2d7ff99 ./web/app.js 17342e3aa40a55285f5d056502b232e6aeed41a64ff3e1fdf94c0d8278385ffe ./web/app.js
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
3a7b734e25955f148c5ef0518f6b4abbb04c0a1e660cd58270292fba3bee149f ./web/index.html 1474d31bf8266f6b6dc2dc47225644394259dc6e0afc8e09f0eaddfa93db1d82 ./web/index.html
c0e628423a82c5037b6d650aad5e8befe30945492c8cb0a5d94433db7b58c77b ./web/manifest.webmanifest c0e628423a82c5037b6d650aad5e8befe30945492c8cb0a5d94433db7b58c77b ./web/manifest.webmanifest
2d20ba68af2b67b1c57104f0fbb67a0144e2f6aeb12ed8fc9b616a32baa89a3e ./web/styles.css b8a3cecbde31f9aa4ed3afb5755c62cf98ad90a12faf0a616835170ee889e252 ./web/styles.css
f75381d719eb299730a85406eb86a58f56fd2ec9bd566bcb3cb84ae6a2d38f87 ./web/sw.js f8bda0083c138828a037469d3db189559190d4b893bcff764a6f9bfd9030b265 ./web/sw.js
+6 -4
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.5.4**. Current version: **0.5.5**.
## 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.5.4.zip unzip gree-controller-v0.5.5.zip
cd gree-controller cd gree-controller
chmod +x scripts/*.sh chmod +x scripts/*.sh
./scripts/dev.sh ./scripts/dev.sh
@@ -177,6 +177,8 @@ Use `--skip-tests` with `install.sh` or `update.sh` only when you explicitly wan
Settings changed from the web panel are stored in SQLite. Explicit history/debug/Influx environment variables override their persisted values at process startup. Supplying an Influx URL enables the archive unless `GREE_CONTROLLER_INFLUX_ENABLED=false` is explicitly set. `GREE_CONTROLLER_APP_TOKEN` is loaded at process startup. Settings changed from the web panel are stored in SQLite. Explicit history/debug/Influx environment variables override their persisted values at process startup. Supplying an Influx URL enables the archive unless `GREE_CONTROLLER_INFLUX_ENABLED=false` is explicitly set. `GREE_CONTROLLER_APP_TOKEN` is loaded at process startup.
Web UI organization: **Settings** contains controller/application and technical storage/debug options, while **Night mode** and **Home Assistant / Sensors** are separate pages under More. Disabled zones continue reporting room temperature even though thermostat control is stopped.
## Climate history ## Climate history
History is collected independently for physical GREE devices, control zones, and configured Home Assistant temperature sensors. Device samples continue to use the long-standing `readings` table, so upgrading does not require waiting for a new zone-history table to fill. When rich `zone_readings` are not available yet, the API automatically reconstructs a compatible zone timeline from existing device readings. History is collected independently for physical GREE devices, control zones, and configured Home Assistant temperature sensors. Device samples continue to use the long-standing `readings` table, so upgrading does not require waiting for a new zone-history table to fill. When rich `zone_readings` are not available yet, the API automatically reconstructs a compatible zone timeline from existing device readings.
@@ -234,9 +236,9 @@ There are two independent HA directions:
1. **HA as an optional sensor source**: the Rust controller can read a selected HA sensor and use it for a zone. 1. **HA as an optional sensor source**: the Rust controller can read a selected HA sensor and use it for a zone.
2. **HA as a client of GREE Controller**: the included custom integration creates `climate` entities whose commands are sent to this Rust application. 2. **HA as a client of GREE Controller**: the included custom integration creates `climate` entities whose commands are sent to this Rust application.
For the HA client connection, open **Settings -> Home Assistant integration access** in GREE Controller and press **Create new token**. The secret is shown once. Paste that token into the Home Assistant `GREE Controller` integration together with the controller URL. Managed HA tokens are stored as SHA-256 hashes and are scoped to the dedicated HA device/control-plan/zone-control API; they cannot change controller settings or manage other tokens. For the HA client connection, open **More -> Home Assistant / Sensors** in GREE Controller and press **Create new token**. The secret is shown once. Paste that token into the Home Assistant `GREE Controller` integration together with the controller URL. Managed HA tokens are stored as SHA-256 hashes and are scoped to the dedicated HA device/control-plan/zone-control API; they cannot change controller settings or manage other tokens.
The outbound HA sensor client also supports local HTTPS endpoints such as `https://10.87.65.2`. If the server uses a self-signed, expired or hostname-mismatched certificate, enable **Settings -> Allow invalid/self-signed HTTPS certificate**. This disables certificate/hostname validation only for the controller's outbound Home Assistant sensor client and should only be used on a trusted LAN. The outbound HA sensor client also supports local HTTPS endpoints such as `https://192.168.50.25`. If the server uses a self-signed, expired or hostname-mismatched certificate, enable **Home Assistant / Sensors -> Allow invalid/self-signed HTTPS certificate**. This disables certificate/hostname validation only for the controller's outbound Home Assistant sensor client and should only be used on a trusted LAN.
The second option is designed to replace the built-in/default GREE integration without changing automation/dashboard references. The second option is designed to replace the built-in/default GREE integration without changing automation/dashboard references.
+1 -1
View File
@@ -10,7 +10,7 @@ BASE='http://127.0.0.1:8787'
## Home Assistant access tokens ## Home Assistant access tokens
Create and revoke integration tokens from the controller Web UI under **Settings -> Home Assistant integration access**. The clear-text secret is returned only once and the SQLite database stores only its SHA-256 hash. Create and revoke integration tokens from the controller Web UI under **More -> Home Assistant / Sensors**. The clear-text secret is returned only once and the SQLite database stores only its SHA-256 hash.
Administrator endpoints: Administrator endpoints:
+1 -1
View File
@@ -27,7 +27,7 @@ Copy the directory into your HA configuration:
/config/custom_components/gree_controller/ /config/custom_components/gree_controller/
``` ```
Before adding the integration, open the standalone controller Web UI and go to **Settings -> Home Assistant integration access -> Create new token**. Copy the generated secret; it is shown only once. Before adding the integration, open the standalone controller Web UI and go to **More -> Home Assistant / Sensors -> Create new token**. Copy the generated secret; it is shown only once.
Restart Home Assistant, then open **Settings -> Devices & services -> Add integration -> GREE Controller** and enter only: Restart Home Assistant, then open **Settings -> Devices & services -> Add integration -> GREE Controller** and enter only:
@@ -1,7 +1,7 @@
{ {
"domain": "gree_controller", "domain": "gree_controller",
"name": "GREE Controller", "name": "GREE Controller",
"version": "0.5.4", "version": "0.5.5",
"config_flow": true, "config_flow": true,
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
+9 -1
View File
@@ -490,6 +490,14 @@
"devices.xfan": "X-FAN", "devices.xfan": "X-FAN",
"devices.health": "Health", "devices.health": "Health",
"devices.air": "Air", "devices.air": "Air",
"devices.sleep": "Sleep" "devices.sleep": "Sleep",
"nav.nightMode": "Night mode",
"nav.homeAssistant": "Home Assistant / Sensors",
"night.eyebrow": "Comfort",
"night.description": "Configure quieter thermostat behavior for sleeping hours without mixing it with application settings.",
"night.saveHint": "Save night-mode changes.",
"homeAssistant.eyebrow": "Integration",
"homeAssistant.description": "Connection, temperature sensors, friendly aliases and access tokens for the Home Assistant integration.",
"homeAssistant.saveHint": "Save Home Assistant and sensor changes."
} }
} }
+9 -1
View File
@@ -490,6 +490,14 @@
"devices.xfan": "X-FAN", "devices.xfan": "X-FAN",
"devices.health": "Health", "devices.health": "Health",
"devices.air": "Air", "devices.air": "Air",
"devices.sleep": "Sleep" "devices.sleep": "Sleep",
"nav.nightMode": "Tryb nocny",
"nav.homeAssistant": "Home Assistant / Czujniki",
"night.eyebrow": "Komfort",
"night.description": "Ustaw cichszą pracę termostatu w godzinach nocnych bez mieszania tych opcji z ustawieniami aplikacji.",
"night.saveHint": "Zapisz zmiany trybu nocnego.",
"homeAssistant.eyebrow": "Integracja",
"homeAssistant.description": "Połączenie, czujniki temperatury, przyjazne aliasy i tokeny dostępu dla integracji Home Assistant.",
"homeAssistant.saveHint": "Zapisz ustawienia Home Assistant i czujników."
} }
} }
+11 -2
View File
@@ -354,8 +354,6 @@ async fn control_zones(state: &AppState) -> Result<()> {
let night_active = night_mode_active(&settings.night_mode, Local::now().time()); let night_active = night_mode_active(&settings.night_mode, Local::now().time());
for mut zone in state.db.list_zones()? { for mut zone in state.db.list_zones()? {
if !zone.enabled { continue; }
if zone.manual_override_until.map(|until| until <= Utc::now()).unwrap_or(false) { if zone.manual_override_until.map(|until| until <= Utc::now()).unwrap_or(false) {
zone.manual_preset = None; zone.manual_preset = None;
zone.manual_setpoint = None; zone.manual_setpoint = None;
@@ -419,6 +417,17 @@ async fn control_zones(state: &AppState) -> Result<()> {
})); }));
} }
// Disabled zones still refresh and publish their room temperature. Disabling a
// thermostat stops control actions, but it must not make the room sensor disappear
// from the dashboard or zone view.
if !zone.enabled {
zone.demand = false;
record_zone_history(state, &zone, outdoor_temperature, settings.poll_interval_seconds);
state.db.save_zone(&zone)?;
state.broadcast("zone.updated", serde_json::to_value(&zone)?);
continue;
}
// Global Off is the only normal path that intentionally powers units down. // Global Off is the only normal path that intentionally powers units down.
if effective_mode == "off" { if effective_mode == "off" {
zone.active_preset = "off".into(); zone.active_preset = "off".into();
+158 -38
View File
@@ -213,6 +213,8 @@ function renderAll() {
renderAccessTokens(); renderAccessTokens();
fillSelects(); fillSelects();
renderSettings(); renderSettings();
renderNightSettings();
renderHomeAssistantSettings();
renderDebugOverlay(); renderDebugOverlay();
} }
@@ -565,6 +567,7 @@ function zoneCard(zone, detailed = true) {
const sensorDetails = zone.sensor_source === 'device' const sensorDetails = zone.sensor_source === 'device'
? `${tr('zones.greeTemp')}: ${fmtTemp(zone.device_temperature ?? device?.current_temperature)}` ? `${tr('zones.greeTemp')}: ${fmtTemp(zone.device_temperature ?? device?.current_temperature)}`
: `${tr('zones.greeTemp')}: ${fmtTemp(zone.device_temperature)} · ${tr('zones.externalTemp')}: ${fmtTemp(zone.external_temperature)} · ${tr('zones.usedSource')}: ${zoneControlSourceLabel(zone.control_temperature_source)}`; : `${tr('zones.greeTemp')}: ${fmtTemp(zone.device_temperature)} · ${tr('zones.externalTemp')}: ${fmtTemp(zone.external_temperature)} · ${tr('zones.usedSource')}: ${zoneControlSourceLabel(zone.control_temperature_source)}`;
const roomTemperature = zone.current_temperature ?? zone.device_temperature ?? device?.current_temperature;
const target = Number(zone.effective_setpoint ?? zone.setpoint); const target = Number(zone.effective_setpoint ?? zone.setpoint);
const manual = zone.manual_preset || 'auto'; const manual = zone.manual_preset || 'auto';
const mode = zone.inherit_house_mode ? 'house' : zone.mode; const mode = zone.inherit_house_mode ? 'house' : zone.mode;
@@ -574,7 +577,7 @@ function zoneCard(zone, detailed = true) {
: `<button type="button" class="zone-enable-toggle ${zone.enabled ? 'active' : ''}" data-action="zone-enabled" data-id="${esc(zone.id)}" data-value="${zone.enabled ? 'false' : 'true'}" aria-label="${esc(tr(zone.enabled ? 'zones.disable' : 'zones.enable'))}"><span>${zone.enabled ? '✓' : '○'}</span>${esc(state)}</button>`; : `<button type="button" class="zone-enable-toggle ${zone.enabled ? 'active' : ''}" data-action="zone-enabled" data-id="${esc(zone.id)}" data-value="${zone.enabled ? 'false' : 'true'}" aria-label="${esc(tr(zone.enabled ? 'zones.disable' : 'zones.enable'))}"><span>${zone.enabled ? '✓' : '○'}</span>${esc(state)}</button>`;
return `<article class="list-card zone-thermostat ${zone.demand ? 'demanding' : ''} ${zone.enabled ? '' : 'zone-disabled'}"> return `<article class="list-card zone-thermostat ${zone.demand ? 'demanding' : ''} ${zone.enabled ? '' : 'zone-disabled'}">
<div class="list-card-head"><div><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(zone.active_preset))}</p></div>${enabledControl}</div> <div class="list-card-head"><div><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(zone.active_preset))}</p></div>${enabledControl}</div>
<div class="thermostat-main"><div><small>${esc(tr('zones.measurement'))}</small><strong>${fmtTemp(zone.current_temperature)}</strong></div><div class="temperature-control compact"><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="-0.5"></button><div class="target-temp compact">${Number.isFinite(target)?target.toFixed(1):'--'}<small>°C</small></div><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="0.5">+</button></div><div><small>${esc(tr('zones.deviceTarget'))}</small><strong>${fmtTemp(zone.device_setpoint)}</strong></div></div> <div class="thermostat-main"><div><small>${esc(tr('zones.measurement'))}</small><strong>${fmtTemp(roomTemperature)}</strong></div><div class="temperature-control compact"><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="-0.5"></button><div class="target-temp compact">${Number.isFinite(target)?target.toFixed(1):'--'}<small>°C</small></div><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="0.5">+</button></div><div><small>${esc(tr('zones.deviceTarget'))}</small><strong>${fmtTemp(zone.device_setpoint)}</strong></div></div>
<div class="preset-row"> <div class="preset-row">
${['auto','comfort','sleep','away'].map(preset=>`<button class="${manual===preset?'active':''}" data-action="zone-preset" data-id="${esc(zone.id)}" data-value="${preset}">${esc(preset==='sleep'?tr('zones.sleepNow'):zonePresetLabel(preset))}</button>`).join('')} ${['auto','comfort','sleep','away'].map(preset=>`<button class="${manual===preset?'active':''}" data-action="zone-preset" data-id="${esc(zone.id)}" data-value="${preset}">${esc(preset==='sleep'?tr('zones.sleepNow'):zonePresetLabel(preset))}</button>`).join('')}
</div> </div>
@@ -663,6 +666,7 @@ function renderLogRetention() {
function renderSettings() { function renderSettings() {
if (!app.settings) return; if (!app.settings) return;
const form = $('#settingsForm'); const form = $('#settingsForm');
if (!form) return;
form.controller_id.value = app.settings.controller_id || ''; form.controller_id.value = app.settings.controller_id || '';
form.poll_interval_seconds.value = app.settings.poll_interval_seconds || 15; form.poll_interval_seconds.value = app.settings.poll_interval_seconds || 15;
form.zone_interval_seconds.value = app.settings.zone_interval_seconds || 5; form.zone_interval_seconds.value = app.settings.zone_interval_seconds || 5;
@@ -673,12 +677,6 @@ function renderSettings() {
form.event_log_retention_days.value = app.settings.event_log_retention_days || 30; form.event_log_retention_days.value = app.settings.event_log_retention_days || 30;
form.history_compaction_enabled.checked = app.settings.history_compaction_enabled !== false; form.history_compaction_enabled.checked = app.settings.history_compaction_enabled !== false;
form.suppress_device_beep.checked = !!app.settings.suppress_device_beep; form.suppress_device_beep.checked = !!app.settings.suppress_device_beep;
form.night_mode_enabled.checked = !!app.settings.night_mode?.enabled;
form.night_mode_start.value = app.settings.night_mode?.start_time || '22:00';
form.night_mode_end.value = app.settings.night_mode?.end_time || '06:00';
form.night_mode_max_fan_speed.value = String(app.settings.night_mode?.max_fan_speed || 1);
form.night_mode_force_quiet.checked = app.settings.night_mode?.force_quiet !== false;
form.night_mode_native_sleep.checked = app.settings.night_mode?.use_native_sleep !== false;
form.influx_enabled.checked = !!app.settings.influxdb?.enabled; form.influx_enabled.checked = !!app.settings.influxdb?.enabled;
form.influx_version.value = String(app.settings.influxdb?.version || '2'); form.influx_version.value = String(app.settings.influxdb?.version || '2');
form.influx_threshold_days.value = app.settings.influxdb?.history_threshold_days || 30; form.influx_threshold_days.value = app.settings.influxdb?.history_threshold_days || 30;
@@ -693,6 +691,27 @@ function renderSettings() {
form.influx_token.placeholder = app.settings.influxdb?.token_configured ? tr('settings.secretSaved') : tr('settings.secretKeep'); form.influx_token.placeholder = app.settings.influxdb?.token_configured ? tr('settings.secretSaved') : tr('settings.secretKeep');
form.debug_overlay_enabled.checked = !!app.settings.debug?.overlay_enabled; form.debug_overlay_enabled.checked = !!app.settings.debug?.overlay_enabled;
form.debug_gree_frames.checked = !!app.settings.debug?.gree_frames; form.debug_gree_frames.checked = !!app.settings.debug?.gree_frames;
updateInfluxFields();
renderLogRetention();
$('#systemInfo').innerHTML = `<h3>${esc(tr('settings.systemState'))}</h3><div>${esc(tr('settings.version'))}: <strong>${esc(app.system.version || '—')}</strong></div><div>${esc(tr('settings.uptime'))}: <strong>${esc(formatDuration(app.system.uptime_seconds || 0))}</strong></div><div>${esc(tr('settings.apiAuth'))}: <strong>${esc(app.system.auth_required ? tr('settings.enabled') : tr('settings.disabled'))}</strong></div>`;
}
function renderNightSettings() {
if (!app.settings) return;
const form = $('#nightModeForm');
if (!form) return;
form.night_mode_enabled.checked = !!app.settings.night_mode?.enabled;
form.night_mode_start.value = app.settings.night_mode?.start_time || '22:00';
form.night_mode_end.value = app.settings.night_mode?.end_time || '06:00';
form.night_mode_max_fan_speed.value = String(app.settings.night_mode?.max_fan_speed || 1);
form.night_mode_force_quiet.checked = app.settings.night_mode?.force_quiet !== false;
form.night_mode_native_sleep.checked = app.settings.night_mode?.use_native_sleep !== false;
}
function renderHomeAssistantSettings() {
if (!app.settings) return;
const form = $('#homeAssistantForm');
if (!form) return;
form.ha_url.value = app.settings.home_assistant?.url || ''; form.ha_url.value = app.settings.home_assistant?.url || '';
form.ha_token.value = ''; form.ha_token.value = '';
form.ha_token.placeholder = app.settings.home_assistant?.token_configured ? tr('settings.haTokenSaved') : tr('settings.haLongLivedToken'); form.ha_token.placeholder = app.settings.home_assistant?.token_configured ? tr('settings.haTokenSaved') : tr('settings.haLongLivedToken');
@@ -700,10 +719,8 @@ function renderSettings() {
form.ha_outdoor_entity_id.value = app.settings.home_assistant?.outdoor_entity_id || ''; form.ha_outdoor_entity_id.value = app.settings.home_assistant?.outdoor_entity_id || '';
form.ha_allow_invalid_tls.checked = !!app.settings.home_assistant?.allow_invalid_tls; form.ha_allow_invalid_tls.checked = !!app.settings.home_assistant?.allow_invalid_tls;
form.outdoor_assist_enabled.checked = !!app.settings.outdoor_assist_enabled; form.outdoor_assist_enabled.checked = !!app.settings.outdoor_assist_enabled;
updateInfluxFields();
renderSensorAliases(); renderSensorAliases();
renderLogRetention(); renderAccessTokens();
$('#systemInfo').innerHTML = `<h3>${esc(tr('settings.systemState'))}</h3><div>${esc(tr('settings.version'))}: <strong>${esc(app.system.version || '—')}</strong></div><div>${esc(tr('settings.uptime'))}: <strong>${esc(formatDuration(app.system.uptime_seconds || 0))}</strong></div><div>${esc(tr('settings.apiAuth'))}: <strong>${esc(app.system.auth_required ? tr('settings.enabled') : tr('settings.disabled'))}</strong></div>`;
} }
function updateInfluxFields() { function updateInfluxFields() {
@@ -747,7 +764,7 @@ function formatDuration(seconds) {
return `${days ? `${days}d ` : ''}${hours}h ${minutes}m`; return `${days ? `${days}d ` : ''}${hours}h ${minutes}m`;
} }
const VIEW_ROUTES = {dashboard:'/dashboard', devices:'/devices', zones:'/zones', schedules:'/schedules', automations:'/automations', simulation:'/simulation', settings:'/settings', logs:'/events'}; const VIEW_ROUTES = {dashboard:'/dashboard', devices:'/devices', zones:'/zones', schedules:'/schedules', automations:'/automations', simulation:'/simulation', night:'/night-mode', homeassistant:'/home-assistant', settings:'/settings', logs:'/events'};
const HISTORY_TABS = ['overview','zones','devices','sensors','custom']; const HISTORY_TABS = ['overview','zones','devices','sensors','custom'];
function currentHistoryPath() { function currentHistoryPath() {
@@ -772,7 +789,7 @@ function updateBrowserUrl(path, replace=false) {
function showView(name, {push=true, scroll=true}={}) { function showView(name, {push=true, scroll=true}={}) {
app.currentView = name; app.currentView = name;
$$('.view').forEach(view => view.classList.toggle('active', view.dataset.view === name)); $$('.view').forEach(view => view.classList.toggle('active', view.dataset.view === name));
$$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['schedules','automations','simulation','settings','logs'].includes(name)))); $$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['schedules','automations','simulation','night','homeassistant','settings','logs'].includes(name))));
if (push) updateBrowserUrl(name === 'history' ? currentHistoryPath() : (VIEW_ROUTES[name] || '/dashboard')); if (push) updateBrowserUrl(name === 'history' ? currentHistoryPath() : (VIEW_ROUTES[name] || '/dashboard'));
if (scroll) window.scrollTo({top: 0, behavior: 'smooth'}); if (scroll) window.scrollTo({top: 0, behavior: 'smooth'});
if (name === 'history') { renderHistoryNavigation(); loadHistory(); } if (name === 'history') { renderHistoryNavigation(); loadHistory(); }
@@ -1302,7 +1319,7 @@ function connectWebSocket() {
else if (message.event === 'device.deleted') { app.devices=app.devices.filter(v=>v.id!==data.id); renderAll(); scheduleControlPlanLoad(); } else if (message.event === 'device.deleted') { app.devices=app.devices.filter(v=>v.id!==data.id); renderAll(); scheduleControlPlanLoad(); }
else if (message.event === 'devices.discovered') { (data.devices||[]).forEach(updateDevice); renderAll(); scheduleControlPlanLoad(); } else if (message.event === 'devices.discovered') { (data.devices||[]).forEach(updateDevice); renderAll(); scheduleControlPlanLoad(); }
else if (message.event === 'zone.updated') { const i=app.zones.findIndex(v=>v.id===data.id); if(i>=0) app.zones[i]=data; else app.zones.push(data); renderSummary(); renderHouseClimate(); renderZones(); scheduleControlPlanLoad(); } else if (message.event === 'zone.updated') { const i=app.zones.findIndex(v=>v.id===data.id); if(i>=0) app.zones[i]=data; else app.zones.push(data); renderSummary(); renderHouseClimate(); renderZones(); scheduleControlPlanLoad(); }
else if (message.event === 'settings.updated') { app.settings=data; app.sensorAliases={...(app.settings?.home_assistant?.sensor_aliases||{})}; renderSettings(); renderHouseClimate(); renderDebugOverlay(); if(app.settings?.debug?.overlay_enabled) loadDebugBacklog(); scheduleControlPlanLoad(); } else if (message.event === 'settings.updated') { app.settings=data; app.sensorAliases={...(app.settings?.home_assistant?.sensor_aliases||{})}; renderSettings(); renderNightSettings(); renderHomeAssistantSettings(); renderHouseClimate(); renderDebugOverlay(); if(app.settings?.debug?.overlay_enabled) loadDebugBacklog(); scheduleControlPlanLoad(); }
else if (message.event === 'debug.settings') { app.settings = app.settings || {}; app.settings.debug = data; renderSettings(); renderDebugOverlay(); if(data.overlay_enabled) loadDebugBacklog(); } else if (message.event === 'debug.settings') { app.settings = app.settings || {}; app.settings.debug = data; renderSettings(); renderDebugOverlay(); if(data.overlay_enabled) loadDebugBacklog(); }
else if (message.event === 'outdoor.updated') { app.outdoorTemperature=Number.isFinite(Number(data.temperature))?Number(data.temperature):null; renderHouseClimate(); scheduleControlPlanLoad(); } else if (message.event === 'outdoor.updated') { app.outdoorTemperature=Number.isFinite(Number(data.temperature))?Number(data.temperature):null; renderHouseClimate(); scheduleControlPlanLoad(); }
else if (message.event === 'gree.frame') { if(app.settings?.debug?.overlay_enabled) debugLine('GREE', `${data.direction || '?'} ${data.protocol_version || ''}`, data.device_name || data.device_id || data.target || '', message.timestamp, data.payload); } else if (message.event === 'gree.frame') { if(app.settings?.debug?.overlay_enabled) debugLine('GREE', `${data.direction || '?'} ${data.protocol_version || ''}`, data.device_name || data.device_id || data.target || '', message.timestamp, data.payload); }
@@ -1463,38 +1480,141 @@ $('#automationForm').addEventListener('submit', async event => {
catch(error){toast(error.message,true);} catch(error){toast(error.message,true);}
}); });
function settingsBodyFromForm(form) { function currentSettingsBody() {
const raw=Object.fromEntries(new FormData(form)); const settings = app.settings || {};
const influx = settings.influxdb || {};
const ha = settings.home_assistant || {};
return { return {
controller_id:raw.controller_id, simulator_enabled:form.simulator_enabled.checked, controller_id: settings.controller_id || 'gree-controller',
poll_interval_seconds:Number(raw.poll_interval_seconds), zone_interval_seconds:Number(raw.zone_interval_seconds), simulator_enabled: !!settings.simulator_enabled,
discovery_timeout_ms:Number(raw.discovery_timeout_ms), discovery_broadcast:raw.discovery_broadcast, poll_interval_seconds: Number(settings.poll_interval_seconds || 15),
house_mode:app.settings?.house_mode||'cool', control_strategy:'setpoint', outdoor_assist_enabled:form.outdoor_assist_enabled.checked, zone_interval_seconds: Number(settings.zone_interval_seconds || 5),
history_retention_days:Number(raw.history_retention_days), history_compaction_enabled:form.history_compaction_enabled.checked, discovery_timeout_ms: Number(settings.discovery_timeout_ms || 3000),
event_log_retention_days:Number(raw.event_log_retention_days), discovery_broadcast: settings.discovery_broadcast || '255.255.255.255:7000',
suppress_device_beep:form.suppress_device_beep.checked, house_mode: settings.house_mode || 'cool',
night_mode:{ control_strategy: 'setpoint',
enabled:form.night_mode_enabled.checked,start_time:raw.night_mode_start,end_time:raw.night_mode_end, outdoor_assist_enabled: !!settings.outdoor_assist_enabled,
max_fan_speed:Number(raw.night_mode_max_fan_speed),force_quiet:form.night_mode_force_quiet.checked, history_retention_days: Number(settings.history_retention_days || 30),
use_native_sleep:form.night_mode_native_sleep.checked, history_compaction_enabled: settings.history_compaction_enabled !== false,
event_log_retention_days: Number(settings.event_log_retention_days || 30),
suppress_device_beep: !!settings.suppress_device_beep,
night_mode: {
enabled: !!settings.night_mode?.enabled,
start_time: settings.night_mode?.start_time || '22:00',
end_time: settings.night_mode?.end_time || '06:00',
max_fan_speed: Number(settings.night_mode?.max_fan_speed || 1),
force_quiet: settings.night_mode?.force_quiet !== false,
use_native_sleep: settings.night_mode?.use_native_sleep !== false,
}, },
influxdb:{ influxdb: {
enabled:form.influx_enabled.checked, version:raw.influx_version, url:raw.influx_url, enabled: !!influx.enabled,
database:raw.influx_database, username:raw.influx_username, password:raw.influx_password, version: String(influx.version || '2'),
org:raw.influx_org, bucket:raw.influx_bucket, token:raw.influx_token, url: influx.url || '',
history_threshold_days:Number(raw.influx_threshold_days), database: influx.database || 'gree_controller',
username: influx.username || '',
password: '',
org: influx.org || '',
bucket: influx.bucket || 'gree_controller',
token: '',
history_threshold_days: Number(influx.history_threshold_days || 30),
},
debug: {
overlay_enabled: !!settings.debug?.overlay_enabled,
gree_frames: !!settings.debug?.gree_frames,
},
home_assistant: {
url: ha.url || '',
token: '',
default_entity_id: ha.default_entity_id || '',
outdoor_entity_id: ha.outdoor_entity_id || '',
allow_invalid_tls: !!ha.allow_invalid_tls,
sensor_aliases: {...(ha.sensor_aliases || {})},
}, },
debug:{overlay_enabled:form.debug_overlay_enabled.checked, gree_frames:form.debug_gree_frames.checked},
home_assistant:{url:raw.ha_url,token:raw.ha_token,default_entity_id:raw.ha_entity_id,outdoor_entity_id:raw.ha_outdoor_entity_id,allow_invalid_tls:form.ha_allow_invalid_tls.checked,sensor_aliases:{...(app.sensorAliases||{})}},
}; };
} }
async function saveSettingsForm(form, notify=true) { function settingsBodyFromForm(form) {
app.settings=await api('/api/settings',{method:'PUT',body:settingsBodyFromForm(form)}); app.sensorAliases={...(app.settings?.home_assistant?.sensor_aliases||{})}; renderSettings(); renderHouseClimate(); renderDebugOverlay(); scheduleControlPlanLoad(); if(app.settings?.debug?.overlay_enabled) loadDebugBacklog(); if(notify) toast(tr('common.saved')); return app.settings; const raw = Object.fromEntries(new FormData(form));
const body = currentSettingsBody();
body.controller_id = raw.controller_id;
body.simulator_enabled = form.simulator_enabled.checked;
body.poll_interval_seconds = Number(raw.poll_interval_seconds);
body.zone_interval_seconds = Number(raw.zone_interval_seconds);
body.discovery_timeout_ms = Number(raw.discovery_timeout_ms);
body.discovery_broadcast = raw.discovery_broadcast;
body.history_retention_days = Number(raw.history_retention_days);
body.history_compaction_enabled = form.history_compaction_enabled.checked;
body.event_log_retention_days = Number(raw.event_log_retention_days);
body.suppress_device_beep = form.suppress_device_beep.checked;
body.influxdb = {
enabled: form.influx_enabled.checked, version: raw.influx_version, url: raw.influx_url,
database: raw.influx_database, username: raw.influx_username, password: raw.influx_password,
org: raw.influx_org, bucket: raw.influx_bucket, token: raw.influx_token,
history_threshold_days: Number(raw.influx_threshold_days),
};
body.debug = {overlay_enabled: form.debug_overlay_enabled.checked, gree_frames: form.debug_gree_frames.checked};
return body;
}
function nightSettingsBodyFromForm(form) {
const raw = Object.fromEntries(new FormData(form));
const body = currentSettingsBody();
body.night_mode = {
enabled: form.night_mode_enabled.checked,
start_time: raw.night_mode_start,
end_time: raw.night_mode_end,
max_fan_speed: Number(raw.night_mode_max_fan_speed),
force_quiet: form.night_mode_force_quiet.checked,
use_native_sleep: form.night_mode_native_sleep.checked,
};
return body;
}
function homeAssistantSettingsBodyFromForm(form) {
const raw = Object.fromEntries(new FormData(form));
const body = currentSettingsBody();
body.outdoor_assist_enabled = form.outdoor_assist_enabled.checked;
body.home_assistant = {
url: raw.ha_url,
token: raw.ha_token,
default_entity_id: raw.ha_entity_id,
outdoor_entity_id: raw.ha_outdoor_entity_id,
allow_invalid_tls: form.ha_allow_invalid_tls.checked,
sensor_aliases: {...(app.sensorAliases || {})},
};
return body;
}
async function saveRuntimeSettings(body, notify = true) {
app.settings = await api('/api/settings', {method:'PUT', body});
app.sensorAliases = {...(app.settings?.home_assistant?.sensor_aliases || {})};
renderSettings();
renderNightSettings();
renderHomeAssistantSettings();
renderHouseClimate();
renderDebugOverlay();
scheduleControlPlanLoad();
if (app.settings?.debug?.overlay_enabled) loadDebugBacklog();
if (notify) toast(tr('common.saved'));
return app.settings;
} }
$('#settingsForm').addEventListener('submit', async event => { $('#settingsForm').addEventListener('submit', async event => {
event.preventDefault(); try { await saveSettingsForm(event.currentTarget, true); } catch(error){toast(error.message,true);} event.preventDefault();
try { await saveRuntimeSettings(settingsBodyFromForm(event.currentTarget), true); }
catch(error) { toast(error.message, true); }
});
$('#nightModeForm')?.addEventListener('submit', async event => {
event.preventDefault();
try { await saveRuntimeSettings(nightSettingsBodyFromForm(event.currentTarget), true); }
catch(error) { toast(error.message, true); }
});
$('#homeAssistantForm')?.addEventListener('submit', async event => {
event.preventDefault();
try { await saveRuntimeSettings(homeAssistantSettingsBodyFromForm(event.currentTarget), true); }
catch(error) { toast(error.message, true); }
}); });
$('#addSensorAlias')?.addEventListener('click', () => { $('#addSensorAlias')?.addEventListener('click', () => {
@@ -1555,9 +1675,9 @@ $('#copyAccessToken').addEventListener('click', async () => {
}); });
$('#haTest').addEventListener('click', async () => { $('#haTest').addEventListener('click', async () => {
const form=$('#settingsForm'); const form=$('#homeAssistantForm');
try { try {
await saveSettingsForm(form, false); await saveRuntimeSettings(homeAssistantSettingsBodyFromForm(form), false);
const entity = form.ha_entity_id.value || form.ha_outdoor_entity_id.value || null; const entity = form.ha_entity_id.value || form.ha_outdoor_entity_id.value || null;
const result=await api('/api/integrations/home-assistant/test',{method:'POST',body:{entity_id:entity}}); const result=await api('/api/integrations/home-assistant/test',{method:'POST',body:{entity_id:entity}});
toast(tr('toast.haTemperature',{temperature:result.temperature_c.toFixed(1)})); toast(tr('toast.haTemperature',{temperature:result.temperature_c.toFixed(1)}));
+55 -33
View File
@@ -135,6 +135,56 @@
<div id="historyCharts" class="history-charts"></div> <div id="historyCharts" class="history-charts"></div>
</section> </section>
<section class="view" data-view="night">
<div class="section-heading"><div><span class="eyebrow" data-i18n="night.eyebrow">Comfort</span><h1 data-i18n="nav.nightMode">Night mode</h1></div></div>
<p class="lead" data-i18n="night.description">Configure quieter thermostat behavior for sleeping hours without mixing it with application settings.</p>
<form class="settings-form standalone-settings-form" id="nightModeForm">
<section class="panel settings-block">
<div class="settings-block-head"><div><h3 data-i18n="settings.nightMode">Night mode</h3><p data-i18n="settings.nightModeHint">During selected hours the thermostat limits fan speed and can request Quiet on supported units.</p></div></div>
<div class="settings-grid">
<label class="check wide"><input type="checkbox" name="night_mode_enabled"> <span data-i18n="settings.nightModeEnabled">Enable night mode</span></label>
<label><span data-i18n="settings.nightStart">Start</span><input type="time" name="night_mode_start" value="22:00"></label>
<label><span data-i18n="settings.nightEnd">End</span><input type="time" name="night_mode_end" value="06:00"></label>
<label><span data-i18n="settings.nightMaxFan">Maximum fan speed</span><select name="night_mode_max_fan_speed"><option value="1" data-i18n="fan.low">Low</option><option value="2" data-i18n="fan.mediumLow">Medium-low</option><option value="3" data-i18n="fan.medium">Medium</option><option value="4" data-i18n="fan.mediumHigh">Medium-high</option><option value="5" data-i18n="fan.high">High</option></select></label>
<label class="check"><input type="checkbox" name="night_mode_force_quiet"> <span data-i18n="settings.nightForceQuiet">Use Quiet when supported</span></label>
<label class="check wide"><input type="checkbox" name="night_mode_native_sleep"> <span data-i18n="settings.nightNativeSleep">Use native Sleep when supported</span></label>
</div>
</section>
<div class="settings-save-bar"><span data-i18n="night.saveHint">Save night-mode changes.</span><button class="primary" type="submit" data-i18n="actions.save">Save</button></div>
</form>
</section>
<section class="view" data-view="homeassistant">
<div class="section-heading"><div><span class="eyebrow" data-i18n="homeAssistant.eyebrow">Integration</span><h1 data-i18n="nav.homeAssistant">Home Assistant / Sensors</h1></div></div>
<p class="lead" data-i18n="homeAssistant.description">Connection, temperature sensors, friendly aliases and access tokens for the Home Assistant integration.</p>
<form class="settings-form standalone-settings-form" id="homeAssistantForm">
<section class="panel settings-block">
<div class="settings-block-head"><div><h3 data-i18n="settings.haSensorInput">Home Assistant sensor input</h3><p data-i18n="settings.haSensorInputHint">Optional. Used when a room zone reads an external Home Assistant temperature sensor.</p></div></div>
<div class="settings-grid">
<label class="wide"><span>URL</span><input type="url" name="ha_url" placeholder="http://homeassistant.local:8123"></label>
<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="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>
<div class="wide sensor-alias-manager">
<div class="settings-subhead"><div><strong data-i18n="settings.sensorAliases">Sensor aliases</strong><small data-i18n="settings.sensorAliasesHint">Friendly names are used in charts and selectors; the original entity_id remains unchanged.</small></div></div>
<div id="sensorAliasList" class="sensor-alias-list"></div>
<div class="sensor-alias-add"><input id="sensorAliasEntity" placeholder="sensor.gabinet_temperature"><input id="sensorAliasName" data-i18n-placeholder="settings.aliasPlaceholder" placeholder="Gabinet"><button type="button" class="secondary" id="addSensorAlias" data-i18n="actions.add">Add</button></div>
</div>
<div class="form-actions wide"><button type="button" class="secondary" id="haTest" data-i18n="settings.testHa">Test HA</button></div>
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head"><div><h3 data-i18n="settings.haIntegrationAccess">Home Assistant integration access</h3><p data-i18n="settings.haIntegrationHint">Create a controller token and paste it into the GREE Controller integration in Home Assistant.</p></div></div>
<div class="token-manager"><div id="accessTokenList" class="token-list"></div><div class="form-actions"><button type="button" class="primary" id="createAccessToken" data-i18n="settings.newToken">Create new token</button></div></div>
</section>
<div class="settings-save-bar"><span data-i18n="homeAssistant.saveHint">Save Home Assistant and sensor changes.</span><button class="primary" type="submit" data-i18n="actions.save">Save</button></div>
</form>
</section>
<section class="view" data-view="settings"> <section class="view" data-view="settings">
<div class="section-heading"><div><span class="eyebrow" data-i18n="settings.system">System</span><h1 data-i18n="nav.settings">Settings</h1></div></div> <div class="section-heading"><div><span class="eyebrow" data-i18n="settings.system">System</span><h1 data-i18n="nav.settings">Settings</h1></div></div>
<form class="settings-form" id="settingsForm"> <form class="settings-form" id="settingsForm">
@@ -150,17 +200,7 @@
</div> </div>
</section> </section>
<section class="panel settings-block">
<div class="settings-block-head"><div><h3 data-i18n="settings.nightMode">Night mode</h3><p data-i18n="settings.nightModeHint">During selected hours the thermostat limits fan speed and can request Quiet on supported units.</p></div></div>
<div class="settings-grid">
<label class="check wide"><input type="checkbox" name="night_mode_enabled"> <span data-i18n="settings.nightModeEnabled">Enable night mode</span></label>
<label><span data-i18n="settings.nightStart">Start</span><input type="time" name="night_mode_start" value="22:00"></label>
<label><span data-i18n="settings.nightEnd">End</span><input type="time" name="night_mode_end" value="06:00"></label>
<label><span data-i18n="settings.nightMaxFan">Maximum fan speed</span><select name="night_mode_max_fan_speed"><option value="1" data-i18n="fan.low">Low</option><option value="2" data-i18n="fan.mediumLow">Medium-low</option><option value="3" data-i18n="fan.medium">Medium</option><option value="4" data-i18n="fan.mediumHigh">Medium-high</option><option value="5" data-i18n="fan.high">High</option></select></label>
<label class="check"><input type="checkbox" name="night_mode_force_quiet"> <span data-i18n="settings.nightForceQuiet">Use Quiet when supported</span></label>
<label class="check wide"><input type="checkbox" name="night_mode_native_sleep"> <span data-i18n="settings.nightNativeSleep">Use native Sleep when supported</span></label>
</div>
</section>
<section class="panel settings-block"> <section class="panel settings-block">
<div class="settings-block-head"><div><h3 data-i18n="settings.metricsAndLogs">Metrics and logs</h3><p data-i18n="settings.compactionHint">SQLite keeps recent data locally and compacts older samples to the resolution used by charts.</p></div></div> <div class="settings-block-head"><div><h3 data-i18n="settings.metricsAndLogs">Metrics and logs</h3><p data-i18n="settings.compactionHint">SQLite keeps recent data locally and compacts older samples to the resolution used by charts.</p></div></div>
@@ -204,29 +244,9 @@
</div> </div>
</section> </section>
<section class="panel settings-block">
<div class="settings-block-head"><div><h3 data-i18n="settings.haSensorInput">Home Assistant sensor input</h3><p data-i18n="settings.haSensorInputHint">Optional. Used when a room zone reads an external Home Assistant temperature sensor.</p></div></div>
<div class="settings-grid">
<label class="wide"><span>URL</span><input type="url" name="ha_url" placeholder="http://homeassistant.local:8123"></label>
<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="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>
<div class="wide sensor-alias-manager">
<div class="settings-subhead"><div><strong data-i18n="settings.sensorAliases">Sensor aliases</strong><small data-i18n="settings.sensorAliasesHint">Friendly names are used in charts and selectors; the original entity_id remains unchanged.</small></div></div>
<div id="sensorAliasList" class="sensor-alias-list"></div>
<div class="sensor-alias-add"><input id="sensorAliasEntity" placeholder="sensor.gabinet_temperature"><input id="sensorAliasName" data-i18n-placeholder="settings.aliasPlaceholder" placeholder="Gabinet"><button type="button" class="secondary" id="addSensorAlias" data-i18n="actions.add">Add</button></div>
</div>
<div class="form-actions wide"><button type="button" class="secondary" id="haTest" data-i18n="settings.testHa">Test HA</button></div>
</div>
</section>
<section class="panel settings-block">
<div class="settings-block-head"><div><h3 data-i18n="settings.haIntegrationAccess">Home Assistant integration access</h3><p data-i18n="settings.haIntegrationHint">Create a controller token and paste it into the GREE Controller integration in Home Assistant.</p></div></div>
<div class="token-manager"><div id="accessTokenList" class="token-list"></div><div class="form-actions"><button type="button" class="primary" id="createAccessToken" data-i18n="settings.newToken">Create new token</button></div></div>
</section>
<section class="panel settings-block"> <section class="panel settings-block">
<div class="settings-block-head"><div><h3 data-i18n="settings.backup">Configuration backup</h3><p class="warning-note" data-i18n="settings.backupHint">Export/import application configuration. Exported files can contain GREE device keys plus Home Assistant and InfluxDB secrets; metrics and API access tokens are not included.</p></div></div> <div class="settings-block-head"><div><h3 data-i18n="settings.backup">Configuration backup</h3><p class="warning-note" data-i18n="settings.backupHint">Export/import application configuration. Exported files can contain GREE device keys plus Home Assistant and InfluxDB secrets; metrics and API access tokens are not included.</p></div></div>
@@ -259,6 +279,8 @@
<button data-go="schedules"><span data-i18n="nav.schedules">Schedules</span><span></span></button> <button data-go="schedules"><span data-i18n="nav.schedules">Schedules</span><span></span></button>
<button data-go="automations"><span data-i18n="nav.automations">Automations</span><span></span></button> <button data-go="automations"><span data-i18n="nav.automations">Automations</span><span></span></button>
<button data-go="simulation"><span data-i18n="nav.simulation">Simulator</span><span></span></button> <button data-go="simulation"><span data-i18n="nav.simulation">Simulator</span><span></span></button>
<button data-go="night"><span data-i18n="nav.nightMode">Night mode</span><span></span></button>
<button data-go="homeassistant"><span data-i18n="nav.homeAssistant">Home Assistant / Sensors</span><span></span></button>
<button data-go="settings"><span data-i18n="nav.settings">Settings</span><span></span></button> <button data-go="settings"><span data-i18n="nav.settings">Settings</span><span></span></button>
<button data-go="logs"><span data-i18n="nav.logsAndEvents">Events and logs</span><span></span></button> <button data-go="logs"><span data-i18n="nav.logsAndEvents">Events and logs</span><span></span></button>
</div> </div>
+9
View File
@@ -612,3 +612,12 @@ html[data-theme='light'] .simulation-metrics > div, html[data-theme='light'] .si
.device-capability-buttons { display:flex; flex-wrap:wrap; gap:7px; } .device-capability-buttons { display:flex; flex-wrap:wrap; gap:7px; }
.device-capability-buttons button { min-height:34px; padding:7px 11px; border:1px solid var(--line); border-radius:11px; background:var(--surface-muted); color:var(--text); } .device-capability-buttons button { min-height:34px; padding:7px 11px; border:1px solid var(--line); border-radius:11px; background:var(--surface-muted); color:var(--text); }
.device-capability-buttons button.active { border-color:var(--accent); color:var(--accent); } .device-capability-buttons button.active { border-color:var(--accent); color:var(--accent); }
/* v0.5.5: compact, centered unit-feature controls */
.device-capability-panel { justify-items:center; text-align:center; padding:11px 0 3px; }
.device-capability-panel > small { text-align:center; }
.device-capability-buttons { justify-content:center; align-items:center; gap:6px; max-width:100%; }
.device-capability-buttons button { min-height:28px; padding:5px 9px; border-radius:10px; color:var(--muted); font-size:10px; line-height:1.1; background:transparent; }
.device-capability-buttons button:hover { color:var(--text); background:var(--surface-muted); }
.device-capability-buttons button.active { color:var(--accent); background:color-mix(in srgb, var(--accent) 12%, var(--surface)); border-color:color-mix(in srgb, var(--accent) 45%, var(--line)); }
.standalone-settings-form { max-width:1100px; }
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'gree-controller-v054'; const CACHE = 'gree-controller-v055';
const ASSETS = ['/', '/styles.css', '/app.js', '/favicon.svg', '/manifest.webmanifest', '/lang/index.json', '/lang/en.json']; const ASSETS = ['/', '/styles.css', '/app.js', '/favicon.svg', '/manifest.webmanifest', '/lang/index.json', '/lang/en.json'];
self.addEventListener('install', event => event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(ASSETS)).then(() => self.skipWaiting()))); self.addEventListener('install', event => event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(ASSETS)).then(() => self.skipWaiting())));
self.addEventListener('activate', event => event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))).then(() => self.clients.claim()))); self.addEventListener('activate', event => event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))).then(() => self.clients.claim())));