From a679eb574724e49fdb9ca4fd291f9cbda2bd0a95 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?=
Date: Wed, 21 Oct 2026 11:27:44 +0200
Subject: [PATCH] v0.16.5
---
Cargo.lock | 2 +-
Cargo.toml | 2 +-
FILE_MANIFEST.sha256 | 53 ++++++-------
README.md | 6 +-
SOURCE_MANIFEST.sha256 | 51 ++++++------
docs/API.md | 8 ++
docs/openapi.json | 17 +++-
.../gree_controller/manifest.json | 2 +-
.../repository/gree-controller/CHANGELOG.md | 8 ++
.../repository/gree-controller/config.yaml | 2 +-
lang/en.json | 21 ++++-
lang/pl.json | 21 ++++-
scripts/test_temperature_explanation_ui.js | 49 ++++++++++++
src/api/zones.rs | 4 +
src/engine/control_plan.rs | 6 +-
src/engine/temperature.rs | 12 +++
src/engine/tests.rs | 41 ++++++++++
src/engine/zone_actions.rs | 4 +
src/engine/zone_control.rs | 77 +++++++++++++++++++
src/home_assistant.rs | 44 ++++++++---
src/models/zone.rs | 8 ++
web/css/styles.css | 58 ++++++++++++++
web/index.html | 2 +-
web/js-dynamic/dashboard.js | 1 +
web/js-dynamic/entities.js | 63 ++++++++++++++-
25 files changed, 484 insertions(+), 78 deletions(-)
create mode 100644 scripts/test_temperature_explanation_ui.js
diff --git a/Cargo.lock b/Cargo.lock
index 8aead0e..59aa397 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -719,7 +719,7 @@ dependencies = [
[[package]]
name = "gree-controller"
-version = "0.16.4"
+version = "0.16.5"
dependencies = [
"aes",
"aes-gcm",
diff --git a/Cargo.toml b/Cargo.toml
index 521fa3b..079cc09 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
-version = "0.16.4"
+version = "0.16.5"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256
index 802b50e..4dc4afd 100644
--- a/FILE_MANIFEST.sha256
+++ b/FILE_MANIFEST.sha256
@@ -1,17 +1,17 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore
-147eed97b308e0c27d6a5ee738a0ee710fa6ba2c6bbf7503df0fe484e0c7f842 ./Cargo.lock
-c604af0060aef4e79d2e11fd1e1222a2444cebc835bc50629cff3d2373c5a05a ./Cargo.toml
+795c232a7845a217a5711da4d9547282ba7f42fd160a249df07d631fac83928b ./Cargo.lock
+2c57e2334ace9e7e320c97b14e1b0372c881717dad4bf3e6ef6abae252ed8056 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
-75a6652611273ae458cb3509401cdb196d1084db1e49496ecdce448f490e6249 ./README.md
-eedafc7faaa9eb95b564a249a4fef82d5f4cc32697eb0e370b96264f3c47b69c ./SOURCE_MANIFEST.sha256
+eb8dd4d456419762d78e8d426326ac33fa99dbf857ab0f34f52dadc30e8a62be ./README.md
+c0f61039799416d2d72d37be85beabd1a68fc3325563189732027df31e7efc2b ./SOURCE_MANIFEST.sha256
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
-bb9840a0dc2b89d057d1461ae581ee61ebf856e3b6d38bc7d1bdd111eef7c550 ./docs/API.md
+e961deaaa82be18f9e2933999dda33f55bfc648200e62043b5e8fffdfdd4464c ./docs/API.md
4f9826f1bf3b42ed2d62d8a8e9221b50175b3a9fdda7e9d219351f7a33039257 ./docs/FLOW.md
-52c8dc48aed987fd28b2b543af1e5a782efb2a44104ccb0fd6b1740255cf384e ./docs/openapi.json
-d8a1b3e34cf8a79ac3296bf600ad47826d639cdef4c1c2c099f8b6b21a391ebd ./flows/simple-heating/igor-simple-heat-v5.flow.json
-bd9a07b8ca4b090ad0089e9037492cc25c47f3456b97ec08152947314ea904a6 ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json
-c4e6e3e8df5eb7f9730a79fb90edd155f0510f7bb4862a04b9f35ba649c22376 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json
+5c24d9f03e5025eda289e6862a7919fe3951992f7120647dc6e0cd0f1f9aa96f ./docs/openapi.json
+a5ad73f7933031dfaa99aa92e27f10223d53fb84b14ee0a378eb5f10eb80e8c8 ./flows/simple-heating/igor-simple-heat-v5.flow.json
+9743f4328a18bc9d89d10a9011d13ca201a4f352db2119335a66a2147a9a486d ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json
+2d804831a72577e80dc1ec3cf4f86dc8bcf00b1d8c80929896f11da0230b2b07 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
@@ -25,7 +25,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
-e8d079ebe63cd2916bd219cd45f583940e3d7ba5769af8deb1038415084a1a35 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
+4ecd424fd1969a1bcd81eb85ea5ac50295a0d7691d50013027a7c2e400aec449 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
@@ -33,10 +33,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
-904b21f54c7af0f59d91016d8513baaea5e927a349e18207f4290c51d7f2be8a ./ha-addon/repository/gree-controller/CHANGELOG.md
+a4ae5bfa385365ef5db5332f2635a77e212a82e00a3cf473c5617ad134d3133e ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md
-efe06cb35044b19474ae11e5ef42e3201787ff1d164087cdbdb845a5ad6a16d8 ./ha-addon/repository/gree-controller/config.yaml
+1fa36a0a0a3219604073950cd936c68fa6de1f51c031443ab22bf66596e0733f ./ha-addon/repository/gree-controller/config.yaml
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
@@ -46,8 +46,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
-4bf92ee5627be67b66d76a6106b224159f95071f0aa2a727cd964e113bc3b32a ./lang/en.json
-1e056cd915ba2e808db1287c99c06bbbbb3c98e5008481471b895e6ec20ea43a ./lang/pl.json
+f3c1286c3758a98b6a2945562753262e9083a6c638a1dcae556c3e978b4a69af ./lang/en.json
+ac6e6285a6153c5d8ce3c45d81ca6fe6c688505957e142e79e0ec0b496c400f3 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -103,6 +103,7 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js
e3825cfd9e6e149fd227c521d886dc116170f2553d4d7639d61874267f257b6e ./scripts/test_flow_disable_ui.js
2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js
+9bef00c3a0956c5c893eff6cf1fee9681140a1e76da630fa5a347bce19f07981 ./scripts/test_temperature_explanation_ui.js
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
c260a6d291ad913f6aca60f23a0acf6928d68cfb0545b2a698d58c5a95fcf934 ./scripts/verify_simple_heating.py
c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs
@@ -127,7 +128,7 @@ b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/publ
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs
3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs
-eaa4070afbc19686d5e9bdb2d24fb2c7fc4bf1f90dc1c4f12fef17017d4cb975 ./src/api/zones.rs
+79076544f4145c0eb120d544e9fab767b5c7ae625c2db007ee8be8a06bb0b85e ./src/api/zones.rs
32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs
de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs
87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs
@@ -146,7 +147,7 @@ f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_
e6d6bf39e254bd2779d3bcb31d966aa3f6688ebfa91b4088b70fd796cad66596 ./src/engine/automations.rs
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
-62a15ea757c53796149e670863dae1b4e1a16ceb6ef88ed84ebd3089c0b9880d ./src/engine/control_plan.rs
+c6b9651090e89fe0436826f66ae56825bf1e4b209ef8a27beb22fb1deaaa8c36 ./src/engine/control_plan.rs
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
@@ -158,13 +159,13 @@ c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/r
48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs
9adfcb3c3dffbaa870b314904bd1995e7b78c3f12febd69e23433179e4bf3fba ./src/engine/simple_heat.rs
2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs
-d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs
+b35d7e96e50d40bf26be2e47e1fa1d393f4c07caeac41f3595f5d595579e55f7 ./src/engine/temperature.rs
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
-4160811f47eca6e75970fe6c6b95198f86a4b97b7050531c19a19efb3d76d97d ./src/engine/tests.rs
-07c3c462884344f018654267de3d454d895343b4699e34635e4d1ec88546df0a ./src/engine/zone_actions.rs
-e246b6d6e3126941b989735a27e5fbe88dcaf351420aaaa060bb3761620d8244 ./src/engine/zone_control.rs
+f563f2da2409bf291d67daa1b2b2fd6c7a463a3a11343afb08131d20cd26cd69 ./src/engine/tests.rs
+63ded0ec62833699cbb7fcc6e4189ef19feafc23b9838d8fe9ba98889feb3e67 ./src/engine/zone_actions.rs
+38256ec0e5ee735aa2f7ad7f8daaf2aa96077ea16a6c2cbceac43c4fe2bf8aa5 ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
-d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs
+8e181f29119ef6c32ee3821fe220ade97298c105c681c41466af0c16e539b7db ./src/home_assistant.rs
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs
2d36405912646b30b1a1a37a7a4e18c2104ab22e865b593316a609dda884aec2 ./src/influxdb/query.rs
@@ -181,7 +182,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f
17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
-79cb16a9f12e8a20fcf0acc8649d771b8a76a05503c0372443d29003536c1bcb ./src/models/zone.rs
+2b861f3b37274f4d0b9713706ae959220935bdaf63afc513329d1c48ac0f4dd9 ./src/models/zone.rs
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
@@ -207,16 +208,16 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
-b030e61b30435495f492e15fa9daf228968cc1de2982e8f40a626452dbf5b81a ./web/css/styles.css
+49d7992d8ebaf0b1eddd6538479b00a750897bc0454f3daa37eb01ed914bea8b ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
-bbe640e99447f632383b2eb4b52da580ab95d60cd45cda47f6fae1c2380e5b8a ./web/index.html
+bb1229db66ab2b510a44308e21796ba01f54c36238f5785ff7fc998dedd09c18 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js
-ef20fb1e706105d0afdc2fa505d92ea52e43d93c9a687f2a09d381227a464564 ./web/js-dynamic/dashboard.js
-3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./web/js-dynamic/entities.js
+1bf5b5e295f685b4c54064eb91220a50cb85264f7e90dfcd5bfe5c5e8ac111a2 ./web/js-dynamic/dashboard.js
+4ddfe365f66b968fae490b19ab321e3497f9fba4e220123edd0a4e0353254d97 ./web/js-dynamic/entities.js
bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js
3e2a5f1aab56e918624e6e7af064eecb9e94e554fd4d185715f8ecf3747a6895 ./web/js-dynamic/flows.js
1d54ae8919edb61414f9ffc46f85394b29c3c355827120264352cccb32f65e68 ./web/js-dynamic/forms.js
diff --git a/README.md b/README.md
index b9157b4..32ea073 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
-**Version: 0.16.4**
+**Version: 0.16.5**
-Version 0.16.4 fixes disabled Flows that could resume execution after an asynchronous condition evaluation, hides disabled Flow outputs from control plans and schedules, resets timing gates on re-enable, and releases stale Flow thermostat intent.
+Version 0.16.5 adds a temperature-origin explanation in the zone/thermostat UI, transparent room-sensor diagnostics, and a fail-safe that pauses automatic heating when HA sensor readings cannot be trusted.
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
> Built-in API: `/api/*` and `/ws`; interactive Swagger docs: `/api-docs`; OpenAPI JSON: `/api-docs/openapi.json`.
@@ -306,7 +306,7 @@ In standalone mode, the Home Assistant connection test checks only `HA_URL` and
- `home_assistant` — Home Assistant room sensor,
- `combined` — weighted GREE + Home Assistant value.
-For `home_assistant` and `combined`, each zone must set its own `ha_entity_id` room-temperature sensor. Room sensors are independent from the outdoor-temperature source and stale/unavailable room data falls back to the GREE sensor when possible.
+For `home_assistant` and `combined`, each zone must set its own `ha_entity_id` room-temperature sensor. Room sensors are independent from the outdoor-temperature source. Stale/unavailable room data falls back to GREE as a **display reading**, but automatic **heating is paused** rather than started or boosted from that fallback. Already-running automatic heating is powered off when compressor protection permits; cooling, explicit manual operation and GREE-only zones retain their existing behavior. The guard also applies when `combined` readings disagree beyond the configured maximum difference. The UI includes a temperature-origin help tooltip and a sensor-safeguard status. Where Home Assistant supplies `last_reported`, it takes precedence over `last_updated` for sensor freshness, so a stable temperature repeatedly reported by HA is not incorrectly treated as stale.
`HA_OUTDOOR_ENTITY_ID` is the global outdoor-temperature source used by outdoor-temperature assist. A zone may leave `ha_outdoor_entity_id` empty to use that global sensor, or set `ha_outdoor_entity_id` to use a different outdoor sensor only for that zone. If a zone override is unavailable, the controller falls back to the global outdoor source. Global and per-zone outdoor sensors are exposed through the normal alias/history/metric paths and appear in Home Assistant entity suggestions used by Visual Flows.
diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256
index 5af6dd6..4c4de39 100644
--- a/SOURCE_MANIFEST.sha256
+++ b/SOURCE_MANIFEST.sha256
@@ -1,16 +1,16 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore
-147eed97b308e0c27d6a5ee738a0ee710fa6ba2c6bbf7503df0fe484e0c7f842 ./Cargo.lock
-c604af0060aef4e79d2e11fd1e1222a2444cebc835bc50629cff3d2373c5a05a ./Cargo.toml
+795c232a7845a217a5711da4d9547282ba7f42fd160a249df07d631fac83928b ./Cargo.lock
+2c57e2334ace9e7e320c97b14e1b0372c881717dad4bf3e6ef6abae252ed8056 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
-75a6652611273ae458cb3509401cdb196d1084db1e49496ecdce448f490e6249 ./README.md
+eb8dd4d456419762d78e8d426326ac33fa99dbf857ab0f34f52dadc30e8a62be ./README.md
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
-bb9840a0dc2b89d057d1461ae581ee61ebf856e3b6d38bc7d1bdd111eef7c550 ./docs/API.md
+e961deaaa82be18f9e2933999dda33f55bfc648200e62043b5e8fffdfdd4464c ./docs/API.md
4f9826f1bf3b42ed2d62d8a8e9221b50175b3a9fdda7e9d219351f7a33039257 ./docs/FLOW.md
-52c8dc48aed987fd28b2b543af1e5a782efb2a44104ccb0fd6b1740255cf384e ./docs/openapi.json
-d8a1b3e34cf8a79ac3296bf600ad47826d639cdef4c1c2c099f8b6b21a391ebd ./flows/simple-heating/igor-simple-heat-v5.flow.json
-bd9a07b8ca4b090ad0089e9037492cc25c47f3456b97ec08152947314ea904a6 ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json
-c4e6e3e8df5eb7f9730a79fb90edd155f0510f7bb4862a04b9f35ba649c22376 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json
+5c24d9f03e5025eda289e6862a7919fe3951992f7120647dc6e0cd0f1f9aa96f ./docs/openapi.json
+a5ad73f7933031dfaa99aa92e27f10223d53fb84b14ee0a378eb5f10eb80e8c8 ./flows/simple-heating/igor-simple-heat-v5.flow.json
+9743f4328a18bc9d89d10a9011d13ca201a4f352db2119335a66a2147a9a486d ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json
+2d804831a72577e80dc1ec3cf4f86dc8bcf00b1d8c80929896f11da0230b2b07 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
@@ -24,7 +24,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
-e8d079ebe63cd2916bd219cd45f583940e3d7ba5769af8deb1038415084a1a35 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
+4ecd424fd1969a1bcd81eb85ea5ac50295a0d7691d50013027a7c2e400aec449 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
@@ -32,10 +32,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
-904b21f54c7af0f59d91016d8513baaea5e927a349e18207f4290c51d7f2be8a ./ha-addon/repository/gree-controller/CHANGELOG.md
+a4ae5bfa385365ef5db5332f2635a77e212a82e00a3cf473c5617ad134d3133e ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md
-efe06cb35044b19474ae11e5ef42e3201787ff1d164087cdbdb845a5ad6a16d8 ./ha-addon/repository/gree-controller/config.yaml
+1fa36a0a0a3219604073950cd936c68fa6de1f51c031443ab22bf66596e0733f ./ha-addon/repository/gree-controller/config.yaml
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
@@ -45,8 +45,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
-4bf92ee5627be67b66d76a6106b224159f95071f0aa2a727cd964e113bc3b32a ./lang/en.json
-1e056cd915ba2e808db1287c99c06bbbbb3c98e5008481471b895e6ec20ea43a ./lang/pl.json
+f3c1286c3758a98b6a2945562753262e9083a6c638a1dcae556c3e978b4a69af ./lang/en.json
+ac6e6285a6153c5d8ce3c45d81ca6fe6c688505957e142e79e0ec0b496c400f3 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -102,6 +102,7 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js
e3825cfd9e6e149fd227c521d886dc116170f2553d4d7639d61874267f257b6e ./scripts/test_flow_disable_ui.js
2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js
+9bef00c3a0956c5c893eff6cf1fee9681140a1e76da630fa5a347bce19f07981 ./scripts/test_temperature_explanation_ui.js
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
c260a6d291ad913f6aca60f23a0acf6928d68cfb0545b2a698d58c5a95fcf934 ./scripts/verify_simple_heating.py
c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs
@@ -126,7 +127,7 @@ b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/publ
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs
3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs
-eaa4070afbc19686d5e9bdb2d24fb2c7fc4bf1f90dc1c4f12fef17017d4cb975 ./src/api/zones.rs
+79076544f4145c0eb120d544e9fab767b5c7ae625c2db007ee8be8a06bb0b85e ./src/api/zones.rs
32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs
de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs
87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs
@@ -145,7 +146,7 @@ f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_
e6d6bf39e254bd2779d3bcb31d966aa3f6688ebfa91b4088b70fd796cad66596 ./src/engine/automations.rs
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
-62a15ea757c53796149e670863dae1b4e1a16ceb6ef88ed84ebd3089c0b9880d ./src/engine/control_plan.rs
+c6b9651090e89fe0436826f66ae56825bf1e4b209ef8a27beb22fb1deaaa8c36 ./src/engine/control_plan.rs
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
@@ -157,13 +158,13 @@ c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/r
48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs
9adfcb3c3dffbaa870b314904bd1995e7b78c3f12febd69e23433179e4bf3fba ./src/engine/simple_heat.rs
2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs
-d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs
+b35d7e96e50d40bf26be2e47e1fa1d393f4c07caeac41f3595f5d595579e55f7 ./src/engine/temperature.rs
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
-4160811f47eca6e75970fe6c6b95198f86a4b97b7050531c19a19efb3d76d97d ./src/engine/tests.rs
-07c3c462884344f018654267de3d454d895343b4699e34635e4d1ec88546df0a ./src/engine/zone_actions.rs
-e246b6d6e3126941b989735a27e5fbe88dcaf351420aaaa060bb3761620d8244 ./src/engine/zone_control.rs
+f563f2da2409bf291d67daa1b2b2fd6c7a463a3a11343afb08131d20cd26cd69 ./src/engine/tests.rs
+63ded0ec62833699cbb7fcc6e4189ef19feafc23b9838d8fe9ba98889feb3e67 ./src/engine/zone_actions.rs
+38256ec0e5ee735aa2f7ad7f8daaf2aa96077ea16a6c2cbceac43c4fe2bf8aa5 ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
-d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs
+8e181f29119ef6c32ee3821fe220ade97298c105c681c41466af0c16e539b7db ./src/home_assistant.rs
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs
2d36405912646b30b1a1a37a7a4e18c2104ab22e865b593316a609dda884aec2 ./src/influxdb/query.rs
@@ -180,7 +181,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f
17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
-79cb16a9f12e8a20fcf0acc8649d771b8a76a05503c0372443d29003536c1bcb ./src/models/zone.rs
+2b861f3b37274f4d0b9713706ae959220935bdaf63afc513329d1c48ac0f4dd9 ./src/models/zone.rs
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
@@ -206,16 +207,16 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
-b030e61b30435495f492e15fa9daf228968cc1de2982e8f40a626452dbf5b81a ./web/css/styles.css
+49d7992d8ebaf0b1eddd6538479b00a750897bc0454f3daa37eb01ed914bea8b ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
-bbe640e99447f632383b2eb4b52da580ab95d60cd45cda47f6fae1c2380e5b8a ./web/index.html
+bb1229db66ab2b510a44308e21796ba01f54c36238f5785ff7fc998dedd09c18 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js
-ef20fb1e706105d0afdc2fa505d92ea52e43d93c9a687f2a09d381227a464564 ./web/js-dynamic/dashboard.js
-3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./web/js-dynamic/entities.js
+1bf5b5e295f685b4c54064eb91220a50cb85264f7e90dfcd5bfe5c5e8ac111a2 ./web/js-dynamic/dashboard.js
+4ddfe365f66b968fae490b19ab321e3497f9fba4e220123edd0a4e0353254d97 ./web/js-dynamic/entities.js
bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js
3e2a5f1aab56e918624e6e7af064eecb9e94e554fd4d185715f8ecf3747a6895 ./web/js-dynamic/flows.js
1d54ae8919edb61414f9ffc46f85394b29c3c355827120264352cccb32f65e68 ./web/js-dynamic/forms.js
diff --git a/docs/API.md b/docs/API.md
index 1097d4d..fc9235b 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -606,6 +606,14 @@ Important rules:
The returned `Zone` also contains runtime state including sensor readings, resolved/effective setpoints, demand, current preset, manual overrides, local Quick Thermostat ownership, temporary session state, device-manual takeover, control owner/source/reason, lockout timestamps and `created_at`/`updated_at`.
+Temperature diagnostics in `GET /api/zones` and zone WebSocket updates:
+
+- `control_temperature_source`: `device`, `external`, `combined`, `device_fallback`, `device_discrepancy_fallback`, `unavailable`.
+- `room_sensor_status`: `not_used`, `ok`, `stale`, `error`, `discrepancy` (empty until the first cycle on older persisted zones).
+- `heating_sensor_guard_active`: `true` when the thermostat would otherwise fall back to GREE while an automatic heating zone is configured for a Home Assistant room sensor. Automatic heating is paused, pending starts are cancelled, and existing automatic heat is switched OFF when compressor protection permits. The guard clears automatically with a valid room reading. Explicit manual control and GREE-only heating are unchanged.
+
+For a `combined` sensor strategy, the displayed control temperature is GREE × (1 − `external_sensor_weight`) + room × `external_sensor_weight`, rounded to 0.1°C, unless the difference exceeds `max_sensor_difference`. The Web UI provides this explanation in the zone and thermostat temperature help. Home Assistant sample freshness prefers `last_reported` when supplied and otherwise uses `last_updated`.
+
### `GET /api/zones`
Returns `Zone[]`.
diff --git a/docs/openapi.json b/docs/openapi.json
index ac9c462..8a56b5c 100644
--- a/docs/openapi.json
+++ b/docs/openapi.json
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "GREE Controller API",
- "version": "0.16.4",
+ "version": "0.16.5",
"summary": "Local HTTP/WebSocket API for GREE Controller",
"description": "Local API used by the GREE Controller Web UI and Home Assistant integration.",
"license": {
@@ -6079,7 +6079,7 @@
},
"version": {
"type": "string",
- "example": "0.16.4"
+ "example": "0.16.5"
},
"uptime_seconds": {
"type": "integer",
@@ -7267,6 +7267,19 @@
"null"
]
},
+ "control_temperature_source": {
+ "type": "string",
+ "description": "Temperature source selected this cycle: device, external, combined, device_fallback, device_discrepancy_fallback or unavailable."
+ },
+ "room_sensor_status": {
+ "type": "string",
+ "enum": ["not_used", "ok", "stale", "error", "discrepancy"],
+ "description": "Room sensor diagnostic for the latest zone cycle; empty for legacy zones not yet cycled."
+ },
+ "heating_sensor_guard_active": {
+ "type": "boolean",
+ "description": "True when automatic heating is paused due to an untrusted room sensor; the unit is turned OFF as compressor protection allows."
+ },
"effective_mode": {
"type": "string"
},
diff --git a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json
index 2dd93a0..56baf19 100644
--- a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json
+++ b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
- "version": "0.16.4",
+ "version": "0.16.5",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
diff --git a/ha-addon/repository/gree-controller/CHANGELOG.md b/ha-addon/repository/gree-controller/CHANGELOG.md
index aeff2e6..125a778 100644
--- a/ha-addon/repository/gree-controller/CHANGELOG.md
+++ b/ha-addon/repository/gree-controller/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.16.5
+
+- Shows an accessible "Where does this temperature come from?" explanation on each zone and dashboard thermostat: weighted GREE/room formula, rounded result, source fallback and sensor status.
+- Adds fail-safe automatic-heating control: missing, stale or excessively different HA room readings cannot start or boost heat from the GREE fallback. Existing automatic heating turns OFF once compressor minimum-on protection permits. Manual control, cooling and GREE-only configurations are unchanged.
+- Displays blocked heating in the live thermostat status and "Why the controller works this way" control plan, including logs on guard activation/recovery.
+- Uses Home Assistant `last_reported` for freshness when available (otherwise `last_updated`), to avoid treating repeatedly reported stable readings as stale.
+- Extends the zone API diagnostics and ships Rust/UI regression tests plus the three day/night Flow examples.
+
## 0.16.4
- Fixes a race in stateful Flow checkpoint saving: no stale condition evaluation can re-enable, overwrite or resurrect a disabled/deleted/recompiled automation.
diff --git a/ha-addon/repository/gree-controller/config.yaml b/ha-addon/repository/gree-controller/config.yaml
index c81b1bb..0c956e4 100644
--- a/ha-addon/repository/gree-controller/config.yaml
+++ b/ha-addon/repository/gree-controller/config.yaml
@@ -1,5 +1,5 @@
name: "GREE Controller"
-version: "0.16.4"
+version: "0.16.5"
slug: "gree_controller"
description: "Local GREE HVAC controller with Web UI, API, and Home Assistant integration"
url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/"
diff --git a/lang/en.json b/lang/en.json
index 770fb96..2960991 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -132,7 +132,7 @@
"zones.roomSensorEntity": "Room sensor entity_id",
"zones.roomSensorWeight": "Room sensor weight %",
"zones.maxDifference": "Max. sensor difference °C",
- "zones.sensorHelp": "Required when the temperature strategy uses Home Assistant. If it becomes unavailable, the controller falls back to the GREE sensor.",
+ "zones.sensorHelp": "Required when the temperature strategy uses Home Assistant. If readings are missing or disagree, GREE is shown as a fallback but automatic heating is paused. Explicit manual control is unaffected.",
"zones.sensorRequired": "Enter the Home Assistant room temperature entity_id for this zone.",
"zones.outdoorSensorEntity": "Outdoor temperature entity_id override",
"zones.outdoorSensorHelp": "Leave empty to use the global Outdoor temperature entity_id. Set another entity_id to use a different outdoor sensor only for this zone. It is used by outdoor-temperature assist and recorded in history/metrics.",
@@ -145,6 +145,25 @@
"zones.sourceFallback": "GREE fallback",
"zones.sourceDiscrepancy": "GREE fallback: sensor mismatch",
"zones.sourceUnavailable": "unavailable",
+ "zones.temperatureHelp": "Where does this temperature come from?",
+ "zones.temperatureFormula": "Weighted average: GREE {gree} × {greeWeight}% + room {room} × {roomWeight}% = {used} (rounded to 0.1°C).",
+ "zones.temperatureFromRoom": "Control temperature {room} comes from the room sensor. The GREE reading {gree} does not affect it.",
+ "zones.temperatureFromGree": "Control temperature {gree} comes only from GREE.",
+ "zones.temperatureDiscrepancy": "Sensors differ by more than {limit}. GREE {gree}, room {room}; using GREE as fallback.",
+ "zones.temperatureFallback": "{reason} Temperature {gree} comes from the GREE fallback.",
+ "zones.temperatureUnavailable": "No reliable temperature measurement is available.",
+ "zones.sensorStale": "The room sensor reading is stale.",
+ "zones.sensorUnavailable": "The room sensor is unavailable or returned an error.",
+ "zones.sensorStatusLabel": "sensor: {status}",
+ "zones.sensorStatusOk": "OK",
+ "zones.sensorStatusStale": "stale",
+ "zones.sensorStatusError": "unavailable",
+ "zones.sensorStatusDiscrepancy": "readings disagree",
+ "zones.heatGuardActive": "Heating sensor safeguard active.",
+ "zones.heatGuardHint": "Automatic heating is paused until a valid room reading returns.",
+ "zones.heatGuardPendingOff": "Heating is paused; the controller will turn the unit off when compressor protection allows or retry.",
+ "zones.heatGuardExplanation": "The fallback GREE reading is for display only. Automatic control will not start or boost heating from it.",
+ "plan.reasonRoomSensorGuard": "Automatic heating paused: no trustworthy room sensor reading.",
"schedules.calendar": "Calendar",
"schedules.weekdays": "Weekdays",
"schedules.crossMidnight": "Intervals crossing midnight are supported",
diff --git a/lang/pl.json b/lang/pl.json
index be2644d..0d3e442 100644
--- a/lang/pl.json
+++ b/lang/pl.json
@@ -132,7 +132,7 @@
"zones.roomSensorEntity": "entity_id czujnika temperatury pokojowej",
"zones.roomSensorWeight": "Waga czujnika pokojowego %",
"zones.maxDifference": "Maks. różnica czujników °C",
- "zones.sensorHelp": "Wymagany, gdy strategia temperatury używa Home Assistant. Gdy czujnik będzie niedostępny, kontroler wróci do sensora GREE.",
+ "zones.sensorHelp": "Wymagany, gdy strategia temperatury używa Home Assistant. Gdy odczyt jest niedostępny lub rozbieżny, GREE służy jako odczyt awaryjny, ale automatyczne grzanie zostaje wstrzymane. Sterowanie ręczne nie jest blokowane.",
"zones.sensorRequired": "Podaj entity_id czujnika temperatury pokojowej z Home Assistant dla tej strefy.",
"zones.outdoorSensorEntity": "Nadpisanie entity_id temperatury zewnętrznej",
"zones.outdoorSensorHelp": "Pozostaw puste, aby użyć globalnego Outdoor temperature entity_id. Wybierz inne entity_id, aby tylko ta strefa używała innego czujnika zewnętrznego. Czujnik jest używany przez wspomaganie temperaturą zewnętrzną oraz zapisywany w historii i metrykach.",
@@ -145,6 +145,25 @@
"zones.sourceFallback": "fallback GREE",
"zones.sourceDiscrepancy": "fallback GREE: rozbieżność",
"zones.sourceUnavailable": "niedostępna",
+ "zones.temperatureHelp": "Skąd ta temperatura?",
+ "zones.temperatureFormula": "Średnia ważona: GREE {gree} × {greeWeight}% + pokój {room} × {roomWeight}% = {used} (wynik zaokrąglony do 0,1°C).",
+ "zones.temperatureFromRoom": "Temperatura sterująca {room} pochodzi z czujnika pokojowego. Odczyt GREE {gree} nie wpływa na wynik.",
+ "zones.temperatureFromGree": "Temperatura sterująca {gree} pochodzi wyłącznie z GREE.",
+ "zones.temperatureDiscrepancy": "Czujniki różnią się o więcej niż {limit}. GREE {gree}, pokój {room}; awaryjnie wybrano GREE.",
+ "zones.temperatureFallback": "{reason} Temperatura {gree} pochodzi awaryjnie z GREE.",
+ "zones.temperatureUnavailable": "Brak wiarygodnego pomiaru temperatury.",
+ "zones.sensorStale": "Odczyt czujnika pokojowego jest nieaktualny.",
+ "zones.sensorUnavailable": "Czujnik pokojowy jest niedostępny lub zgłosił błąd.",
+ "zones.sensorStatusLabel": "czujnik: {status}",
+ "zones.sensorStatusOk": "OK",
+ "zones.sensorStatusStale": "nieaktualny",
+ "zones.sensorStatusError": "niedostępny",
+ "zones.sensorStatusDiscrepancy": "rozbieżność odczytów",
+ "zones.heatGuardActive": "Zabezpieczenie grzania aktywne.",
+ "zones.heatGuardHint": "Automatyczne grzanie wstrzymane do powrotu prawidłowego odczytu pokojowego.",
+ "zones.heatGuardPendingOff": "Grzanie wstrzymane; sterownik wyłączy jednostkę po spełnieniu ochrony sprężarki lub ponowi próbę.",
+ "zones.heatGuardExplanation": "Odczyt awaryjny GREE służy tylko do prezentacji. Automatyka nie uruchomi ani nie zwiększy grzania na jego podstawie.",
+ "plan.reasonRoomSensorGuard": "Automatyczne grzanie wstrzymane: brak wiarygodnego odczytu czujnika pokojowego.",
"schedules.calendar": "Kalendarz",
"schedules.weekdays": "Dni tygodnia",
"schedules.crossMidnight": "Obsługa przedziałów przez północ",
diff --git a/scripts/test_temperature_explanation_ui.js b/scripts/test_temperature_explanation_ui.js
new file mode 100644
index 0000000..c470e16
--- /dev/null
+++ b/scripts/test_temperature_explanation_ui.js
@@ -0,0 +1,49 @@
+// Focused UI checks for zone temperature origin and sensor-safety indicators.
+// Run from the project root: node scripts/test_temperature_explanation_ui.js
+'use strict';
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const vm = require('node:vm');
+const dict = JSON.parse(fs.readFileSync('lang/pl.json', 'utf8')).translations;
+const ctx = vm.createContext({
+ app: { zones: [], devices: [], groups: [] },
+ tr: (key, vars = {}) => String(dict[key] || key).replace(/\{(\w+)\}/g, (match, name) => vars[name] ?? match),
+ fmtTemp: value => value == null ? '--' : `${Number(value).toFixed(1)}°C`,
+ uiIcon: name => ``,
+ esc: value => String(value ?? '').replace(/[&<>'"]/g, x => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' })[x]),
+});
+vm.runInContext(fs.readFileSync('web/js-dynamic/entities.js', 'utf8'), ctx);
+
+const combined = {
+ sensor_source: 'combined',
+ control_temperature_source: 'combined',
+ device_temperature: 19,
+ external_temperature: 21,
+ external_sensor_weight: 0.8,
+ current_temperature: 20.6,
+ room_sensor_status: 'ok',
+ enabled: true,
+};
+const formula = ctx.zoneTemperatureExplanation(combined);
+assert.match(formula, /GREE 19\.0°C × 20%/);
+assert.match(formula, /pokój 21\.0°C × 80% = 20\.6°C/);
+const help = ctx.zoneTemperatureHelp(combined);
+assert.match(help, / Result Result Result bool {
+ mode == "heat"
+ && zone.enabled
+ && zone.control_owner == "automation"
+ && matches!(zone.sensor_source.as_str(), "home_assistant" | "combined")
+ && !matches!(zone.control_temperature_source.as_str(), "external" | "combined")
+}
+
fn adjustment_allowed(zone: &Zone) -> bool {
let Some(last) = zone.last_action_at else {
return true;
diff --git a/src/engine/tests.rs b/src/engine/tests.rs
index e30e2fe..a5db87a 100644
--- a/src/engine/tests.rs
+++ b/src/engine/tests.rs
@@ -770,6 +770,8 @@ mod tests {
external_temperature: None,
current_temperature: None,
control_temperature_source: "device".into(),
+ room_sensor_status: "not_used".into(),
+ heating_sensor_guard_active: false,
active_preset: "comfort".into(),
manual_preset: None,
manual_setpoint: None,
@@ -1502,6 +1504,45 @@ mod tests {
assert!(!discrepancy);
}
+ #[test]
+ fn room_sensor_guard_blocks_only_automatic_heating_on_untrusted_fallback() {
+ let mut zone = test_zone("combined");
+ zone.control_temperature_source = "device_fallback".into();
+ assert!(automatic_heating_needs_room_sensor_guard(&zone, "heat"));
+ zone.control_temperature_source = "device_discrepancy_fallback".into();
+ assert!(automatic_heating_needs_room_sensor_guard(&zone, "heat"));
+ zone.control_temperature_source = "unavailable".into();
+ assert!(automatic_heating_needs_room_sensor_guard(&zone, "heat"));
+
+ zone.control_temperature_source = "combined".into();
+ assert!(!automatic_heating_needs_room_sensor_guard(&zone, "heat"));
+ zone.control_temperature_source = "external".into();
+ assert!(!automatic_heating_needs_room_sensor_guard(&zone, "heat"));
+ zone.control_temperature_source = "device_fallback".into();
+ assert!(!automatic_heating_needs_room_sensor_guard(&zone, "cool"));
+ zone.control_owner = "local_thermostat".into();
+ assert!(!automatic_heating_needs_room_sensor_guard(&zone, "heat"));
+ zone.control_owner = "automation".into();
+ zone.sensor_source = "device".into();
+ assert!(!automatic_heating_needs_room_sensor_guard(&zone, "heat"));
+ zone.sensor_source = "home_assistant".into();
+ assert!(automatic_heating_needs_room_sensor_guard(&zone, "heat"));
+ zone.enabled = false;
+ assert!(!automatic_heating_needs_room_sensor_guard(&zone, "heat"));
+ }
+
+ #[test]
+ fn zone_sensor_guard_fields_default_when_loading_existing_zone() {
+ let zone = test_zone("combined");
+ let mut payload = serde_json::to_value(&zone).unwrap();
+ let object = payload.as_object_mut().unwrap();
+ object.remove("room_sensor_status");
+ object.remove("heating_sensor_guard_active");
+ let migrated: Zone = serde_json::from_value(payload).unwrap();
+ assert_eq!(migrated.room_sensor_status, "");
+ assert!(!migrated.heating_sensor_guard_active);
+ }
+
#[test]
fn seasonal_profiles_resolve_independently() {
let zone = test_zone("device");
diff --git a/src/engine/zone_actions.rs b/src/engine/zone_actions.rs
index 4594fab..f8e550b 100644
--- a/src/engine/zone_actions.rs
+++ b/src/engine/zone_actions.rs
@@ -37,6 +37,7 @@ fn zone_runtime_change_affects_control_plan(before: &Zone, after: &Zone) -> bool
"heat_away_setpoint": zone.heat_away_setpoint,
"current_temperature": zone.current_temperature,
"control_temperature_source": zone.control_temperature_source,
+ "heating_sensor_guard_active": zone.heating_sensor_guard_active,
"manual_preset": zone.manual_preset,
"manual_setpoint": zone.manual_setpoint,
"manual_override_until": zone.manual_override_until,
@@ -111,6 +112,9 @@ fn persist_zone_cycle(
latest.external_temperature = computed.external_temperature;
latest.current_temperature = computed.current_temperature;
latest.control_temperature_source = computed.control_temperature_source.clone();
+ latest.room_sensor_status = computed.room_sensor_status.clone();
+ // Guard state is a controller decision, not just a sensor observation; do not
+ // overwrite a newer manual/configuration change with a stale cycle's decision.
latest.updated_at = Utc::now();
state.db.save_zone(&latest)?;
}
diff --git a/src/engine/zone_control.rs b/src/engine/zone_control.rs
index 0f09fe8..042dd0a 100644
--- a/src/engine/zone_control.rs
+++ b/src/engine/zone_control.rs
@@ -206,6 +206,11 @@ fn refresh_zone_temperature(
room_sensor_results: &mut HashMap, Result)>,
) -> (String, bool) {
let previous_source = zone.control_temperature_source.clone();
+ let mut room_sensor_status = if zone.sensor_source == "device" {
+ "not_used"
+ } else {
+ "error"
+ };
let device_temperature =
if device.enabled && device.online && device.communication_failures == 0 {
device.current_temperature
@@ -216,6 +221,7 @@ fn refresh_zone_temperature(
if matches!(zone.sensor_source.as_str(), "home_assistant" | "combined") {
match room_sensor_results.remove(&zone.id) {
Some((resolved_entity, Ok(value))) => {
+ room_sensor_status = "ok";
if let Some(entity_id) = resolved_entity.as_deref() {
record_ha_history(
state,
@@ -229,6 +235,11 @@ fn refresh_zone_temperature(
Some(value)
}
Some((resolved_entity, Err(err))) => {
+ room_sensor_status = if err.contains("Home Assistant sensor is stale:") {
+ "stale"
+ } else {
+ "error"
+ };
if !matches!(
previous_source.as_str(),
"device_fallback" | "device_discrepancy_fallback"
@@ -262,10 +273,14 @@ fn refresh_zone_temperature(
};
let (temperature, source, discrepancy) =
select_zone_temperature(zone, device_temperature, external_temperature);
+ if discrepancy {
+ room_sensor_status = "discrepancy";
+ }
zone.device_temperature = device_temperature;
zone.external_temperature = external_temperature;
zone.current_temperature = temperature;
zone.control_temperature_source = source;
+ zone.room_sensor_status = room_sensor_status.into();
zone.updated_at = Utc::now();
(previous_source, discrepancy)
}
@@ -743,6 +758,8 @@ async fn control_zones(state: &AppState) -> Result<()> {
refresh_control_ownership(&mut zone);
let effective_mode = effective_mode_owned.as_str();
+ let was_heating_sensor_guard_active = zone.heating_sensor_guard_active;
+ zone.heating_sensor_guard_active = false;
let (previous_source, discrepancy) = refresh_zone_temperature(
state,
&settings,
@@ -902,6 +919,66 @@ async fn control_zones(state: &AppState) -> Result<()> {
zone.active_preset = preset;
zone.effective_setpoint = Some(target);
+ if automatic_heating_needs_room_sensor_guard(&zone, effective_mode) {
+ // Failing over to GREE is useful for displaying a temperature, but its
+ // lower reading must never start or boost automatic heating when the
+ // configured room sensor is stale/missing or disagrees excessively.
+ // Physically stop existing automatic heat once compressor protection allows.
+ zone.heating_sensor_guard_active = true;
+ zone.heat_coasting = false;
+ zone.demand = false;
+ zone.demand_since = None;
+ zone.target_alerted_at = None;
+ zone.simple_heat_runtime = SimpleHeatRuntime::default();
+ clear_compressor_pending(&mut zone, true);
+ zone.device_setpoint = if device.power { Some(device.target_temperature) } else { None };
+ if !was_heating_sensor_guard_active {
+ state.log("warn", "zone.heating_sensor_guard", &format!(
+ "Automatic heating paused for {}: room sensor {} (using {} temperature for display)",
+ zone.name, zone.room_sensor_status, zone.control_temperature_source,
+ ), json!({
+ "zone_id": zone.id,
+ "room_sensor_status": zone.room_sensor_status,
+ "control_temperature_source": zone.control_temperature_source,
+ "ha_entity_id": zone.ha_entity_id,
+ }));
+ }
+ if device.power && matches!(device.mode.as_str(), "heat" | "auto")
+ && device.online && device.communication_failures == 0
+ {
+ let protection = chrono::Duration::seconds(
+ zone.min_on_seconds.max(settings.compressor_protection_seconds).max(30) as i64,
+ );
+ let safe_to_switch_off = !settings.compressor_protection_enabled
+ || zone.last_power_change_at.is_none_or(|at|
+ Utc::now().signed_duration_since(at) >= protection);
+ if safe_to_switch_off {
+ match send_zone_command_if_owned(state, &zone.id, &zone.device_id, DeviceCommand {
+ power: Some(false), ..Default::default()
+ }).await {
+ Ok(Some(updated_device)) => {
+ if !updated_device.power {
+ zone.last_power_change_at = Some(Utc::now());
+ zone.last_action_at = Some(Utc::now());
+ zone.device_setpoint = None;
+ }
+ }
+ Ok(None) => {}
+ Err(err) => state.log("warn", "zone.heating_sensor_guard_off_error", &err.to_string(),
+ json!({"zone_id": zone.id})),
+ }
+ }
+ }
+ record_zone_history(state, &zone, outdoor_temperature, settings.poll_interval_seconds);
+ publish_persisted_zone_cycle(state, persist_zone_cycle(state, &zone, cycle_started_at)?)?;
+ continue;
+ }
+ if was_heating_sensor_guard_active {
+ state.log("info", "zone.heating_sensor_guard_released", &format!(
+ "Automatic heating sensor guard cleared for {} (sensor recovered or control configuration changed)", zone.name,
+ ), json!({"zone_id": zone.id, "room_sensor_status": zone.room_sensor_status}));
+ }
+
let Some(temp) = zone.current_temperature else {
record_zone_history(
state,
diff --git a/src/home_assistant.rs b/src/home_assistant.rs
index f6c08c2..a7b3544 100644
--- a/src/home_assistant.rs
+++ b/src/home_assistant.rs
@@ -227,17 +227,7 @@ pub async fn read_temperature(
.await
.context("invalid Home Assistant JSON")?;
if let Some(limit) = stale_after_seconds.filter(|value| *value > 0) {
- let updated = payload
- .get("last_updated")
- .and_then(Value::as_str)
- .ok_or_else(|| anyhow!("Home Assistant last_updated is missing"))?;
- let updated = chrono::DateTime::parse_from_rfc3339(updated)
- .context("invalid Home Assistant last_updated")?
- .with_timezone(&chrono::Utc);
- let age = chrono::Utc::now()
- .signed_duration_since(updated)
- .num_seconds()
- .max(0) as u64;
+ let age = temperature_sample_age_seconds(&payload, chrono::Utc::now())?;
if age > limit {
bail!("Home Assistant sensor is stale: {age}s old (limit {limit}s)")
}
@@ -256,9 +246,27 @@ pub async fn read_temperature(
if unit.eq_ignore_ascii_case("°F") || unit.eq_ignore_ascii_case("F") {
temperature = (temperature - 32.0) * 5.0 / 9.0;
}
+ if !temperature.is_finite() {
+ bail!("Home Assistant temperature must be finite")
+ }
Ok((temperature * 10.0).round() / 10.0)
}
+fn temperature_sample_age_seconds(payload: &Value, now: chrono::DateTime) -> Result {
+ // last_updated may stay unchanged while a sensor repeatedly reports the same
+ // temperature. Prefer last_reported (when HA provides it) for freshness,
+ // and retain last_updated compatibility with older HA versions.
+ let timestamp = payload
+ .get("last_reported")
+ .and_then(Value::as_str)
+ .or_else(|| payload.get("last_updated").and_then(Value::as_str))
+ .ok_or_else(|| anyhow!("Home Assistant sensor timestamp is missing"))?;
+ let reported_at = chrono::DateTime::parse_from_rfc3339(timestamp)
+ .context("invalid Home Assistant sensor timestamp")?
+ .with_timezone(&chrono::Utc);
+ Ok(now.signed_duration_since(reported_at).num_seconds().max(0) as u64)
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -279,6 +287,20 @@ mod tests {
}
}
+ #[test]
+ fn unchanged_temperature_can_be_fresh_when_last_reported_is_recent() {
+ let now = chrono::Utc::now();
+ let payload = serde_json::json!({
+ "last_updated": (now - chrono::Duration::minutes(16)).to_rfc3339(),
+ "last_reported": (now - chrono::Duration::seconds(45)).to_rfc3339(),
+ });
+ assert_eq!(temperature_sample_age_seconds(&payload, now).unwrap(), 45);
+ let old_payload = serde_json::json!({
+ "last_updated": (now - chrono::Duration::minutes(16)).to_rfc3339(),
+ });
+ assert_eq!(temperature_sample_age_seconds(&old_payload, now).unwrap(), 960);
+ }
+
#[test]
fn aliases_never_replace_real_home_assistant_entity_ids() {
let settings = settings();
diff --git a/src/models/zone.rs b/src/models/zone.rs
index b30c483..b511bbd 100644
--- a/src/models/zone.rs
+++ b/src/models/zone.rs
@@ -114,6 +114,14 @@ pub struct Zone {
/// `device`, `external`, `combined`, `device_fallback`, or `device_discrepancy_fallback`.
#[serde(default = "default_control_temperature_source")]
pub control_temperature_source: String,
+ /// Room sensor diagnostic from the latest zone cycle: not_used, ok, stale, error or discrepancy.
+ /// It is intentionally distinct from the selected control temperature source.
+ #[serde(default)]
+ pub room_sensor_status: String,
+ /// Automatic heating is suspended because a zone configured to use a room sensor
+ /// currently has no trustworthy room-sensor reading. Manual operation is unaffected.
+ #[serde(default)]
+ pub heating_sensor_guard_active: bool,
/// Effective profile currently used by the zone: comfort/sleep/away/custom.
#[serde(default = "default_active_preset")]
pub active_preset: String,
diff --git a/web/css/styles.css b/web/css/styles.css
index a2e2b9f..be77f11 100644
--- a/web/css/styles.css
+++ b/web/css/styles.css
@@ -5128,6 +5128,64 @@ body.simulation-standalone [data-view="simulation"] {
letter-spacing: -.03em;
}
+.zone-temperature-help {
+ margin-top: 5px;
+ min-width: 0;
+ font-size: 11px;
+}
+
+.zone-temperature-help summary {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ cursor: pointer;
+ list-style: none;
+ color: var(--accent);
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 1.4;
+}
+
+.zone-temperature-help summary::-webkit-details-marker { display: none; }
+
+.zone-temperature-help summary .ui-icon { width: 14px; height: 14px; flex: 0 0 auto; }
+
+.zone-temperature-help summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
+
+.zone-temperature-help-content {
+ margin-top: 6px;
+ padding: 9px 10px;
+ border: 1px solid var(--line);
+ border-radius: 9px;
+ background: var(--surface);
+ color: var(--text);
+ line-height: 1.5;
+ overflow-wrap: anywhere;
+ font-weight: 400;
+}
+
+.zone-temperature-help-content p { margin: 6px 0 0; }
+
+.zone-temperature-guard-detail { margin: 6px 0 0; color: var(--warning); }
+
+.zone-heating-sensor-guard {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ padding: 9px 10px;
+ margin: 0;
+ border: 1px solid var(--warning);
+ border-radius: 10px;
+ background: var(--warning-soft);
+ color: var(--text);
+ font-size: 11px;
+ line-height: 1.45;
+}
+
+.zone-heating-sensor-guard .ui-icon { width: 16px; height: 16px; flex: 0 0 auto; color: var(--warning); }
+
+.zone-heating-sensor-guard strong { font-weight: 700; }
+
.zone-config-runtime {
display: grid;
justify-items: end;
diff --git a/web/index.html b/web/index.html
index 7e9f079..6d3d046 100644
--- a/web/index.html
+++ b/web/index.html
@@ -1621,7 +1621,7 @@
data-i18n="zones.maxDifference">Max. sensor difference °C
This room sensor is required when the temperature strategy
- uses Home Assistant. If it becomes unavailable, the controller falls back to the GREE sensor.
+ uses Home Assistant. Missing or disagreeing readings pause automatic heating; manual control is unaffected.