This commit is contained in:
Mateusz Gruszczyński committed 2026-10-21 11:27:44 +02:00
1 parent bb5caa769f
commit a679eb5747
25 files changed
+484 -78

No files matched your search

Generated
+1 -1
View File
@@ -719,7 +719,7 @@ dependencies = [
[[package]] [[package]]
name = "gree-controller" name = "gree-controller"
version = "0.16.4" version = "0.16.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.16.4" version = "0.16.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"
+27 -26
View File
@@ -1,17 +1,17 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore 2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore
147eed97b308e0c27d6a5ee738a0ee710fa6ba2c6bbf7503df0fe484e0c7f842 ./Cargo.lock 795c232a7845a217a5711da4d9547282ba7f42fd160a249df07d631fac83928b ./Cargo.lock
c604af0060aef4e79d2e11fd1e1222a2444cebc835bc50629cff3d2373c5a05a ./Cargo.toml 2c57e2334ace9e7e320c97b14e1b0372c881717dad4bf3e6ef6abae252ed8056 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
75a6652611273ae458cb3509401cdb196d1084db1e49496ecdce448f490e6249 ./README.md eb8dd4d456419762d78e8d426326ac33fa99dbf857ab0f34f52dadc30e8a62be ./README.md
eedafc7faaa9eb95b564a249a4fef82d5f4cc32697eb0e370b96264f3c47b69c ./SOURCE_MANIFEST.sha256 c0f61039799416d2d72d37be85beabd1a68fc3325563189732027df31e7efc2b ./SOURCE_MANIFEST.sha256
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
bb9840a0dc2b89d057d1461ae581ee61ebf856e3b6d38bc7d1bdd111eef7c550 ./docs/API.md e961deaaa82be18f9e2933999dda33f55bfc648200e62043b5e8fffdfdd4464c ./docs/API.md
4f9826f1bf3b42ed2d62d8a8e9221b50175b3a9fdda7e9d219351f7a33039257 ./docs/FLOW.md 4f9826f1bf3b42ed2d62d8a8e9221b50175b3a9fdda7e9d219351f7a33039257 ./docs/FLOW.md
52c8dc48aed987fd28b2b543af1e5a782efb2a44104ccb0fd6b1740255cf384e ./docs/openapi.json 5c24d9f03e5025eda289e6862a7919fe3951992f7120647dc6e0cd0f1f9aa96f ./docs/openapi.json
d8a1b3e34cf8a79ac3296bf600ad47826d639cdef4c1c2c099f8b6b21a391ebd ./flows/simple-heating/igor-simple-heat-v5.flow.json a5ad73f7933031dfaa99aa92e27f10223d53fb84b14ee0a378eb5f10eb80e8c8 ./flows/simple-heating/igor-simple-heat-v5.flow.json
bd9a07b8ca4b090ad0089e9037492cc25c47f3456b97ec08152947314ea904a6 ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json 9743f4328a18bc9d89d10a9011d13ca201a4f352db2119335a66a2147a9a486d ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json
c4e6e3e8df5eb7f9730a79fb90edd155f0510f7bb4862a04b9f35ba649c22376 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json 2d804831a72577e80dc1ec3cf4f86dc8bcf00b1d8c80929896f11da0230b2b07 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example 77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile 0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md 0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
@@ -25,7 +25,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py 6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png 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 c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py 39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.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 a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json 50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.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 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.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 3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png 0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml 2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
@@ -46,8 +46,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
4bf92ee5627be67b66d76a6106b224159f95071f0aa2a727cd964e113bc3b32a ./lang/en.json f3c1286c3758a98b6a2945562753262e9083a6c638a1dcae556c3e978b4a69af ./lang/en.json
1e056cd915ba2e808db1287c99c06bbbbb3c98e5008481471b895e6ec20ea43a ./lang/pl.json ac6e6285a6153c5d8ce3c45d81ca6fe6c688505957e142e79e0ec0b496c400f3 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -103,6 +103,7 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js
e3825cfd9e6e149fd227c521d886dc116170f2553d4d7639d61874267f257b6e ./scripts/test_flow_disable_ui.js e3825cfd9e6e149fd227c521d886dc116170f2553d4d7639d61874267f257b6e ./scripts/test_flow_disable_ui.js
2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js 2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js
9bef00c3a0956c5c893eff6cf1fee9681140a1e76da630fa5a347bce19f07981 ./scripts/test_temperature_explanation_ui.js
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
c260a6d291ad913f6aca60f23a0acf6928d68cfb0545b2a698d58c5a95fcf934 ./scripts/verify_simple_heating.py c260a6d291ad913f6aca60f23a0acf6928d68cfb0545b2a698d58c5a95fcf934 ./scripts/verify_simple_heating.py
c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs
@@ -127,7 +128,7 @@ b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/publ
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs 7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs
3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs 3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs
eaa4070afbc19686d5e9bdb2d24fb2c7fc4bf1f90dc1c4f12fef17017d4cb975 ./src/api/zones.rs 79076544f4145c0eb120d544e9fab767b5c7ae625c2db007ee8be8a06bb0b85e ./src/api/zones.rs
32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs 32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs
de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs
87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs 87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs
@@ -146,7 +147,7 @@ f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_
e6d6bf39e254bd2779d3bcb31d966aa3f6688ebfa91b4088b70fd796cad66596 ./src/engine/automations.rs e6d6bf39e254bd2779d3bcb31d966aa3f6688ebfa91b4088b70fd796cad66596 ./src/engine/automations.rs
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs 1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs 357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
62a15ea757c53796149e670863dae1b4e1a16ceb6ef88ed84ebd3089c0b9880d ./src/engine/control_plan.rs c6b9651090e89fe0436826f66ae56825bf1e4b209ef8a27beb22fb1deaaa8c36 ./src/engine/control_plan.rs
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs 7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs 519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
@@ -158,13 +159,13 @@ c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/r
48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs 48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs
9adfcb3c3dffbaa870b314904bd1995e7b78c3f12febd69e23433179e4bf3fba ./src/engine/simple_heat.rs 9adfcb3c3dffbaa870b314904bd1995e7b78c3f12febd69e23433179e4bf3fba ./src/engine/simple_heat.rs
2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs 2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs
d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs b35d7e96e50d40bf26be2e47e1fa1d393f4c07caeac41f3595f5d595579e55f7 ./src/engine/temperature.rs
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs 833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
4160811f47eca6e75970fe6c6b95198f86a4b97b7050531c19a19efb3d76d97d ./src/engine/tests.rs f563f2da2409bf291d67daa1b2b2fd6c7a463a3a11343afb08131d20cd26cd69 ./src/engine/tests.rs
07c3c462884344f018654267de3d454d895343b4699e34635e4d1ec88546df0a ./src/engine/zone_actions.rs 63ded0ec62833699cbb7fcc6e4189ef19feafc23b9838d8fe9ba98889feb3e67 ./src/engine/zone_actions.rs
e246b6d6e3126941b989735a27e5fbe88dcaf351420aaaa060bb3761620d8244 ./src/engine/zone_control.rs 38256ec0e5ee735aa2f7ad7f8daaf2aa96077ea16a6c2cbceac43c4fe2bf8aa5 ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs 8e181f29119ef6c32ee3821fe220ade97298c105c681c41466af0c16e539b7db ./src/home_assistant.rs
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs
2d36405912646b30b1a1a37a7a4e18c2104ab22e865b593316a609dda884aec2 ./src/influxdb/query.rs 2d36405912646b30b1a1a37a7a4e18c2104ab22e865b593316a609dda884aec2 ./src/influxdb/query.rs
@@ -181,7 +182,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f
17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs 17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs 41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
79cb16a9f12e8a20fcf0acc8649d771b8a76a05503c0372443d29003536c1bcb ./src/models/zone.rs 2b861f3b37274f4d0b9713706ae959220935bdaf63afc513329d1c48ac0f4dd9 ./src/models/zone.rs
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs 4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs 7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs 578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
@@ -207,16 +208,16 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
b030e61b30435495f492e15fa9daf228968cc1de2982e8f40a626452dbf5b81a ./web/css/styles.css 49d7992d8ebaf0b1eddd6538479b00a750897bc0454f3daa37eb01ed914bea8b ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
bbe640e99447f632383b2eb4b52da580ab95d60cd45cda47f6fae1c2380e5b8a ./web/index.html bb1229db66ab2b510a44308e21796ba01f54c36238f5785ff7fc998dedd09c18 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js 07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js
ef20fb1e706105d0afdc2fa505d92ea52e43d93c9a687f2a09d381227a464564 ./web/js-dynamic/dashboard.js 1bf5b5e295f685b4c54064eb91220a50cb85264f7e90dfcd5bfe5c5e8ac111a2 ./web/js-dynamic/dashboard.js
3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./web/js-dynamic/entities.js 4ddfe365f66b968fae490b19ab321e3497f9fba4e220123edd0a4e0353254d97 ./web/js-dynamic/entities.js
bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js
3e2a5f1aab56e918624e6e7af064eecb9e94e554fd4d185715f8ecf3747a6895 ./web/js-dynamic/flows.js 3e2a5f1aab56e918624e6e7af064eecb9e94e554fd4d185715f8ecf3747a6895 ./web/js-dynamic/flows.js
1d54ae8919edb61414f9ffc46f85394b29c3c355827120264352cccb32f65e68 ./web/js-dynamic/forms.js 1d54ae8919edb61414f9ffc46f85394b29c3c355827120264352cccb32f65e68 ./web/js-dynamic/forms.js
+3 -3
View File
@@ -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. 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. > [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`. > 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, - `home_assistant` — Home Assistant room sensor,
- `combined` — weighted GREE + Home Assistant value. - `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. `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.
+26 -25
View File
@@ -1,16 +1,16 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore 2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore
147eed97b308e0c27d6a5ee738a0ee710fa6ba2c6bbf7503df0fe484e0c7f842 ./Cargo.lock 795c232a7845a217a5711da4d9547282ba7f42fd160a249df07d631fac83928b ./Cargo.lock
c604af0060aef4e79d2e11fd1e1222a2444cebc835bc50629cff3d2373c5a05a ./Cargo.toml 2c57e2334ace9e7e320c97b14e1b0372c881717dad4bf3e6ef6abae252ed8056 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
75a6652611273ae458cb3509401cdb196d1084db1e49496ecdce448f490e6249 ./README.md eb8dd4d456419762d78e8d426326ac33fa99dbf857ab0f34f52dadc30e8a62be ./README.md
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
bb9840a0dc2b89d057d1461ae581ee61ebf856e3b6d38bc7d1bdd111eef7c550 ./docs/API.md e961deaaa82be18f9e2933999dda33f55bfc648200e62043b5e8fffdfdd4464c ./docs/API.md
4f9826f1bf3b42ed2d62d8a8e9221b50175b3a9fdda7e9d219351f7a33039257 ./docs/FLOW.md 4f9826f1bf3b42ed2d62d8a8e9221b50175b3a9fdda7e9d219351f7a33039257 ./docs/FLOW.md
52c8dc48aed987fd28b2b543af1e5a782efb2a44104ccb0fd6b1740255cf384e ./docs/openapi.json 5c24d9f03e5025eda289e6862a7919fe3951992f7120647dc6e0cd0f1f9aa96f ./docs/openapi.json
d8a1b3e34cf8a79ac3296bf600ad47826d639cdef4c1c2c099f8b6b21a391ebd ./flows/simple-heating/igor-simple-heat-v5.flow.json a5ad73f7933031dfaa99aa92e27f10223d53fb84b14ee0a378eb5f10eb80e8c8 ./flows/simple-heating/igor-simple-heat-v5.flow.json
bd9a07b8ca4b090ad0089e9037492cc25c47f3456b97ec08152947314ea904a6 ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json 9743f4328a18bc9d89d10a9011d13ca201a4f352db2119335a66a2147a9a486d ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json
c4e6e3e8df5eb7f9730a79fb90edd155f0510f7bb4862a04b9f35ba649c22376 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json 2d804831a72577e80dc1ec3cf4f86dc8bcf00b1d8c80929896f11da0230b2b07 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example 77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile 0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md 0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
@@ -24,7 +24,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py 6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png 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 c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py 39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.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 a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json 50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.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 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.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 3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png 0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml 2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
@@ -45,8 +45,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
4bf92ee5627be67b66d76a6106b224159f95071f0aa2a727cd964e113bc3b32a ./lang/en.json f3c1286c3758a98b6a2945562753262e9083a6c638a1dcae556c3e978b4a69af ./lang/en.json
1e056cd915ba2e808db1287c99c06bbbbb3c98e5008481471b895e6ec20ea43a ./lang/pl.json ac6e6285a6153c5d8ce3c45d81ca6fe6c688505957e142e79e0ec0b496c400f3 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -102,6 +102,7 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js
e3825cfd9e6e149fd227c521d886dc116170f2553d4d7639d61874267f257b6e ./scripts/test_flow_disable_ui.js e3825cfd9e6e149fd227c521d886dc116170f2553d4d7639d61874267f257b6e ./scripts/test_flow_disable_ui.js
2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js 2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js
9bef00c3a0956c5c893eff6cf1fee9681140a1e76da630fa5a347bce19f07981 ./scripts/test_temperature_explanation_ui.js
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
c260a6d291ad913f6aca60f23a0acf6928d68cfb0545b2a698d58c5a95fcf934 ./scripts/verify_simple_heating.py c260a6d291ad913f6aca60f23a0acf6928d68cfb0545b2a698d58c5a95fcf934 ./scripts/verify_simple_heating.py
c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs
@@ -126,7 +127,7 @@ b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/publ
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs 7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs
3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs 3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs
eaa4070afbc19686d5e9bdb2d24fb2c7fc4bf1f90dc1c4f12fef17017d4cb975 ./src/api/zones.rs 79076544f4145c0eb120d544e9fab767b5c7ae625c2db007ee8be8a06bb0b85e ./src/api/zones.rs
32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs 32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs
de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs
87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs 87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs
@@ -145,7 +146,7 @@ f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_
e6d6bf39e254bd2779d3bcb31d966aa3f6688ebfa91b4088b70fd796cad66596 ./src/engine/automations.rs e6d6bf39e254bd2779d3bcb31d966aa3f6688ebfa91b4088b70fd796cad66596 ./src/engine/automations.rs
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs 1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs 357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
62a15ea757c53796149e670863dae1b4e1a16ceb6ef88ed84ebd3089c0b9880d ./src/engine/control_plan.rs c6b9651090e89fe0436826f66ae56825bf1e4b209ef8a27beb22fb1deaaa8c36 ./src/engine/control_plan.rs
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs 7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs 519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
@@ -157,13 +158,13 @@ c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/r
48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs 48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs
9adfcb3c3dffbaa870b314904bd1995e7b78c3f12febd69e23433179e4bf3fba ./src/engine/simple_heat.rs 9adfcb3c3dffbaa870b314904bd1995e7b78c3f12febd69e23433179e4bf3fba ./src/engine/simple_heat.rs
2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs 2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs
d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs b35d7e96e50d40bf26be2e47e1fa1d393f4c07caeac41f3595f5d595579e55f7 ./src/engine/temperature.rs
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs 833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
4160811f47eca6e75970fe6c6b95198f86a4b97b7050531c19a19efb3d76d97d ./src/engine/tests.rs f563f2da2409bf291d67daa1b2b2fd6c7a463a3a11343afb08131d20cd26cd69 ./src/engine/tests.rs
07c3c462884344f018654267de3d454d895343b4699e34635e4d1ec88546df0a ./src/engine/zone_actions.rs 63ded0ec62833699cbb7fcc6e4189ef19feafc23b9838d8fe9ba98889feb3e67 ./src/engine/zone_actions.rs
e246b6d6e3126941b989735a27e5fbe88dcaf351420aaaa060bb3761620d8244 ./src/engine/zone_control.rs 38256ec0e5ee735aa2f7ad7f8daaf2aa96077ea16a6c2cbceac43c4fe2bf8aa5 ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs 8e181f29119ef6c32ee3821fe220ade97298c105c681c41466af0c16e539b7db ./src/home_assistant.rs
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs
2d36405912646b30b1a1a37a7a4e18c2104ab22e865b593316a609dda884aec2 ./src/influxdb/query.rs 2d36405912646b30b1a1a37a7a4e18c2104ab22e865b593316a609dda884aec2 ./src/influxdb/query.rs
@@ -180,7 +181,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f
17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs 17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs 41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
79cb16a9f12e8a20fcf0acc8649d771b8a76a05503c0372443d29003536c1bcb ./src/models/zone.rs 2b861f3b37274f4d0b9713706ae959220935bdaf63afc513329d1c48ac0f4dd9 ./src/models/zone.rs
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs 4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs 7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs 578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
@@ -206,16 +207,16 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
b030e61b30435495f492e15fa9daf228968cc1de2982e8f40a626452dbf5b81a ./web/css/styles.css 49d7992d8ebaf0b1eddd6538479b00a750897bc0454f3daa37eb01ed914bea8b ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
bbe640e99447f632383b2eb4b52da580ab95d60cd45cda47f6fae1c2380e5b8a ./web/index.html bb1229db66ab2b510a44308e21796ba01f54c36238f5785ff7fc998dedd09c18 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js 07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js
ef20fb1e706105d0afdc2fa505d92ea52e43d93c9a687f2a09d381227a464564 ./web/js-dynamic/dashboard.js 1bf5b5e295f685b4c54064eb91220a50cb85264f7e90dfcd5bfe5c5e8ac111a2 ./web/js-dynamic/dashboard.js
3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./web/js-dynamic/entities.js 4ddfe365f66b968fae490b19ab321e3497f9fba4e220123edd0a4e0353254d97 ./web/js-dynamic/entities.js
bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js
3e2a5f1aab56e918624e6e7af064eecb9e94e554fd4d185715f8ecf3747a6895 ./web/js-dynamic/flows.js 3e2a5f1aab56e918624e6e7af064eecb9e94e554fd4d185715f8ecf3747a6895 ./web/js-dynamic/flows.js
1d54ae8919edb61414f9ffc46f85394b29c3c355827120264352cccb32f65e68 ./web/js-dynamic/forms.js 1d54ae8919edb61414f9ffc46f85394b29c3c355827120264352cccb32f65e68 ./web/js-dynamic/forms.js
+8
View File
@@ -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`. 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` ### `GET /api/zones`
Returns `Zone[]`. Returns `Zone[]`.
+15 -2
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0", "openapi": "3.1.0",
"info": { "info": {
"title": "GREE Controller API", "title": "GREE Controller API",
"version": "0.16.4", "version": "0.16.5",
"summary": "Local HTTP/WebSocket API for GREE Controller", "summary": "Local HTTP/WebSocket API for GREE Controller",
"description": "Local API used by the GREE Controller Web UI and Home Assistant integration.", "description": "Local API used by the GREE Controller Web UI and Home Assistant integration.",
"license": { "license": {
@@ -6079,7 +6079,7 @@
}, },
"version": { "version": {
"type": "string", "type": "string",
"example": "0.16.4" "example": "0.16.5"
}, },
"uptime_seconds": { "uptime_seconds": {
"type": "integer", "type": "integer",
@@ -7267,6 +7267,19 @@
"null" "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": { "effective_mode": {
"type": "string" "type": "string"
}, },
@@ -1,7 +1,7 @@
{ {
"domain": "gree_controller", "domain": "gree_controller",
"name": "GREE Controller", "name": "GREE Controller",
"version": "0.16.4", "version": "0.16.5",
"config_flow": true, "config_flow": true,
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
@@ -1,5 +1,13 @@
# Changelog # 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 ## 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. - Fixes a race in stateful Flow checkpoint saving: no stale condition evaluation can re-enable, overwrite or resurrect a disabled/deleted/recompiled automation.
@@ -1,5 +1,5 @@
name: "GREE Controller" name: "GREE Controller"
version: "0.16.4" version: "0.16.5"
slug: "gree_controller" slug: "gree_controller"
description: "Local GREE HVAC controller with Web UI, API, and Home Assistant integration" description: "Local GREE HVAC controller with Web UI, API, and Home Assistant integration"
url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/" url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/"
+20 -1
View File
@@ -132,7 +132,7 @@
"zones.roomSensorEntity": "Room sensor entity_id", "zones.roomSensorEntity": "Room sensor entity_id",
"zones.roomSensorWeight": "Room sensor weight %", "zones.roomSensorWeight": "Room sensor weight %",
"zones.maxDifference": "Max. sensor difference °C", "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.sensorRequired": "Enter the Home Assistant room temperature entity_id for this zone.",
"zones.outdoorSensorEntity": "Outdoor temperature entity_id override", "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.", "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.sourceFallback": "GREE fallback",
"zones.sourceDiscrepancy": "GREE fallback: sensor mismatch", "zones.sourceDiscrepancy": "GREE fallback: sensor mismatch",
"zones.sourceUnavailable": "unavailable", "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.calendar": "Calendar",
"schedules.weekdays": "Weekdays", "schedules.weekdays": "Weekdays",
"schedules.crossMidnight": "Intervals crossing midnight are supported", "schedules.crossMidnight": "Intervals crossing midnight are supported",
+20 -1
View File
@@ -132,7 +132,7 @@
"zones.roomSensorEntity": "entity_id czujnika temperatury pokojowej", "zones.roomSensorEntity": "entity_id czujnika temperatury pokojowej",
"zones.roomSensorWeight": "Waga czujnika pokojowego %", "zones.roomSensorWeight": "Waga czujnika pokojowego %",
"zones.maxDifference": "Maks. różnica czujników °C", "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.sensorRequired": "Podaj entity_id czujnika temperatury pokojowej z Home Assistant dla tej strefy.",
"zones.outdoorSensorEntity": "Nadpisanie entity_id temperatury zewnętrznej", "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.", "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.sourceFallback": "fallback GREE",
"zones.sourceDiscrepancy": "fallback GREE: rozbieżność", "zones.sourceDiscrepancy": "fallback GREE: rozbieżność",
"zones.sourceUnavailable": "niedostępna", "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.calendar": "Kalendarz",
"schedules.weekdays": "Dni tygodnia", "schedules.weekdays": "Dni tygodnia",
"schedules.crossMidnight": "Obsługa przedziałów przez północ", "schedules.crossMidnight": "Obsługa przedziałów przez północ",
@@ -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 => `<icon name="${name}"></icon>`,
esc: value => String(value ?? '').replace(/[&<>'"]/g, x => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' })[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, /<details/);
assert.match(help, /Skąd ta temperatura/);
assert.match(help, /aria-label=/);
assert.match(ctx.zoneTemperatureExplanation({ ...combined, control_temperature_source: 'external' }), /21\.0°C pochodzi z czujnika pokojowego/);
assert.match(ctx.zoneTemperatureExplanation({ ...combined, control_temperature_source: 'device' }), /19\.0°C pochodzi wyłącznie z GREE/);
const stale = {
...combined, control_temperature_source: 'device_fallback', room_sensor_status: 'stale',
external_temperature: null, current_temperature: 19, heating_sensor_guard_active: true,
};
assert.match(ctx.zoneTemperatureExplanation(stale), /nieaktualny/);
assert.match(ctx.zoneTemperatureHelp(stale), /nie uruchomi ani nie zwiększy grzania/);
assert.match(ctx.zoneHeatingGuardNotice(stale, { power: true, mode: 'heat' }), /ochrony sprężarki/);
assert.match(ctx.zoneHeatingGuardNotice(stale, { power: true, mode: 'auto' }), /ochrony sprężarki/);
assert.equal(ctx.zoneRuntimeStatusLabel(stale, 'heat', { power: true }), dict['zones.heatGuardActive']);
const mismatch = { ...stale, room_sensor_status: 'discrepancy', control_temperature_source: 'device_discrepancy_fallback', external_temperature: 23, max_sensor_difference: 3 };
assert.match(ctx.zoneTemperatureExplanation(mismatch), /więcej niż 3\.0°C/);
assert.equal(ctx.zoneHeatingGuardNotice({ ...combined, heating_sensor_guard_active: false }, null), '');
console.log('OK: weighted formula, control sources, stale/mismatch warning and automatic heat safeguard UI');
+4
View File
@@ -230,6 +230,8 @@ impl ZoneInput {
external_temperature: None, external_temperature: None,
current_temperature: None, current_temperature: None,
control_temperature_source: "device".into(), control_temperature_source: "device".into(),
room_sensor_status: "not_used".into(),
heating_sensor_guard_active: false,
active_preset: "comfort".into(), active_preset: "comfort".into(),
manual_preset: None, manual_preset: None,
manual_setpoint: None, manual_setpoint: None,
@@ -385,6 +387,8 @@ async fn update_zone(
zone.external_temperature = existing.external_temperature; zone.external_temperature = existing.external_temperature;
zone.current_temperature = existing.current_temperature; zone.current_temperature = existing.current_temperature;
zone.control_temperature_source = existing.control_temperature_source; zone.control_temperature_source = existing.control_temperature_source;
zone.room_sensor_status = existing.room_sensor_status;
zone.heating_sensor_guard_active = existing.heating_sensor_guard_active;
zone.active_preset = existing.active_preset; zone.active_preset = existing.active_preset;
zone.simple_heat = existing.simple_heat.clone(); zone.simple_heat = existing.simple_heat.clone();
zone.simple_heat_runtime = existing.simple_heat_runtime.clone(); zone.simple_heat_runtime = existing.simple_heat_runtime.clone();
+5 -1
View File
@@ -222,7 +222,7 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
device_setpoint: device device_setpoint: device
.filter(|item| item.power) .filter(|item| item.power)
.map(|item| item.target_temperature), .map(|item| item.target_temperature),
desired_power: automatic_active && !simple_day_off, desired_power: automatic_active && !simple_day_off && !zone.heating_sensor_guard_active,
desired_mode: effective_mode.to_string(), desired_mode: effective_mode.to_string(),
actual_power: device.map(|item| item.power), actual_power: device.map(|item| item.power),
actual_mode: device.map(|item| { actual_mode: device.map(|item| {
@@ -248,6 +248,8 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
resume_at: zone.control_resume_at, resume_at: zone.control_resume_at,
control_reason: if emergency_stop_active { control_reason: if emergency_stop_active {
"Emergency stop pauses automatic climate control".into() "Emergency stop pauses automatic climate control".into()
} else if zone.heating_sensor_guard_active {
format!("Automatic heating paused: room sensor {}", zone.room_sensor_status)
} else if automation_idle { } else if automation_idle {
"Automation idle: no active schedule or explicit thermostat request".into() "Automation idle: no active schedule or explicit thermostat request".into()
} else { } else {
@@ -274,6 +276,8 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
.unwrap_or(true) .unwrap_or(true)
{ {
Some("offline".into()) Some("offline".into())
} else if zone.heating_sensor_guard_active {
Some("heating_room_sensor_guard".into())
} else if automation_idle { } else if automation_idle {
Some("no_active_thermostat_intent".into()) Some("no_active_thermostat_intent".into())
} else { } else {
+12
View File
@@ -55,6 +55,18 @@ fn select_zone_temperature(
} }
} }
/// Never infer a new AUTOMATIC heating demand from GREE when the room sensor
/// selected by the user is stale, missing or disagrees beyond the configured limit.
/// A valid HA-only reading is fine even if GREE is offline. Explicit manual
/// ownership and GREE-only zones keep their existing semantics.
fn automatic_heating_needs_room_sensor_guard(zone: &Zone, mode: &str) -> 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 { fn adjustment_allowed(zone: &Zone) -> bool {
let Some(last) = zone.last_action_at else { let Some(last) = zone.last_action_at else {
return true; return true;
+41
View File
@@ -770,6 +770,8 @@ mod tests {
external_temperature: None, external_temperature: None,
current_temperature: None, current_temperature: None,
control_temperature_source: "device".into(), control_temperature_source: "device".into(),
room_sensor_status: "not_used".into(),
heating_sensor_guard_active: false,
active_preset: "comfort".into(), active_preset: "comfort".into(),
manual_preset: None, manual_preset: None,
manual_setpoint: None, manual_setpoint: None,
@@ -1502,6 +1504,45 @@ mod tests {
assert!(!discrepancy); 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] #[test]
fn seasonal_profiles_resolve_independently() { fn seasonal_profiles_resolve_independently() {
let zone = test_zone("device"); let zone = test_zone("device");
+4
View File
@@ -37,6 +37,7 @@ fn zone_runtime_change_affects_control_plan(before: &Zone, after: &Zone) -> bool
"heat_away_setpoint": zone.heat_away_setpoint, "heat_away_setpoint": zone.heat_away_setpoint,
"current_temperature": zone.current_temperature, "current_temperature": zone.current_temperature,
"control_temperature_source": zone.control_temperature_source, "control_temperature_source": zone.control_temperature_source,
"heating_sensor_guard_active": zone.heating_sensor_guard_active,
"manual_preset": zone.manual_preset, "manual_preset": zone.manual_preset,
"manual_setpoint": zone.manual_setpoint, "manual_setpoint": zone.manual_setpoint,
"manual_override_until": zone.manual_override_until, "manual_override_until": zone.manual_override_until,
@@ -111,6 +112,9 @@ fn persist_zone_cycle(
latest.external_temperature = computed.external_temperature; latest.external_temperature = computed.external_temperature;
latest.current_temperature = computed.current_temperature; latest.current_temperature = computed.current_temperature;
latest.control_temperature_source = computed.control_temperature_source.clone(); 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(); latest.updated_at = Utc::now();
state.db.save_zone(&latest)?; state.db.save_zone(&latest)?;
} }
+77
View File
@@ -206,6 +206,11 @@ fn refresh_zone_temperature(
room_sensor_results: &mut HashMap<String, (Option<String>, Result<f64, String>)>, room_sensor_results: &mut HashMap<String, (Option<String>, Result<f64, String>)>,
) -> (String, bool) { ) -> (String, bool) {
let previous_source = zone.control_temperature_source.clone(); 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 = let device_temperature =
if device.enabled && device.online && device.communication_failures == 0 { if device.enabled && device.online && device.communication_failures == 0 {
device.current_temperature device.current_temperature
@@ -216,6 +221,7 @@ fn refresh_zone_temperature(
if matches!(zone.sensor_source.as_str(), "home_assistant" | "combined") { if matches!(zone.sensor_source.as_str(), "home_assistant" | "combined") {
match room_sensor_results.remove(&zone.id) { match room_sensor_results.remove(&zone.id) {
Some((resolved_entity, Ok(value))) => { Some((resolved_entity, Ok(value))) => {
room_sensor_status = "ok";
if let Some(entity_id) = resolved_entity.as_deref() { if let Some(entity_id) = resolved_entity.as_deref() {
record_ha_history( record_ha_history(
state, state,
@@ -229,6 +235,11 @@ fn refresh_zone_temperature(
Some(value) Some(value)
} }
Some((resolved_entity, Err(err))) => { Some((resolved_entity, Err(err))) => {
room_sensor_status = if err.contains("Home Assistant sensor is stale:") {
"stale"
} else {
"error"
};
if !matches!( if !matches!(
previous_source.as_str(), previous_source.as_str(),
"device_fallback" | "device_discrepancy_fallback" "device_fallback" | "device_discrepancy_fallback"
@@ -262,10 +273,14 @@ fn refresh_zone_temperature(
}; };
let (temperature, source, discrepancy) = let (temperature, source, discrepancy) =
select_zone_temperature(zone, device_temperature, external_temperature); select_zone_temperature(zone, device_temperature, external_temperature);
if discrepancy {
room_sensor_status = "discrepancy";
}
zone.device_temperature = device_temperature; zone.device_temperature = device_temperature;
zone.external_temperature = external_temperature; zone.external_temperature = external_temperature;
zone.current_temperature = temperature; zone.current_temperature = temperature;
zone.control_temperature_source = source; zone.control_temperature_source = source;
zone.room_sensor_status = room_sensor_status.into();
zone.updated_at = Utc::now(); zone.updated_at = Utc::now();
(previous_source, discrepancy) (previous_source, discrepancy)
} }
@@ -743,6 +758,8 @@ async fn control_zones(state: &AppState) -> Result<()> {
refresh_control_ownership(&mut zone); refresh_control_ownership(&mut zone);
let effective_mode = effective_mode_owned.as_str(); 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( let (previous_source, discrepancy) = refresh_zone_temperature(
state, state,
&settings, &settings,
@@ -902,6 +919,66 @@ async fn control_zones(state: &AppState) -> Result<()> {
zone.active_preset = preset; zone.active_preset = preset;
zone.effective_setpoint = Some(target); 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 { let Some(temp) = zone.current_temperature else {
record_zone_history( record_zone_history(
state, state,
+33 -11
View File
@@ -227,17 +227,7 @@ pub async fn read_temperature(
.await .await
.context("invalid Home Assistant JSON")?; .context("invalid Home Assistant JSON")?;
if let Some(limit) = stale_after_seconds.filter(|value| *value > 0) { if let Some(limit) = stale_after_seconds.filter(|value| *value > 0) {
let updated = payload let age = temperature_sample_age_seconds(&payload, chrono::Utc::now())?;
.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;
if age > limit { if age > limit {
bail!("Home Assistant sensor is stale: {age}s old (limit {limit}s)") 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") { if unit.eq_ignore_ascii_case("°F") || unit.eq_ignore_ascii_case("F") {
temperature = (temperature - 32.0) * 5.0 / 9.0; 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) Ok((temperature * 10.0).round() / 10.0)
} }
fn temperature_sample_age_seconds(payload: &Value, now: chrono::DateTime<chrono::Utc>) -> Result<u64> {
// 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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] #[test]
fn aliases_never_replace_real_home_assistant_entity_ids() { fn aliases_never_replace_real_home_assistant_entity_ids() {
let settings = settings(); let settings = settings();
+8
View File
@@ -114,6 +114,14 @@ pub struct Zone {
/// `device`, `external`, `combined`, `device_fallback`, or `device_discrepancy_fallback`. /// `device`, `external`, `combined`, `device_fallback`, or `device_discrepancy_fallback`.
#[serde(default = "default_control_temperature_source")] #[serde(default = "default_control_temperature_source")]
pub control_temperature_source: String, 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. /// Effective profile currently used by the zone: comfort/sleep/away/custom.
#[serde(default = "default_active_preset")] #[serde(default = "default_active_preset")]
pub active_preset: String, pub active_preset: String,
+58
View File
@@ -5128,6 +5128,64 @@ body.simulation-standalone [data-view="simulation"] {
letter-spacing: -.03em; 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 { .zone-config-runtime {
display: grid; display: grid;
justify-items: end; justify-items: end;
+1 -1
View File
@@ -1621,7 +1621,7 @@
data-i18n="zones.maxDifference">Max. sensor difference °C</span><input type="text" inputmode="decimal" data-i18n="zones.maxDifference">Max. sensor difference °C</span><input type="text" inputmode="decimal"
step="0.1" min="0.1" max="20" name="max_sensor_difference" value="3.0"></label></div> step="0.1" min="0.1" max="20" name="max_sensor_difference" value="3.0"></label></div>
<p class="field-note" data-i18n="zones.sensorHelp">This room sensor is required when the temperature strategy <p class="field-note" data-i18n="zones.sensorHelp">This room sensor is required when the temperature strategy
uses Home Assistant. If it becomes unavailable, the controller falls back to the GREE sensor.</p> uses Home Assistant. Missing or disagreeing readings pause automatic heating; manual control is unaffected.</p>
</div> </div>
<label><span data-i18n="zones.outdoorSensorEntity">Outdoor temperature entity_id override</span><input <label><span data-i18n="zones.outdoorSensorEntity">Outdoor temperature entity_id override</span><input
name="ha_outdoor_entity_id" list="haEntitySuggestions" placeholder="sensor.outdoor_temperature"></label> name="ha_outdoor_entity_id" list="haEntitySuggestions" placeholder="sensor.outdoor_temperature"></label>
+1
View File
@@ -81,6 +81,7 @@ function planBlockedReasonLabel(reason) {
manual_override: 'plan.reasonManual', manual_override: 'plan.reasonManual',
zone_disabled: 'plan.reasonDisabled', zone_disabled: 'plan.reasonDisabled',
offline: 'plan.reasonOffline', offline: 'plan.reasonOffline',
heating_room_sensor_guard: 'plan.reasonRoomSensorGuard',
no_active_thermostat_intent: 'plan.reasonNoIntent', no_active_thermostat_intent: 'plan.reasonNoIntent',
}[reason]; }[reason];
return key ? tr(key) : reason; return key ? tr(key) : reason;
+60 -3
View File
@@ -331,6 +331,59 @@ function zoneControlSourceLabel(source) {
})[source] || source || tr('zones.sourceUnavailable'); })[source] || source || tr('zones.sourceUnavailable');
} }
function zoneTemperatureExplanation(zone) {
const gree = fmtTemp(zone.device_temperature);
const room = fmtTemp(zone.external_temperature);
const used = fmtTemp(zone.current_temperature);
const source = zone.control_temperature_source;
if (source === 'combined' && zone.device_temperature != null && zone.external_temperature != null) {
const roomWeight = Math.round(Math.max(0, Math.min(1, Number(zone.external_sensor_weight ?? 0.4))) * 100);
return tr('zones.temperatureFormula', { gree, room, used, greeWeight: 100 - roomWeight, roomWeight });
}
if (source === 'external') {
return tr('zones.temperatureFromRoom', { room, gree });
}
if (source === 'device') {
return tr('zones.temperatureFromGree', { gree });
}
if (source === 'device_discrepancy_fallback') {
return tr('zones.temperatureDiscrepancy', { gree, room, limit: fmtTemp(zone.max_sensor_difference) });
}
if (source === 'device_fallback') {
const reason = zone.room_sensor_status === 'stale'
? tr('zones.sensorStale')
: tr('zones.sensorUnavailable');
return tr('zones.temperatureFallback', { gree, reason });
}
return tr('zones.temperatureUnavailable');
}
function zoneTemperatureHelp(zone) {
const explanation = zoneTemperatureExplanation(zone);
const guarded = zone.heating_sensor_guard_active === true;
const guardDetail = guarded ? tr('zones.heatGuardExplanation') : '';
const sensorStatus = zoneRoomSensorStatusText(zone);
const tooltip = [explanation, sensorStatus, guardDetail].filter(Boolean).join(' ');
return `<details class="zone-temperature-help"><summary title="${esc(tooltip)}" aria-label="${esc(tr('zones.temperatureHelp'))}">${uiIcon('system-info')}<span>${esc(tr('zones.temperatureHelp'))}</span></summary><div class="zone-temperature-help-content">${esc(explanation)}${sensorStatus ? `<p>${esc(sensorStatus)}</p>` : ''}${guarded ? `<p class="zone-temperature-guard-detail">${esc(guardDetail)}</p>` : ''}</div></details>`;
}
function zoneRoomSensorStatusText(zone) {
if (!zone || zone.sensor_source === 'device') return '';
const key = {
ok: 'zones.sensorStatusOk',
stale: 'zones.sensorStatusStale',
error: 'zones.sensorStatusError',
discrepancy: 'zones.sensorStatusDiscrepancy',
}[zone.room_sensor_status];
return key ? tr('zones.sensorStatusLabel', { status: tr(key) }) : '';
}
function zoneHeatingGuardNotice(zone, device) {
if (zone.heating_sensor_guard_active !== true) return '';
const pending = device?.power && ['heat', 'auto'].includes(device?.mode);
return `<p class="zone-heating-sensor-guard" role="status">${uiIcon('system-info')}<span><strong>${esc(tr('zones.heatGuardActive'))}</strong> ${esc(tr(pending ? 'zones.heatGuardPendingOff' : 'zones.heatGuardHint'))}</span></p>`;
}
function zonePresetLabel(preset) { function zonePresetLabel(preset) {
const key = `preset.${preset || 'comfort'}`; const key = `preset.${preset || 'comfort'}`;
return tr(key) === key ? (preset || 'comfort') : tr(key); return tr(key) === key ? (preset || 'comfort') : tr(key);
@@ -471,6 +524,7 @@ function zoneRuntimeStatusLabel(zone, effectiveMode, device = null) {
if (zone.device_manual_override === true) return tr('zones.manualDeviceControl'); if (zone.device_manual_override === true) return tr('zones.manualDeviceControl');
if (!zone.enabled) return tr('common.disabled'); if (!zone.enabled) return tr('common.disabled');
if (zone.local_thermostat_power === false) return tr('zones.localThermostatOff'); if (zone.local_thermostat_power === false) return tr('zones.localThermostatOff');
if (zone.heating_sensor_guard_active === true) return tr('zones.heatGuardActive');
if ((effectiveMode || 'off') === 'off') { if ((effectiveMode || 'off') === 'off') {
if (zone.control_source === 'automation.flow' && zone.simple_heat && zone.control_owner === 'automation') return tr(device?.power ? 'zones.flowDayPendingOff' : 'zones.flowDayOff'); if (zone.control_source === 'automation.flow' && zone.simple_heat && zone.control_owner === 'automation') return tr(device?.power ? 'zones.flowDayPendingOff' : 'zones.flowDayOff');
return tr('zones.waiting'); return tr('zones.waiting');
@@ -494,8 +548,9 @@ function zoneCard(zone, detailed = true) {
const state = zone.enabled ? tr('common.active') : tr('common.disabled'); const state = zone.enabled ? tr('common.active') : tr('common.disabled');
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)}${zoneRoomSensorStatusText(zone) ? ` · ${zoneRoomSensorStatusText(zone)}` : ''}`;
const roomTemperature = zone.current_temperature ?? zone.device_temperature ?? device?.current_temperature; const roomTemperature = zone.current_temperature ?? zone.device_temperature ?? device?.current_temperature;
const temperatureTooltip = zoneTemperatureExplanation(zone);
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;
const effectiveMode = zone.effective_mode || (zone.inherit_house_mode ? (app.settings?.house_mode || 'off') : zone.mode) || 'off'; const effectiveMode = zone.effective_mode || (zone.inherit_house_mode ? (app.settings?.house_mode || 'off') : zone.mode) || 'off';
@@ -556,7 +611,7 @@ function zoneCard(zone, detailed = true) {
return `<article class="list-card zone-config-card ${workingDemand ? 'demanding' : ''} ${waitingDemand ? 'lockout-waiting' : ''} ${zone.enabled ? '' : 'zone-disabled'} ${deviceManualOverride ? 'manual-takeover' : ''} ${controlGroup ? 'group-controlled' : ''}" data-zone-config="${esc(zone.id)}"${groupControlStyle(visualGroup)}> return `<article class="list-card zone-config-card ${workingDemand ? 'demanding' : ''} ${waitingDemand ? 'lockout-waiting' : ''} ${zone.enabled ? '' : 'zone-disabled'} ${deviceManualOverride ? 'manual-takeover' : ''} ${controlGroup ? 'group-controlled' : ''}" data-zone-config="${esc(zone.id)}"${groupControlStyle(visualGroup)}>
<div class="list-card-head"><div><span class="eyebrow">${esc(tr('zones.configuration'))}</span><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(tr('groups.group'))}: ${esc(groupText)}</p></div><button type="button" class="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'))}" title="${esc(tr('zones.automationToggleHint'))}"><span>${uiIcon(zone.enabled ? 'check' : 'circle')}</span>${esc(state)}</button></div> <div class="list-card-head"><div><span class="eyebrow">${esc(tr('zones.configuration'))}</span><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(tr('groups.group'))}: ${esc(groupText)}</p></div><button type="button" class="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'))}" title="${esc(tr('zones.automationToggleHint'))}"><span>${uiIcon(zone.enabled ? 'check' : 'circle')}</span>${esc(state)}</button></div>
<div class="zone-config-status"> <div class="zone-config-status">
<div class="zone-config-temperature"><small>${esc(tr('zones.currentStatus'))}</small><div><span>${fmtTemp(roomTemperature)}</span><b>→</b><strong>${Number.isFinite(target) ? `${target.toFixed(1)}°C` : '—'}</strong></div></div> <div class="zone-config-temperature"><small>${esc(tr('zones.currentStatus'))}</small><div><span title="${esc(temperatureTooltip)}">${fmtTemp(roomTemperature)}</span><b>→</b><strong>${Number.isFinite(target) ? `${target.toFixed(1)}°C` : '—'}</strong></div>${zoneTemperatureHelp(zone)}</div>
<div class="zone-config-runtime"><span class="badge ${workingDemand ? 'active' : ''}">${esc(zoneRuntimeStatusLabel(zone, effectiveMode, device))}</span><span>${esc(houseModeLabel(effectiveMode))} · ${esc(zonePresetLabel(displayPreset))}</span><small>${esc(override)}</small><small><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}${zoneControlOwnerMeta(zone) ? ` · ${esc(zoneControlOwnerMeta(zone))}` : ''}</small></div> <div class="zone-config-runtime"><span class="badge ${workingDemand ? 'active' : ''}">${esc(zoneRuntimeStatusLabel(zone, effectiveMode, device))}</span><span>${esc(houseModeLabel(effectiveMode))} · ${esc(zonePresetLabel(displayPreset))}</span><small>${esc(override)}</small><small><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}${zoneControlOwnerMeta(zone) ? ` · ${esc(zoneControlOwnerMeta(zone))}` : ''}</small></div>
</div> </div>
<div class="zone-config-grid"> <div class="zone-config-grid">
@@ -567,6 +622,7 @@ function zoneCard(zone, detailed = true) {
<div><small>${esc(tr('zones.heatAnticipation'))}</small><strong>${Number(zone.heat_anticipation_c ?? 0).toFixed(1)}°C</strong></div> <div><small>${esc(tr('zones.heatAnticipation'))}</small><strong>${Number(zone.heat_anticipation_c ?? 0).toFixed(1)}°C</strong></div>
</div> </div>
<div class="sensor-detail">${esc(sensorDetails)}</div> <div class="sensor-detail">${esc(sensorDetails)}</div>
${zoneHeatingGuardNotice(zone, device)}
${compressorQueuePanel(zone)} ${compressorQueuePanel(zone)}
${manualTakeover} ${manualTakeover}
<div class="card-footer"><small>${esc(tr('zones.controlOnDashboard'))}</small><div class="card-menu"><button class="primary" data-action="zone-go-control" data-id="${esc(zone.id)}">${esc(tr('zones.controlNow'))}</button><button data-action="edit-zone" data-id="${esc(zone.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-zone" data-id="${esc(zone.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div> <div class="card-footer"><small>${esc(tr('zones.controlOnDashboard'))}</small><div class="card-menu"><button class="primary" data-action="zone-go-control" data-id="${esc(zone.id)}">${esc(tr('zones.controlNow'))}</button><button data-action="edit-zone" data-id="${esc(zone.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-zone" data-id="${esc(zone.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
@@ -577,7 +633,8 @@ function zoneCard(zone, detailed = true) {
return `<article class="list-card zone-thermostat quick-control-card quick-thermostat-control ${workingDemand ? 'demanding' : ''} ${waitingDemand ? 'lockout-waiting' : ''} ${zone.enabled ? '' : 'zone-disabled'} ${deviceManualOverride ? 'manual-takeover' : ''} ${controlGroup ? 'group-controlled' : ''}" data-zone-card="${esc(zone.id)}"${groupControlStyle(visualGroup)}> return `<article class="list-card zone-thermostat quick-control-card quick-thermostat-control ${workingDemand ? 'demanding' : ''} ${waitingDemand ? 'lockout-waiting' : ''} ${zone.enabled ? '' : 'zone-disabled'} ${deviceManualOverride ? 'manual-takeover' : ''} ${controlGroup ? 'group-controlled' : ''}" data-zone-card="${esc(zone.id)}"${groupControlStyle(visualGroup)}>
<div class="list-card-head"><div><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(displayPreset))}</p></div><div class="zone-quick-actions">${localThermostatPowerControl}${temporaryThermostatControl}</div></div> <div class="list-card-head"><div><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(displayPreset))}</p></div><div class="zone-quick-actions">${localThermostatPowerControl}${temporaryThermostatControl}</div></div>
${disabledManualControlNotice} ${disabledManualControlNotice}
<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">${uiIcon('minus')}</button><div class="target-temp compact editable-target" data-temperature-kind="zone" data-id="${esc(zone.id)}" data-value="${Number.isFinite(target) ? target : ''}" tabindex="0" role="button" title="${esc(tr('common.clickTemperatureToEdit'))}">${Number.isFinite(target) ? target.toFixed(1) : '--'}<small>°C</small></div><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="0.5">${uiIcon('plus')}</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 title="${esc(temperatureTooltip)}">${fmtTemp(roomTemperature)}</strong>${zoneTemperatureHelp(zone)}</div><div class="temperature-control compact"><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="-0.5">${uiIcon('minus')}</button><div class="target-temp compact editable-target" data-temperature-kind="zone" data-id="${esc(zone.id)}" data-value="${Number.isFinite(target) ? target : ''}" tabindex="0" role="button" title="${esc(tr('common.clickTemperatureToEdit'))}">${Number.isFinite(target) ? target.toFixed(1) : '--'}<small>°C</small></div><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="0.5">${uiIcon('plus')}</button></div><div><small>${esc(tr('zones.deviceTarget'))}</small><strong>${fmtTemp(zone.device_setpoint)}</strong></div></div>
${zoneHeatingGuardNotice(zone, device)}
<div class="preset-row quick-control-row quick-control-row-4"> <div class="preset-row quick-control-row quick-control-row-4">
${['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>