This commit is contained in:
Mateusz Gruszczyński committed 2026-10-21 08:08:59 +02:00
1 parent 05185a94f7
commit bb5caa769f
28 files changed
+592 -130

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.3" version = "0.16.4"
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.3" version = "0.16.4"
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"
+33 -32
View File
@@ -1,17 +1,17 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore 2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore
2f663bd03e41e40d584183074dca077c8963afc8308bd66e7d33ea6aed6ec1e5 ./Cargo.lock 147eed97b308e0c27d6a5ee738a0ee710fa6ba2c6bbf7503df0fe484e0c7f842 ./Cargo.lock
8f4f035398326b1beec96d2c0d4b8fcf4b7f1542e39fe719743cc95ecca740e1 ./Cargo.toml c604af0060aef4e79d2e11fd1e1222a2444cebc835bc50629cff3d2373c5a05a ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
eaea820d2578f56442792e6fd6403eb1253686e50dbfcd8e75179c98574e1859 ./README.md 75a6652611273ae458cb3509401cdb196d1084db1e49496ecdce448f490e6249 ./README.md
2cc1518c6484c340037700ef258e27cdf71a7aa5233aefcb51cfa7c199c3f3a7 ./SOURCE_MANIFEST.sha256 eedafc7faaa9eb95b564a249a4fef82d5f4cc32697eb0e370b96264f3c47b69c ./SOURCE_MANIFEST.sha256
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
e23bf048051f522364d213b0bd0ddfaff82d311a84542e20568dcf3f6aec7ffc ./docs/API.md bb9840a0dc2b89d057d1461ae581ee61ebf856e3b6d38bc7d1bdd111eef7c550 ./docs/API.md
c64564481d1a7bb4657f12b2fe0cbddf86050a4c6a2fc600ad8f02051387aa27 ./docs/FLOW.md 4f9826f1bf3b42ed2d62d8a8e9221b50175b3a9fdda7e9d219351f7a33039257 ./docs/FLOW.md
bfb0bba73a02e71aba78c0f5de1fd41d51d70812f6e08eaf2eaceb1295908c9d ./docs/openapi.json 52c8dc48aed987fd28b2b543af1e5a782efb2a44104ccb0fd6b1740255cf384e ./docs/openapi.json
a5ad73f7933031dfaa99aa92e27f10223d53fb84b14ee0a378eb5f10eb80e8c8 ./flows/simple-heating/igor-simple-heat-v5.flow.json d8a1b3e34cf8a79ac3296bf600ad47826d639cdef4c1c2c099f8b6b21a391ebd ./flows/simple-heating/igor-simple-heat-v5.flow.json
9743f4328a18bc9d89d10a9011d13ca201a4f352db2119335a66a2147a9a486d ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json bd9a07b8ca4b090ad0089e9037492cc25c47f3456b97ec08152947314ea904a6 ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json
2d804831a72577e80dc1ec3cf4f86dc8bcf00b1d8c80929896f11da0230b2b07 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json c4e6e3e8df5eb7f9730a79fb90edd155f0510f7bb4862a04b9f35ba649c22376 ./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
ebe14ca743c50cc0cc02f94621b17119984704f702df04cd94e1e5b86635a014 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json e8d079ebe63cd2916bd219cd45f583940e3d7ba5769af8deb1038415084a1a35 ./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
1b32f617ed09f37d58fd8532107cfcbf2fb92c4434bd173b0fc73464664ce927 ./ha-addon/repository/gree-controller/CHANGELOG.md 904b21f54c7af0f59d91016d8513baaea5e927a349e18207f4290c51d7f2be8a ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
0e92e538aa03b442f8fc2c21562cc7aa19d8ae346f78cecca1eec131288edade ./ha-addon/repository/gree-controller/README.md e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md
e48d172951db6df8afe97b0d463902e744c22bd3e677041bf5e6ee5624646f25 ./ha-addon/repository/gree-controller/config.yaml efe06cb35044b19474ae11e5ef42e3201787ff1d164087cdbdb845a5ad6a16d8 ./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
@@ -101,19 +101,20 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
36396ecfcfd30f50016b17a83cd76cbccd8debaacef45a0d6b4e61946fef098d ./scripts/smoke.sh 36396ecfcfd30f50016b17a83cd76cbccd8debaacef45a0d6b4e61946fef098d ./scripts/smoke.sh
b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js
e3825cfd9e6e149fd227c521d886dc116170f2553d4d7639d61874267f257b6e ./scripts/test_flow_disable_ui.js
2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js 2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
14f07768257945ed52c218f3bf03c25cb55053e7a6683c8ff17f395e16d28f2e ./scripts/verify_simple_heating.py c260a6d291ad913f6aca60f23a0acf6928d68cfb0545b2a698d58c5a95fcf934 ./scripts/verify_simple_heating.py
c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs 1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs
34378c387d34d67483030d1016b874a74115c1ef5f268f854180c25146fdb08b ./src/api/automations.rs 69ce9a3e0cc3b805e605beb811ee8fd80c37cc38a552896500bf522dccdbb23e ./src/api/automations.rs
fffad7874465739cd6b508e9d7791387b864fa88420ce0f7e2fc933d129a2751 ./src/api/configuration.rs fffad7874465739cd6b508e9d7791387b864fa88420ce0f7e2fc933d129a2751 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs 8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
8c5d46ccaa45950f4dbaae63e68ad6efe393ff0904e46420f5e7950f043fb691 ./src/api/flows.rs c7a01af66fe69de6dda6ca0d3bf4489ab3f7c1181853bd394340cdc8dc65c7fc ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs
@@ -122,11 +123,11 @@ b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/hist
7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs 7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs
9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs 9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs
b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs 1d51fe0def463fd0d1a9a97f66f362f3f8bd24daafb14993f3105b1a8ffed5b6 ./src/api/schedules.rs
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
7c65692070e51af1ebd66d98b087b7f7a241ed31f8ff2f41baf77e46e027688c ./src/api/zones.rs eaa4070afbc19686d5e9bdb2d24fb2c7fc4bf1f90dc1c4f12fef17017d4cb975 ./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
@@ -135,17 +136,17 @@ a8fe3636ac79df191e2f2c9d8c1eec1e5181e048ef8c9036cfa18f84aef1a462 ./src/db/confi
4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs 4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs
7bc79d9d33726fbdd33b485c8c2e839cedbb8fe29fee872611da978ed149d6a0 ./src/db/energy_history.rs 7bc79d9d33726fbdd33b485c8c2e839cedbb8fe29fee872611da978ed149d6a0 ./src/db/energy_history.rs
96aac6510ddda8c18fd7e29802a937b96e8a74badf504980846c80cd7933a4c6 ./src/db/events_tokens.rs 96aac6510ddda8c18fd7e29802a937b96e8a74badf504980846c80cd7933a4c6 ./src/db/events_tokens.rs
bba6e3fb9f4800ae06d0ca67772b06a8c389d308243306719442d0c1f2623420 ./src/db/flows.rs a951422c51073ad8131edb9c0d264ce04c7cd71e62da7412619d67b774472ecf ./src/db/flows.rs
5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs 5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs
4fc007c08304e96818ad1d78997a5b2458e31aa9c48ad68b5be2a007533924bb ./src/db/network_history.rs 4fc007c08304e96818ad1d78997a5b2458e31aa9c48ad68b5be2a007533924bb ./src/db/network_history.rs
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs 253b0c77897a848f93452ddf809c794373ec4942203838a51976805f50e2aa1b ./src/db/schedules_automations.rs
28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs 51ecf9c0a2554bfd599ee9881ff17a88f184d3cfa2cd57f3166bbb1dd544be2d ./src/db/tests.rs
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
7b29709d2a97e201a6f7baab9c3a72ea3792488499c47e426ebb93369fcbb3fc ./src/engine.rs 7b29709d2a97e201a6f7baab9c3a72ea3792488499c47e426ebb93369fcbb3fc ./src/engine.rs
4412770793e4905d490a56e0f46cc678a5e28044fc0d136912ef1b81da3e3e3f ./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
dbf73f1459185836487653d05e73ffd8314fb0244d23878fc84232fd10935b8a ./src/engine/control_plan.rs 62a15ea757c53796149e670863dae1b4e1a16ceb6ef88ed84ebd3089c0b9880d ./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
@@ -159,9 +160,9 @@ c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/r
2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs 2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs
d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs 833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
6f81f1b93f653b8450b9e086ed4728eca90b3d5d7639b7f4fb4fa2f42c514a28 ./src/engine/tests.rs 4160811f47eca6e75970fe6c6b95198f86a4b97b7050531c19a19efb3d76d97d ./src/engine/tests.rs
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs 07c3c462884344f018654267de3d454d895343b4699e34635e4d1ec88546df0a ./src/engine/zone_actions.rs
2cbfc34ad0868591e60e3110d8ae346dff07a58c894e87598e4b7b5749fb4613 ./src/engine/zone_control.rs e246b6d6e3126941b989735a27e5fbe88dcaf351420aaaa060bb3761620d8244 ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.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
5229bdb5ce1018601b081654d16cee9ac28afdfda6c67effe1b14d44b14b3257 ./src/models/zone.rs 79cb16a9f12e8a20fcf0acc8649d771b8a76a05503c0372443d29003536c1bcb ./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,15 +207,15 @@ 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
282ef3c84f053cfce36fa85828365cac2cb6dfa4375993345ff3bee05c8a884f ./web/css/styles.css b030e61b30435495f492e15fa9daf228968cc1de2982e8f40a626452dbf5b81a ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
e76b1e0bbdad0297d5ae2e79e40e658e0263f038ab70db81d330a342e01a0a8b ./web/index.html bbe640e99447f632383b2eb4b52da580ab95d60cd45cda47f6fae1c2380e5b8a ./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
008a85cddf6a5357eff35764eabddf1d7baaecf949b481d195124de50431e0d2 ./web/js-dynamic/dashboard.js ef20fb1e706105d0afdc2fa505d92ea52e43d93c9a687f2a09d381227a464564 ./web/js-dynamic/dashboard.js
3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./web/js-dynamic/entities.js 3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./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
@@ -222,7 +223,7 @@ bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynam
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
d9596cef1b86c0d3f73ccfc8d3a73c2b5293d81293fdbdcefd77346cbd3fe983 ./web/js-dynamic/navigation.js d9596cef1b86c0d3f73ccfc8d3a73c2b5293d81293fdbdcefd77346cbd3fe983 ./web/js-dynamic/navigation.js
3ff9c027beb8fb033330bf3cfb3496e06fb38c42041f1ffe522c6c7af18e9e30 ./web/js-dynamic/realtime.js 54412ea35574b1e852df86d33fac089ca925253277316b6917eacd66c9927c0e ./web/js-dynamic/realtime.js
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js 1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js
+2 -2
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.3** **Version: 0.16.4**
Version 0.16.3 adds built-in, localized explanations for the two Flow blocks (constant Yes and Simple day/night heating), including live examples calculated from each room's configured temperature and tooltips for every setting. Heating/control behavior from 0.16.2 is unchanged. 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.
> [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`.
+32 -31
View File
@@ -1,16 +1,16 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore 2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore
2f663bd03e41e40d584183074dca077c8963afc8308bd66e7d33ea6aed6ec1e5 ./Cargo.lock 147eed97b308e0c27d6a5ee738a0ee710fa6ba2c6bbf7503df0fe484e0c7f842 ./Cargo.lock
8f4f035398326b1beec96d2c0d4b8fcf4b7f1542e39fe719743cc95ecca740e1 ./Cargo.toml c604af0060aef4e79d2e11fd1e1222a2444cebc835bc50629cff3d2373c5a05a ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
eaea820d2578f56442792e6fd6403eb1253686e50dbfcd8e75179c98574e1859 ./README.md 75a6652611273ae458cb3509401cdb196d1084db1e49496ecdce448f490e6249 ./README.md
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
e23bf048051f522364d213b0bd0ddfaff82d311a84542e20568dcf3f6aec7ffc ./docs/API.md bb9840a0dc2b89d057d1461ae581ee61ebf856e3b6d38bc7d1bdd111eef7c550 ./docs/API.md
c64564481d1a7bb4657f12b2fe0cbddf86050a4c6a2fc600ad8f02051387aa27 ./docs/FLOW.md 4f9826f1bf3b42ed2d62d8a8e9221b50175b3a9fdda7e9d219351f7a33039257 ./docs/FLOW.md
bfb0bba73a02e71aba78c0f5de1fd41d51d70812f6e08eaf2eaceb1295908c9d ./docs/openapi.json 52c8dc48aed987fd28b2b543af1e5a782efb2a44104ccb0fd6b1740255cf384e ./docs/openapi.json
a5ad73f7933031dfaa99aa92e27f10223d53fb84b14ee0a378eb5f10eb80e8c8 ./flows/simple-heating/igor-simple-heat-v5.flow.json d8a1b3e34cf8a79ac3296bf600ad47826d639cdef4c1c2c099f8b6b21a391ebd ./flows/simple-heating/igor-simple-heat-v5.flow.json
9743f4328a18bc9d89d10a9011d13ca201a4f352db2119335a66a2147a9a486d ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json bd9a07b8ca4b090ad0089e9037492cc25c47f3456b97ec08152947314ea904a6 ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json
2d804831a72577e80dc1ec3cf4f86dc8bcf00b1d8c80929896f11da0230b2b07 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json c4e6e3e8df5eb7f9730a79fb90edd155f0510f7bb4862a04b9f35ba649c22376 ./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
ebe14ca743c50cc0cc02f94621b17119984704f702df04cd94e1e5b86635a014 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json e8d079ebe63cd2916bd219cd45f583940e3d7ba5769af8deb1038415084a1a35 ./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
1b32f617ed09f37d58fd8532107cfcbf2fb92c4434bd173b0fc73464664ce927 ./ha-addon/repository/gree-controller/CHANGELOG.md 904b21f54c7af0f59d91016d8513baaea5e927a349e18207f4290c51d7f2be8a ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
0e92e538aa03b442f8fc2c21562cc7aa19d8ae346f78cecca1eec131288edade ./ha-addon/repository/gree-controller/README.md e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md
e48d172951db6df8afe97b0d463902e744c22bd3e677041bf5e6ee5624646f25 ./ha-addon/repository/gree-controller/config.yaml efe06cb35044b19474ae11e5ef42e3201787ff1d164087cdbdb845a5ad6a16d8 ./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
@@ -100,19 +100,20 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
36396ecfcfd30f50016b17a83cd76cbccd8debaacef45a0d6b4e61946fef098d ./scripts/smoke.sh 36396ecfcfd30f50016b17a83cd76cbccd8debaacef45a0d6b4e61946fef098d ./scripts/smoke.sh
b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js
e3825cfd9e6e149fd227c521d886dc116170f2553d4d7639d61874267f257b6e ./scripts/test_flow_disable_ui.js
2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js 2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
14f07768257945ed52c218f3bf03c25cb55053e7a6683c8ff17f395e16d28f2e ./scripts/verify_simple_heating.py c260a6d291ad913f6aca60f23a0acf6928d68cfb0545b2a698d58c5a95fcf934 ./scripts/verify_simple_heating.py
c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs 1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs
34378c387d34d67483030d1016b874a74115c1ef5f268f854180c25146fdb08b ./src/api/automations.rs 69ce9a3e0cc3b805e605beb811ee8fd80c37cc38a552896500bf522dccdbb23e ./src/api/automations.rs
fffad7874465739cd6b508e9d7791387b864fa88420ce0f7e2fc933d129a2751 ./src/api/configuration.rs fffad7874465739cd6b508e9d7791387b864fa88420ce0f7e2fc933d129a2751 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs 8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
8c5d46ccaa45950f4dbaae63e68ad6efe393ff0904e46420f5e7950f043fb691 ./src/api/flows.rs c7a01af66fe69de6dda6ca0d3bf4489ab3f7c1181853bd394340cdc8dc65c7fc ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs
@@ -121,11 +122,11 @@ b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/hist
7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs 7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs
9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs 9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs
b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs 1d51fe0def463fd0d1a9a97f66f362f3f8bd24daafb14993f3105b1a8ffed5b6 ./src/api/schedules.rs
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
7c65692070e51af1ebd66d98b087b7f7a241ed31f8ff2f41baf77e46e027688c ./src/api/zones.rs eaa4070afbc19686d5e9bdb2d24fb2c7fc4bf1f90dc1c4f12fef17017d4cb975 ./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
@@ -134,17 +135,17 @@ a8fe3636ac79df191e2f2c9d8c1eec1e5181e048ef8c9036cfa18f84aef1a462 ./src/db/confi
4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs 4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs
7bc79d9d33726fbdd33b485c8c2e839cedbb8fe29fee872611da978ed149d6a0 ./src/db/energy_history.rs 7bc79d9d33726fbdd33b485c8c2e839cedbb8fe29fee872611da978ed149d6a0 ./src/db/energy_history.rs
96aac6510ddda8c18fd7e29802a937b96e8a74badf504980846c80cd7933a4c6 ./src/db/events_tokens.rs 96aac6510ddda8c18fd7e29802a937b96e8a74badf504980846c80cd7933a4c6 ./src/db/events_tokens.rs
bba6e3fb9f4800ae06d0ca67772b06a8c389d308243306719442d0c1f2623420 ./src/db/flows.rs a951422c51073ad8131edb9c0d264ce04c7cd71e62da7412619d67b774472ecf ./src/db/flows.rs
5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs 5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs
4fc007c08304e96818ad1d78997a5b2458e31aa9c48ad68b5be2a007533924bb ./src/db/network_history.rs 4fc007c08304e96818ad1d78997a5b2458e31aa9c48ad68b5be2a007533924bb ./src/db/network_history.rs
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs 253b0c77897a848f93452ddf809c794373ec4942203838a51976805f50e2aa1b ./src/db/schedules_automations.rs
28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs 51ecf9c0a2554bfd599ee9881ff17a88f184d3cfa2cd57f3166bbb1dd544be2d ./src/db/tests.rs
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
7b29709d2a97e201a6f7baab9c3a72ea3792488499c47e426ebb93369fcbb3fc ./src/engine.rs 7b29709d2a97e201a6f7baab9c3a72ea3792488499c47e426ebb93369fcbb3fc ./src/engine.rs
4412770793e4905d490a56e0f46cc678a5e28044fc0d136912ef1b81da3e3e3f ./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
dbf73f1459185836487653d05e73ffd8314fb0244d23878fc84232fd10935b8a ./src/engine/control_plan.rs 62a15ea757c53796149e670863dae1b4e1a16ceb6ef88ed84ebd3089c0b9880d ./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,9 +159,9 @@ c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/r
2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs 2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs
d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs 833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
6f81f1b93f653b8450b9e086ed4728eca90b3d5d7639b7f4fb4fa2f42c514a28 ./src/engine/tests.rs 4160811f47eca6e75970fe6c6b95198f86a4b97b7050531c19a19efb3d76d97d ./src/engine/tests.rs
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs 07c3c462884344f018654267de3d454d895343b4699e34635e4d1ec88546df0a ./src/engine/zone_actions.rs
2cbfc34ad0868591e60e3110d8ae346dff07a58c894e87598e4b7b5749fb4613 ./src/engine/zone_control.rs e246b6d6e3126941b989735a27e5fbe88dcaf351420aaaa060bb3761620d8244 ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
@@ -179,7 +180,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
5229bdb5ce1018601b081654d16cee9ac28afdfda6c67effe1b14d44b14b3257 ./src/models/zone.rs 79cb16a9f12e8a20fcf0acc8649d771b8a76a05503c0372443d29003536c1bcb ./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
@@ -205,15 +206,15 @@ 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
282ef3c84f053cfce36fa85828365cac2cb6dfa4375993345ff3bee05c8a884f ./web/css/styles.css b030e61b30435495f492e15fa9daf228968cc1de2982e8f40a626452dbf5b81a ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
e76b1e0bbdad0297d5ae2e79e40e658e0263f038ab70db81d330a342e01a0a8b ./web/index.html bbe640e99447f632383b2eb4b52da580ab95d60cd45cda47f6fae1c2380e5b8a ./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
008a85cddf6a5357eff35764eabddf1d7baaecf949b481d195124de50431e0d2 ./web/js-dynamic/dashboard.js ef20fb1e706105d0afdc2fa505d92ea52e43d93c9a687f2a09d381227a464564 ./web/js-dynamic/dashboard.js
3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./web/js-dynamic/entities.js 3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./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
@@ -221,7 +222,7 @@ bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynam
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
d9596cef1b86c0d3f73ccfc8d3a73c2b5293d81293fdbdcefd77346cbd3fe983 ./web/js-dynamic/navigation.js d9596cef1b86c0d3f73ccfc8d3a73c2b5293d81293fdbdcefd77346cbd3fe983 ./web/js-dynamic/navigation.js
3ff9c027beb8fb033330bf3cfb3496e06fb38c42041f1ffe522c6c7af18e9e30 ./web/js-dynamic/realtime.js 54412ea35574b1e852df86d33fac089ca925253277316b6917eacd66c9927c0e ./web/js-dynamic/realtime.js
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js 1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js
+4 -4
View File
@@ -1,8 +1,8 @@
# GREE Controller API reference # GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.16.3**. HTTP and WebSocket API for GREE Controller **0.16.4**.
Version 0.16.3: the Flow editor adds localized descriptions and contextual examples for the two heating blocks; the API and heating behavior remain compatible with 0.16.2. Version 0.16.4: disabled parent Flows suppress their generated automations and schedules throughout execution, API status and control plans. Newly enabled Flows restart their stateful timing gates. The zone response includes optional `flow_control_id` and `flow_control_node_id` as internal ownership diagnostics.
Version 0.16.2: Flow adds the `zone_heat_cycle` action with `zone_id`, `day_target`, `night_target`, `day_min`, `boost_after_minutes` and `day_off_after_minutes`. An always-true Flow condition is sufficient. The Zone runtime stores the active `simple_heat` profile and timers; source `automation.flow` means automatic control, not manual. Nighttime modulation keeps the unit ON and uses 19/21/23 °C around a 20.6 °C target. Daytime power is OFF above the minimum until required, then OFF after 30 minutes at target. Existing `zone_thermostat` coasting options remain supported. Version 0.16.2: Flow adds the `zone_heat_cycle` action with `zone_id`, `day_target`, `night_target`, `day_min`, `boost_after_minutes` and `day_off_after_minutes`. An always-true Flow condition is sufficient. The Zone runtime stores the active `simple_heat` profile and timers; source `automation.flow` means automatic control, not manual. Nighttime modulation keeps the unit ON and uses 19/21/23 °C around a 20.6 °C target. Daytime power is OFF above the minimum until required, then OFF after 30 minutes at target. Existing `zone_thermostat` coasting options remain supported.
@@ -248,7 +248,7 @@ Response:
{ {
"status": "ok", "status": "ok",
"name": "gree-controller", "name": "gree-controller",
"version": "0.16.3", "version": "0.16.4",
"uptime_seconds": 1234, "uptime_seconds": 1234,
"control_ready": true, "control_ready": true,
"time": "2026-08-30T06:54:00Z" "time": "2026-08-30T06:54:00Z"
@@ -311,7 +311,7 @@ Returns the initial Web UI snapshot:
"control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []}, "control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []},
"control_plan_revision": 42, "control_plan_revision": 42,
"system": { "system": {
"version": "0.16.3", "version": "0.16.4",
"uptime_seconds": 1234, "uptime_seconds": 1234,
"auth_required": false, "auth_required": false,
"control_ready": true, "control_ready": true,
+6
View File
@@ -22,6 +22,12 @@ Flow is an authoring/orchestration layer. It does not replace the thermostat, sc
- Thermostat hysteresis, compressor protection, schedule hand-back, manual/local ownership and Temporary Quick Thermostat keep their existing priority. - Thermostat hysteresis, compressor protection, schedule hand-back, manual/local ownership and Temporary Quick Thermostat keep their existing priority.
- GREE fan/quiet/sleep and dry/fan HVAC modes are suppressed on thermostat-assigned devices when they would fight the regulator. Device-only features such as light/turbo/louver/air/xfan/health remain available through the normal command path. - GREE fan/quiet/sleep and dry/fan HVAC modes are suppressed on thermostat-assigned devices when they would fight the regulator. Device-only features such as light/turbo/louver/air/xfan/health remain available through the normal command path.
## Wyłączanie Flow / Disabling Flow (0.16.4)
`Flow.enabled=false` and `draft=true` are authoritative: generated automations and schedules do not execute, create deadlines or contribute to the control plan even if a legacy/imported child record still says `enabled=true`. Runtime-only Flow checkpoints cannot overwrite `enabled`, resurrect deleted rules or persist across a Flow recompile. Actions recheck both their own enabled state and the parent Flow under the automation execution lock.
Disabling, deleting or replacing a thermostat Flow releases zone-level thermostat intent attributed to that Flow (including stale manual setpoints and pending compressor actions) immediately during the Flow edit, with a regulator-cycle fallback for old/imported data. Manual/local takeover is never cleared. Single-shot device/group/HA commands that have already reached an external device/service are not retroactively undone; disable blocks **future executions**. Stateful timing counters, change-event latches, rolling samples and cooldowns start fresh when the Flow is enabled again; disabled time is not counted.
## Concurrency invariants ## Concurrency invariants
Structural Flow writes use the global order: Structural Flow writes use the global order:
+16 -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.3", "version": "0.16.4",
"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.3" "example": "0.16.4"
}, },
"uptime_seconds": { "uptime_seconds": {
"type": "integer", "type": "integer",
@@ -7380,6 +7380,20 @@
"heat_coasting": { "heat_coasting": {
"type": "boolean", "type": "boolean",
"description": "True when the controller requested low setpoint while keeping unit powered ON." "description": "True when the controller requested low setpoint while keeping unit powered ON."
},
"flow_control_id": {
"type": [
"string",
"null"
],
"description": "Last Flow owning the zone; null if none or legacy record."
},
"flow_control_node_id": {
"type": [
"string",
"null"
],
"description": "Last Flow action block owning the zone; null if none or legacy record."
} }
}, },
"required": [ "required": [
@@ -1,7 +1,7 @@
{ {
"domain": "gree_controller", "domain": "gree_controller",
"name": "GREE Controller", "name": "GREE Controller",
"version": "0.16.3", "version": "0.16.4",
"config_flow": true, "config_flow": true,
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
@@ -1,5 +1,16 @@
# Changelog # Changelog
## 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.
- Requires both the generated automation/schedule and its parent Flow to be enabled before participating in the engine, control plan, next-event timeline or API enabled status, including mismatched imported/legacy data.
- Adds a second enable/revision check under the execution lock immediately before sending an action. No newly initiated Flow actions after disabling it.
- Releases orphaned thermostat intent and pending compressor tasks when a Flow is disabled, deleted, or its action is removed; preserves real manual ownership and avoids retroactively undoing already sent external commands.
- Resets Flow timer, change-detection, rolling-window and cooldown state after an OFF → ON transition, so disabled intervals never advance timing conditions.
- Filters the dashboard's recent-Flow and simulation cards against the parent state, including the moment between a WebSocket Flow update and a rebuilt control plan.
- Adds database and engine regression tests for disabled parents, stale writes, orphaned rules, zone handback and manual takeover.
- Restores the three ready-to-import simple heating Flow JSON examples missing from the prior source package and updates the static verification script for the new ownership checker.
## 0.16.3 ## 0.16.3
- Adds expandable, per-room examples for nighttime 19/21/23°C modulation, daytime OFF/boost/30-minute stability, zone temperature-source fallback and Flow vs manual ownership. - Adds expandable, per-room examples for nighttime 19/21/23°C modulation, daytime OFF/boost/30-minute stability, zone temperature-source fallback and Flow vs manual ownership.
@@ -1,5 +1,5 @@
name: "GREE Controller" name: "GREE Controller"
version: "0.16.3" version: "0.16.4"
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/"
+52
View File
@@ -0,0 +1,52 @@
// Run with: node scripts/test_flow_disable_ui.js
// Regression checks for Flow OFF hiding generated rules even with stale plan data.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const host = { innerHTML: '' };
const section = { hidden: false };
const app = {
flows: [{ id: 'flow1', enabled: false, draft: false, name: 'Disabled heating' }],
automations: [], devices: [], groups: [], zones: [],
controlPlan: { zones: [], next_events: [], rules: [], emergency_stop_active: false },
};
const context = vm.createContext({
app,
$: selector => selector === '#controlPlan' ? host : selector === '#controlPlanSection' ? section : null,
tr: key => key,
esc: x => String(x),
fmtTemp: n => String(n),
houseModeLabel: x => x,
zoneRuntimeStatusLabel: () => '',
zonePresetLabel: x => x,
groupState: () => ({}),
modeLabel: x => x,
locale: () => 'pl-PL',
});
vm.runInContext(fs.readFileSync(path.join(root, 'web/js-dynamic/dashboard.js'), 'utf8'), context);
function rule(flowId, enabled = true, id = 'FLOW_RULE_MARKER') {
return { id, name: id, enabled, flow_id: flowId, trigger_kind: 'flow', action: {} };
}
const generated = rule('flow1');
const orphan = rule('missing-flow', true, 'ORPHAN_RULE_MARKER');
const local = rule(null, true, 'LOCAL_RULE_MARKER');
app.controlPlan.rules = [generated, orphan, local];
function visible(r) { return context.planRuleEffectivelyEnabled(r); }
assert.equal(visible(generated), false, 'disabled parent must block generated rule');
assert.equal(visible(orphan), false, 'missing parent must block generated rule');
assert.equal(visible(local), true, 'unrelated local automation remains available');
context.renderControlPlan();
assert.doesNotMatch(host.innerHTML, /FLOW_RULE_MARKER|ORPHAN_RULE_MARKER/);
assert.match(host.innerHTML, /LOCAL_RULE_MARKER/);
app.flows[0].enabled = true;
assert.equal(visible(generated), true);
context.renderControlPlan();
assert.match(host.innerHTML, /FLOW_RULE_MARKER/);
app.flows[0].draft = true;
assert.equal(visible(generated), false, 'draft Flow must never execute');
app.flows[0].draft = false;
app.flows[0].enabled = false;
assert.equal(visible(rule('flow1', false)), false, 'disabled compiled rule must remain disabled');
console.log('OK: 9 disabled Flow UI/regression checks');
+1 -1
View File
@@ -78,7 +78,7 @@ def main() -> None:
for value in ("zone_heat_cycle", "boost_after_minutes", "day_off_after_minutes"): for value in ("zone_heat_cycle", "boost_after_minutes", "day_off_after_minutes"):
assert value in rust and value in ui, value assert value in rust and value in ui, value
assert "simple_heat_step(" in engine and "action_zone_simple_heat" in auto assert "simple_heat_step(" in engine and "action_zone_simple_heat" in auto
assert "zone.simple_heat" in engine and "enabled_heat_actions" in engine assert "zone.simple_heat" in engine and "active_flow_zone_action_ids" in engine
assert "automation.flow" in status and "zones.flowDayOff" in status assert "automation.flow" in status and "zones.flowDayOff" in status
assert 'data-flow-add="zone_heat_cycle"' in html assert 'data-flow-add="zone_heat_cycle"' in html
assert "day_target_20_starts_at_21_then_boosts_to_22" in (ROOT / "src" / "engine" / "simple_heat.rs").read_text() assert "day_target_20_starts_at_21_then_boosts_to_22" in (ROOT / "src" / "engine" / "simple_heat.rs").read_text()
+6 -5
View File
@@ -243,11 +243,12 @@ async fn get_automation(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<String>, Path(id): Path<String>,
) -> Result<Json<Automation>, AppError> { ) -> Result<Json<Automation>, AppError> {
state let mut item = state.db.get_automation(&id)?
.db .ok_or_else(|| AppError::NotFound(format!("automation {id}")))?;
.get_automation(&id)? if !state.db.flow_output_enabled(item.flow_id.as_deref())? {
.map(Json) item.enabled = false;
.ok_or_else(|| AppError::NotFound(format!("automation {id}"))) }
Ok(Json(item))
} }
async fn create_automation( async fn create_automation(
State(state): State<AppState>, State(state): State<AppState>,
+15 -7
View File
@@ -1365,6 +1365,11 @@ fn compile_flow(
let zones = state.db.list_zones()?; let zones = state.db.list_zones()?;
let groups = state.db.list_groups()?; let groups = state.db.list_groups()?;
let devices = state.db.list_devices()?; let devices = state.db.list_devices()?;
// Disabling a Flow is a hard timing boundary. Neither disabled time nor
// the previous run's latches/observations may advance a newly enabled Flow.
let resumed = state.db.get_flow(&flow.id)?
.is_some_and(|previous| !previous.enabled && flow.enabled);
let reset_flow_runtime = !flow.enabled || resumed;
let previous_schedules: std::collections::HashMap<String, Schedule> = state let previous_schedules: std::collections::HashMap<String, Schedule> = state
.db .db
.list_schedules()? .list_schedules()?
@@ -1504,13 +1509,13 @@ fn compile_flow(
.get(&id) .get(&id)
.map(|item| item.created_at.clone()) .map(|item| item.created_at.clone())
.unwrap_or_else(|| now.clone()); .unwrap_or_else(|| now.clone());
let last_fired_at = previous_automations let last_fired_at = if resumed { None } else {
.get(&id) previous_automations.get(&id).and_then(|item| item.last_fired_at.clone())
.and_then(|item| item.last_fired_at.clone()); };
let last_trigger_trace = previous_automations let last_trigger_trace = if reset_flow_runtime { Vec::new() } else {
.get(&id) previous_automations.get(&id)
.map(|item| item.last_trigger_trace.clone()) .map(|item| item.last_trigger_trace.clone()).unwrap_or_default()
.unwrap_or_default(); };
let flow_runtime = previous_automations let flow_runtime = previous_automations
.get(&id) .get(&id)
.map(|item| { .map(|item| {
@@ -1547,6 +1552,7 @@ fn compile_flow(
runtime runtime
}) })
.unwrap_or_default(); .unwrap_or_default();
let flow_runtime = if reset_flow_runtime { Default::default() } else { flow_runtime };
let mut item = Automation { let mut item = Automation {
id, id,
name: generated_flow_name(&flow.id, &action_node.id), name: generated_flow_name(&flow.id, &action_node.id),
@@ -1975,6 +1981,7 @@ async fn update_flow(
state state
.db .db
.replace_flow_outputs(&flow, &schedules, &automations)?; .replace_flow_outputs(&flow, &schedules, &automations)?;
engine::release_inactive_flow_intents(&state).await?;
let mut zone_ids = old_zone_ids; let mut zone_ids = old_zone_ids;
zone_ids.extend(schedules.iter().map(|s| s.zone_id.clone())); zone_ids.extend(schedules.iter().map(|s| s.zone_id.clone()));
for zone_id in zone_ids { for zone_id in zone_ids {
@@ -2008,6 +2015,7 @@ async fn delete_flow(
if !state.db.delete_flow(&id)? { if !state.db.delete_flow(&id)? {
return Err(AppError::NotFound(format!("flow {id}"))); return Err(AppError::NotFound(format!("flow {id}")));
} }
engine::release_inactive_flow_intents(&state).await?;
for zone_id in zone_ids { for zone_id in zone_ids {
refresh_zone_override_boundary(&state, &zone_id).await?; refresh_zone_override_boundary(&state, &zone_id).await?;
} }
+6 -5
View File
@@ -158,11 +158,12 @@ async fn get_schedule(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<String>, Path(id): Path<String>,
) -> Result<Json<Schedule>, AppError> { ) -> Result<Json<Schedule>, AppError> {
state let mut item = state.db.get_schedule(&id)?
.db .ok_or_else(|| AppError::NotFound(format!("schedule {id}")))?;
.get_schedule(&id)? if !state.db.flow_output_enabled(item.flow_id.as_deref())? {
.map(Json) item.enabled = false;
.ok_or_else(|| AppError::NotFound(format!("schedule {id}"))) }
Ok(Json(item))
} }
async fn create_schedule( async fn create_schedule(
State(state): State<AppState>, State(state): State<AppState>,
+2
View File
@@ -246,6 +246,8 @@ impl ZoneInput {
revision: 1, revision: 1,
control_owner: "automation".into(), control_owner: "automation".into(),
control_source: "automation".into(), control_source: "automation".into(),
flow_control_id: None,
flow_control_node_id: None,
control_since: Some(Utc::now()), control_since: Some(Utc::now()),
control_resume_at: None, control_resume_at: None,
control_reason: "zone created".into(), control_reason: "zone created".into(),
+20
View File
@@ -1,4 +1,24 @@
impl Db { impl Db {
fn enabled_flow_ids(&self) -> Result<std::collections::HashSet<String>> {
Ok(self.list_flows()?.into_iter()
.filter(|flow| flow.enabled && !flow.draft)
.map(|flow| flow.id)
.collect())
}
fn flow_output_enabled_locked(conn: &Connection, flow_id: Option<&str>) -> Result<bool> {
let Some(flow_id) = flow_id else { return Ok(true); };
let raw: Option<String> = conn.query_row(queries::GET_FLOW, [flow_id], |row| row.get(0)).optional()?;
let Some(raw) = raw else { return Ok(false); };
let flow: Flow = Self::from_json(raw)?;
Ok(flow.enabled && !flow.draft)
}
pub fn flow_output_enabled(&self, flow_id: Option<&str>) -> Result<bool> {
let conn = self.lock()?;
Self::flow_output_enabled_locked(&conn, flow_id)
}
pub fn list_flows(&self) -> Result<Vec<Flow>> { pub fn list_flows(&self) -> Result<Vec<Flow>> {
self.list_payloads(queries::LIST_FLOWS) self.list_payloads(queries::LIST_FLOWS)
} }
+44 -2
View File
@@ -15,7 +15,16 @@ impl Db {
} }
pub fn list_schedules(&self) -> Result<Vec<Schedule>> { pub fn list_schedules(&self) -> Result<Vec<Schedule>> {
self.list_payloads(queries::LIST_SCHEDULES) let mut schedules: Vec<Schedule> = self.list_payloads(queries::LIST_SCHEDULES)?;
// The parent Flow is the execution authority. Legacy backups and stale engine
// snapshots may contain generated schedules whose own enabled bit is still true.
let enabled_flows = self.enabled_flow_ids()?;
for item in &mut schedules {
if item.flow_id.as_deref().is_some_and(|id| !enabled_flows.contains(id)) {
item.enabled = false;
}
}
Ok(schedules)
} }
pub fn get_schedule(&self, id: &str) -> Result<Option<Schedule>> { pub fn get_schedule(&self, id: &str) -> Result<Option<Schedule>> {
@@ -57,13 +66,46 @@ impl Db {
} }
pub fn list_automations(&self) -> Result<Vec<Automation>> { pub fn list_automations(&self) -> Result<Vec<Automation>> {
self.list_payloads(queries::LIST_AUTOMATIONS) let mut automations: Vec<Automation> = self.list_payloads(queries::LIST_AUTOMATIONS)?;
let enabled_flows = self.enabled_flow_ids()?;
for item in &mut automations {
if item.flow_id.as_deref().is_some_and(|id| !enabled_flows.contains(id)) {
item.enabled = false;
}
}
Ok(automations)
} }
pub fn get_automation(&self, id: &str) -> Result<Option<Automation>> { pub fn get_automation(&self, id: &str) -> Result<Option<Automation>> {
self.get_payload(queries::GET_AUTOMATION, id) self.get_payload(queries::GET_AUTOMATION, id)
} }
/// Never upsert a stale Flow evaluation. Only the runtime field is mutable here:
/// configuration, enabled flag, execution time and even deletion must be preserved.
/// Reading the current record, checking its owner and writing it share the DB mutex.
pub fn save_flow_runtime_if_current(&self, sampled: &Automation) -> Result<bool> {
let conn = self.lock()?;
let raw: Option<String> = conn.query_row(
queries::GET_AUTOMATION,
[sampled.id.as_str()],
|row| row.get(0),
).optional()?;
let Some(raw) = raw else { return Ok(false); };
let mut current: Automation = Self::from_json(raw)?;
if !current.enabled || current.updated_at != sampled.updated_at
|| current.flow_id != sampled.flow_id || current.trigger_kind != "flow"
|| !Self::flow_output_enabled_locked(&conn, current.flow_id.as_deref())?
{
return Ok(false);
}
current.flow_runtime = sampled.flow_runtime.clone();
conn.execute(
"UPDATE automations SET payload=?2 WHERE id=?1",
params![current.id, Self::to_json(&current)?],
)?;
Ok(true)
}
pub fn delete_automation(&self, id: &str) -> Result<bool> { pub fn delete_automation(&self, id: &str) -> Result<bool> {
self.delete_by_id("automations", id) self.delete_by_id("automations", id)
} }
+99
View File
@@ -3,6 +3,105 @@ mod tests {
use super::*; use super::*;
use crate::models::{ApiTokenInfo, ConnectionType, Device, HaReading, Reading}; use crate::models::{ApiTokenInfo, ConnectionType, Device, HaReading, Reading};
#[test]
fn disabled_flow_must_not_schedule_execute_or_restore_stale_automation() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(&dir.path().join("flow_disabled.db")).unwrap();
let now = Utc::now();
let mut flow = Flow {
id: "flow-1".into(),
name: "Heating".into(),
enabled: true,
draft: false,
description: String::new(),
nodes: Vec::new(),
edges: Vec::new(),
summary: String::new(),
compiled_schedule_ids: vec!["schedule-1".into()],
compiled_automation_ids: vec!["auto-1".into()],
revision: 1,
created_at: now,
updated_at: now,
};
let mut auto: Automation = serde_json::from_value(serde_json::json!({
"id": "auto-1", "name": "Generated", "enabled": true,
"trigger_kind": "flow", "flow_id": flow.id, "flow_node_id": "action-1",
"created_at": now, "updated_at": now
})).unwrap();
let schedule = Schedule {
id: "schedule-1".into(), zone_id: "zone-1".into(),
name: "Generated schedule".into(), enabled: true,
weekdays: vec![1, 2, 3, 4, 5, 6, 7],
start_time: "08:00".into(), end_time: "20:00".into(),
preset: "comfort".into(), setpoint: 21.0,
created_at: now, updated_at: now,
flow_id: Some(flow.id.clone()), flow_node_id: Some("action-2".into()),
};
db.replace_flow_outputs(&flow, &[schedule.clone()], &[auto.clone()]).unwrap();
assert!(db.list_automations().unwrap()[0].enabled);
assert!(db.list_schedules().unwrap()[0].enabled);
// Simulate the asynchronous evaluator taking a snapshot before Flow OFF.
let mut sampled = db.get_automation("auto-1").unwrap().unwrap();
sampled.flow_runtime.insert("timer-1".into(), Default::default());
assert!(db.save_flow_runtime_if_current(&sampled).unwrap());
assert_eq!(db.get_automation("auto-1").unwrap().unwrap().flow_runtime.len(), 1);
flow.enabled = false;
flow.revision = 2;
flow.updated_at = now + Duration::seconds(2);
auto.enabled = false;
auto.updated_at = flow.updated_at;
let mut disabled_schedule = schedule.clone();
disabled_schedule.enabled = false;
db.replace_flow_outputs(&flow, &[disabled_schedule], &[auto.clone()]).unwrap();
assert!(!db.save_flow_runtime_if_current(&sampled).unwrap());
assert!(!db.get_automation("auto-1").unwrap().unwrap().enabled);
assert!(!db.list_automations().unwrap()[0].enabled);
assert!(!db.list_schedules().unwrap()[0].enabled);
// Imported/legacy/corrupt configurations can have an enabled child and
// disabled parent. Parent authority must still win for ALL consumers.
db.save_automation(&sampled).unwrap();
db.save_schedule(&schedule).unwrap();
assert!(db.get_automation("auto-1").unwrap().unwrap().enabled);
assert!(!db.list_automations().unwrap()[0].enabled);
assert!(!db.list_schedules().unwrap()[0].enabled);
assert!(!db.flow_output_enabled(Some("flow-1")).unwrap());
assert!(!db.save_flow_runtime_if_current(&sampled).unwrap());
// A delete must not be undone by a previously in-flight evaluation.
db.delete_flow("flow-1").unwrap();
assert!(!db.save_flow_runtime_if_current(&sampled).unwrap());
assert!(db.get_automation("auto-1").unwrap().is_none());
assert!(!db.flow_output_enabled(Some("flow-1")).unwrap());
db.save_automation(&sampled).unwrap();
assert!(!db.list_automations().unwrap()[0].enabled); // Orphan fails closed
}
#[test]
fn disabled_rule_can_never_be_reenabled_by_runtime_checkpoint() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(&dir.path().join("race.db")).unwrap();
let now = Utc::now();
let mut rule: Automation = serde_json::from_value(serde_json::json!({
"id":"test-rule", "name":"No parent", "enabled":true, "trigger_kind":"flow",
"created_at":now, "updated_at":now
})).unwrap();
db.save_automation(&rule).unwrap();
let stale = rule.clone();
rule.enabled = false;
rule.updated_at = now + Duration::seconds(1);
db.save_automation(&rule).unwrap();
assert!(!db.save_flow_runtime_if_current(&stale).unwrap());
assert!(!db.get_automation("test-rule").unwrap().unwrap().enabled);
// Also validate optimistic concurrency: a new definition takes precedence.
rule.enabled = true;
db.save_automation(&rule).unwrap();
assert!(!db.save_flow_runtime_if_current(&stale).unwrap());
assert!(db.get_automation("test-rule").unwrap().unwrap().enabled);
}
#[test] #[test]
fn history_compaction_keeps_one_sample_per_old_bucket() { fn history_compaction_keeps_one_sample_per_old_bucket() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
+38 -6
View File
@@ -26,6 +26,8 @@ fn flow_off_physical_mode(action: &DeviceCommand) -> Option<String> {
fn flow_zone_action_already_satisfied( fn flow_zone_action_already_satisfied(
zone: &Zone, zone: &Zone,
flow_id: Option<&str>,
flow_node_id: Option<&str>,
device_power: bool, device_power: bool,
device_swing_vertical: u8, device_swing_vertical: u8,
device_swing_horizontal: u8, device_swing_horizontal: u8,
@@ -35,9 +37,18 @@ fn flow_zone_action_already_satisfied(
standby_offset_c: Option<f64>, standby_offset_c: Option<f64>,
simple_heat: Option<&SimpleHeatSettings>, simple_heat: Option<&SimpleHeatSettings>,
) -> bool { ) -> bool {
// Matching settings from another (or formerly deleted) Flow are not ownership of
// this action. It needs to execute and record its own provenance.
if zone.control_source != "automation.flow"
// Legacy zone rows without attribution must run once to claim their
// current Flow; matching settings alone do not prove ownership.
|| (flow_id.is_some() && zone.flow_control_id.as_deref() != flow_id)
|| (flow_node_id.is_some() && zone.flow_control_node_id.as_deref() != flow_node_id)
{
return false;
}
if let Some(profile) = simple_heat { if let Some(profile) = simple_heat {
return zone.simple_heat.as_ref() == Some(profile) return zone.simple_heat.as_ref() == Some(profile);
&& zone.control_source == "automation.flow";
} }
if heat_anticipation_c.is_some_and(|value| (zone.heat_anticipation_c - value).abs() > 0.001) if heat_anticipation_c.is_some_and(|value| (zone.heat_anticipation_c - value).abs() > 0.001)
|| standby_offset_c.is_some_and(|value| (zone.standby_offset_c - value).abs() > 0.001) || standby_offset_c.is_some_and(|value| (zone.standby_offset_c - value).abs() > 0.001)
@@ -115,7 +126,13 @@ async fn run_automations(state: &AppState) -> Result<()> {
"flow" => { "flow" => {
let before_runtime = item.flow_runtime.clone(); let before_runtime = item.flow_runtime.clone();
let result = flow_conditions_match(state, &devices, &mut item).await; let result = flow_conditions_match(state, &devices, &mut item).await;
if item.flow_runtime != before_runtime { state.db.save_automation(&item)?; } if item.flow_runtime != before_runtime
&& !state.db.save_flow_runtime_if_current(&item)?
{
// A Flow was disabled, deleted or recompiled during asynchronous
// condition evaluation. Never execute or resurrect its old snapshot.
continue;
}
match result { match result {
Ok((value, trace)) => { Ok((value, trace)) => {
trigger_trace = trace; trigger_trace = trace;
@@ -146,7 +163,11 @@ async fn run_automations(state: &AppState) -> Result<()> {
continue; continue;
} }
let Some(latest_item) = state.db.get_automation(&item.id)? else { continue; }; let Some(latest_item) = state.db.get_automation(&item.id)? else { continue; };
if latest_item.updated_at != item.updated_at { continue; } if !latest_item.enabled || latest_item.updated_at != item.updated_at
|| !state.db.flow_output_enabled(latest_item.flow_id.as_deref())?
{
continue;
}
item = latest_item; item = latest_item;
let mut effective_action = item.action.clone(); let mut effective_action = item.action.clone();
let mut effective_ha_data = item.action_ha_data.clone(); let mut effective_ha_data = item.action_ha_data.clone();
@@ -200,6 +221,8 @@ async fn run_automations(state: &AppState) -> Result<()> {
if let Some(device) = state.db.get_device(&zone.device_id)? { if let Some(device) = state.db.get_device(&zone.device_id)? {
if flow_zone_action_already_satisfied( if flow_zone_action_already_satisfied(
zone, zone,
item.flow_id.as_deref(),
item.flow_node_id.as_deref(),
device.power, device.power,
device.swing_vertical, device.swing_vertical,
device.swing_horizontal, device.swing_horizontal,
@@ -212,7 +235,9 @@ async fn run_automations(state: &AppState) -> Result<()> {
let before_runtime = item.flow_runtime.clone(); let before_runtime = item.flow_runtime.clone();
flow_acknowledge_change_gates(&mut item); flow_acknowledge_change_gates(&mut item);
if item.flow_runtime != before_runtime { if item.flow_runtime != before_runtime {
state.db.save_automation(&item)?; // The action lock prevents Flow edits here. A runtime-only
// update still guarantees we cannot overwrite enablement.
if !state.db.save_flow_runtime_if_current(&item)? { continue; }
} }
continue; continue;
} }
@@ -296,6 +321,7 @@ async fn run_automations(state: &AppState) -> Result<()> {
state, zone_id, item.action_zone_preset.as_deref(), &effective_action, state, zone_id, item.action_zone_preset.as_deref(), &effective_action,
item.action_zone_heat_anticipation_c, item.action_zone_standby_offset_c, item.action_zone_heat_anticipation_c, item.action_zone_standby_offset_c,
item.action_zone_simple_heat.as_ref(), item.action_zone_simple_heat.as_ref(),
item.flow_id.as_deref(), item.flow_node_id.as_deref(),
).await ).await
} else if let Some(group_id) = item.action_group_id.as_deref() { } else if let Some(group_id) = item.action_group_id.as_deref() {
let group_mode = effective_action.mode.as_deref().map(|mode| if mode == "auto" { "house".to_string() } else { mode.to_string() }); let group_mode = effective_action.mode.as_deref().map(|mode| if mode == "auto" { "house".to_string() } else { mode.to_string() });
@@ -306,7 +332,10 @@ async fn run_automations(state: &AppState) -> Result<()> {
setpoint: effective_action.target_temperature, setpoint: effective_action.target_temperature,
}, "automation.group").await.map(|value| !value.get("suppressed").and_then(Value::as_bool).unwrap_or(false)) }, "automation.group").await.map(|value| !value.get("suppressed").and_then(Value::as_bool).unwrap_or(false))
} else { } else {
match apply_automatic_device_action(state, &item.action_device_id, effective_action.clone()).await { match apply_automatic_device_action(
state, &item.action_device_id, effective_action.clone(),
item.flow_id.as_deref().zip(item.flow_node_id.as_deref()),
).await {
Ok(Some(_)) => Ok(true), Ok(Some(_)) => Ok(true),
Ok(None) => { Ok(None) => {
state.log("info", "automation.blocked_by_fresh_ownership", &format!("Automation {} was suppressed after ownership changed", item.name), json!({ state.log("info", "automation.blocked_by_fresh_ownership", &format!("Automation {} was suppressed after ownership changed", item.name), json!({
@@ -1128,6 +1157,7 @@ async fn apply_flow_zone_action(
state: &AppState, zone_id: &str, preset: Option<&str>, action: &DeviceCommand, state: &AppState, zone_id: &str, preset: Option<&str>, action: &DeviceCommand,
heat_anticipation_c: Option<f64>, standby_offset_c: Option<f64>, heat_anticipation_c: Option<f64>, standby_offset_c: Option<f64>,
simple_heat: Option<&SimpleHeatSettings>, simple_heat: Option<&SimpleHeatSettings>,
flow_id: Option<&str>, flow_node_id: Option<&str>,
) -> Result<bool, AppError> { ) -> Result<bool, AppError> {
let _schedule_guard = state.lock_schedule_operation().await; let _schedule_guard = state.lock_schedule_operation().await;
// Match the canonical thermostat ordering used by Web/HA zone control. This prevents // Match the canonical thermostat ordering used by Web/HA zone control. This prevents
@@ -1185,6 +1215,8 @@ async fn apply_flow_zone_action(
zone.updated_at = Utc::now(); zone.updated_at = Utc::now();
zone.control_owner = "automation".into(); zone.control_owner = "automation".into();
zone.control_source = "automation.flow".into(); zone.control_source = "automation.flow".into();
zone.flow_control_id = flow_id.map(str::to_string);
zone.flow_control_node_id = flow_node_id.map(str::to_string);
zone.control_reason = "Visual Flow automation".into(); zone.control_reason = "Visual Flow automation".into();
let schedules = state.db.list_schedules()?; let schedules = state.db.list_schedules()?;
let runtime_settings = state.settings.read().await; let runtime_settings = state.settings.read().await;
+4 -3
View File
@@ -311,9 +311,10 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
.as_deref() .as_deref()
.and_then(|id| devices.iter().find(|device| device.id == id)) .and_then(|id| devices.iter().find(|device| device.id == id))
.map(|device| device.name.clone()); .map(|device| device.name.clone());
let next_ready_at = item let next_ready_at = if item.enabled {
.last_fired_at item.last_fired_at
.map(|last| last + chrono::Duration::seconds(item.cooldown_seconds as i64)); .map(|last| last + chrono::Duration::seconds(item.cooldown_seconds as i64))
} else { None };
if item.enabled && item.trigger_kind == "time" { if item.enabled && item.trigger_kind == "time" {
if let Some(event) = next_time_automation_event(&item, &action_name, now) { if let Some(event) = next_time_automation_event(&item, &action_name, now) {
house_events.push(event); house_events.push(event);
+71
View File
@@ -3,6 +3,54 @@ mod tests {
use super::*; use super::*;
use chrono::TimeZone; use chrono::TimeZone;
#[test]
fn inactive_flow_releases_previous_zone_intent_and_compressor_task() {
let mut zone = test_zone("device");
zone.control_source = "automation.flow".into();
zone.flow_control_id = Some("flow-a".into());
zone.flow_control_node_id = Some("action-a".into());
zone.manual_preset = Some("custom".into());
zone.manual_setpoint = Some(22.0);
zone.compressor_pending_action = Some("power_on:heat:22.0".into());
let active: HashSet<(String, String, String)> = [
("flow-a".into(), "action-a".into(), zone.id.clone()),
].into_iter().collect();
assert!(!release_inactive_flow_zone_intent(&mut zone, &active));
assert_eq!(zone.manual_setpoint, Some(22.0));
assert!(release_inactive_flow_zone_intent(&mut zone, &HashSet::new()));
assert_eq!(zone.control_source, "automation");
assert!(zone.manual_preset.is_none());
assert!(zone.manual_setpoint.is_none());
assert!(zone.compressor_pending_action.is_none());
assert!(zone.flow_control_id.is_none());
}
#[test]
fn old_flow_state_and_manual_takeover_are_handled_safely() {
let mut zone = test_zone("device");
zone.control_source = "automation.flow".into();
zone.manual_preset = Some("comfort".into());
// A pre-upgrade zone has no flow_control_id: when no compiled Flow
// targets it, its previous automatic intent must also be released.
assert!(release_inactive_flow_zone_intent(&mut zone, &HashSet::new()));
assert!(zone.manual_preset.is_none());
zone.control_source = "web_thermostat".into();
zone.manual_setpoint = Some(24.0);
assert!(!release_inactive_flow_zone_intent(&mut zone, &HashSet::new()));
assert_eq!(zone.manual_setpoint, Some(24.0));
zone.control_source = "automation.flow".into();
zone.flow_control_id = Some("flow-old".into());
zone.flow_control_node_id = Some("old-action".into());
zone.manual_preset = Some("sleep".into());
let new_action: HashSet<(String, String, String)> = [
("flow-old".into(), "new-action".into(), zone.id.clone()),
].into_iter().collect();
assert!(release_inactive_flow_zone_intent(&mut zone, &new_action));
assert!(zone.manual_preset.is_none());
}
#[test] #[test]
fn heat_coasting_modulates_setpoint_without_power_cycle() { fn heat_coasting_modulates_setpoint_without_power_cycle() {
assert!(!heat_coasting_next_state(false, "heat", 19.8, 20.8, 0.9)); assert!(!heat_coasting_next_state(false, "heat", 19.8, 20.8, 0.9));
@@ -241,6 +289,9 @@ mod tests {
#[test] #[test]
fn flow_zone_action_noop_detects_persisted_on_intent_and_real_off_state() { fn flow_zone_action_noop_detects_persisted_on_intent_and_real_off_state() {
let mut zone = test_zone("device"); let mut zone = test_zone("device");
zone.control_source = "automation.flow".into();
zone.flow_control_id = Some("flow-test".into());
zone.flow_control_node_id = Some("action-test".into());
zone.inherit_house_mode = false; zone.inherit_house_mode = false;
zone.mode = "heat".into(); zone.mode = "heat".into();
zone.manual_preset = Some("custom".into()); zone.manual_preset = Some("custom".into());
@@ -254,6 +305,7 @@ mod tests {
}; };
assert!(flow_zone_action_already_satisfied( assert!(flow_zone_action_already_satisfied(
&zone, &zone,
Some("flow-test"), Some("action-test"),
false, false,
0, 0,
0, 0,
@@ -265,6 +317,7 @@ mod tests {
)); ));
assert!(!flow_zone_action_already_satisfied( assert!(!flow_zone_action_already_satisfied(
&zone, &zone,
Some("flow-test"), Some("action-test"),
false, false,
0, 0,
0, 0,
@@ -274,10 +327,19 @@ mod tests {
Some(5.0), Some(5.0),
None None
)); ));
// A matching pre-upgrade setpoint without a Flow owner is not enough
// to suppress the first real action; the action must claim ownership.
zone.flow_control_id = None;
assert!(!flow_zone_action_already_satisfied(
&zone, Some("flow-test"), Some("action-test"), false, 0, 0,
Some("custom"), &on, None, None, None,
));
zone.flow_control_id = Some("flow-test".into());
zone.heat_anticipation_c = 1.1; zone.heat_anticipation_c = 1.1;
zone.standby_offset_c = 5.0; zone.standby_offset_c = 5.0;
assert!(flow_zone_action_already_satisfied( assert!(flow_zone_action_already_satisfied(
&zone, &zone,
Some("flow-test"), Some("action-test"),
false, false,
0, 0,
0, 0,
@@ -301,6 +363,7 @@ mod tests {
}; };
assert!(flow_zone_action_already_satisfied( assert!(flow_zone_action_already_satisfied(
&zone, &zone,
Some("flow-test"), Some("action-test"),
false, false,
0, 0,
0, 0,
@@ -312,6 +375,7 @@ mod tests {
)); ));
assert!(!flow_zone_action_already_satisfied( assert!(!flow_zone_action_already_satisfied(
&zone, &zone,
Some("flow-test"), Some("action-test"),
true, true,
0, 0,
0, 0,
@@ -326,6 +390,9 @@ mod tests {
#[test] #[test]
fn flow_zone_action_noop_keeps_auxiliary_swing_changes_executable() { fn flow_zone_action_noop_keeps_auxiliary_swing_changes_executable() {
let mut zone = test_zone("device"); let mut zone = test_zone("device");
zone.control_source = "automation.flow".into();
zone.flow_control_id = Some("flow-test".into());
zone.flow_control_node_id = Some("action-test".into());
zone.inherit_house_mode = false; zone.inherit_house_mode = false;
zone.mode = "heat".into(); zone.mode = "heat".into();
zone.manual_preset = Some("custom".into()); zone.manual_preset = Some("custom".into());
@@ -339,6 +406,7 @@ mod tests {
}; };
assert!(!flow_zone_action_already_satisfied( assert!(!flow_zone_action_already_satisfied(
&zone, &zone,
Some("flow-test"), Some("action-test"),
true, true,
0, 0,
0, 0,
@@ -350,6 +418,7 @@ mod tests {
)); ));
assert!(flow_zone_action_already_satisfied( assert!(flow_zone_action_already_satisfied(
&zone, &zone,
Some("flow-test"), Some("action-test"),
true, true,
1, 1,
0, 0,
@@ -717,6 +786,8 @@ mod tests {
revision: 1, revision: 1,
control_owner: "automation".into(), control_owner: "automation".into(),
control_source: "automation".into(), control_source: "automation".into(),
flow_control_id: None,
flow_control_node_id: None,
control_since: Some(Utc::now()), control_since: Some(Utc::now()),
control_resume_at: None, control_resume_at: None,
control_reason: "test".into(), control_reason: "test".into(),
+3
View File
@@ -180,6 +180,7 @@ async fn apply_automatic_device_action(
state: &AppState, state: &AppState,
device_id: &str, device_id: &str,
command: DeviceCommand, command: DeviceCommand,
flow_source: Option<(&str, &str)>,
) -> Result<Option<Device>, AppError> { ) -> Result<Option<Device>, AppError> {
// Direct automation target/setpoint is translated into zone state and may use the next // Direct automation target/setpoint is translated into zone state and may use the next
// schedule boundary. Serialize that derivation with schedule edits before locking the zone. // schedule boundary. Serialize that derivation with schedule edits before locking the zone.
@@ -249,6 +250,8 @@ async fn apply_automatic_device_action(
zone.revision = zone.revision.saturating_add(1); zone.revision = zone.revision.saturating_add(1);
zone.updated_at = Utc::now(); zone.updated_at = Utc::now();
zone.control_source = "automation.device".into(); zone.control_source = "automation.device".into();
zone.flow_control_id = flow_source.map(|(id, _)| id.to_string());
zone.flow_control_node_id = flow_source.map(|(_, node)| node.to_string());
state.db.save_zone(&zone)?; state.db.save_zone(&zone)?;
state.broadcast("zone.updated", serde_json::to_value(&zone)?); state.broadcast("zone.updated", serde_json::to_value(&zone)?);
state.wake_zone_control(); state.wake_zone_control();
+105 -20
View File
@@ -533,6 +533,102 @@ async fn handle_zone_pre_control_state(
Ok(false) Ok(false)
} }
/// Flow actions are durable zone intent, not just one-shot physical commands. An
/// inactive/deleted Flow must release any remaining thermostat demand and compressor
/// task even if a previous version persisted an enabled compiled automation.
fn release_inactive_flow_zone_intent(
zone: &mut Zone,
active_actions: &HashSet<(String, String, String)>,
) -> bool {
let old_profile = zone.simple_heat.as_ref();
let stale_simple_heat = old_profile.is_some_and(|profile| {
!active_actions.contains(&(
profile.flow_id.clone(), profile.flow_node_id.clone(), zone.id.clone(),
))
});
let actual_owner = zone.flow_control_id.as_deref()
.zip(zone.flow_control_node_id.as_deref())
.or_else(|| old_profile.map(|profile| (profile.flow_id.as_str(), profile.flow_node_id.as_str())));
let active_owner = match actual_owner {
Some((flow_id, node_id)) => active_actions.contains(&(
flow_id.to_string(), node_id.to_string(), zone.id.clone(),
)),
None => active_actions.iter().any(|(_, _, zone_id)| zone_id == &zone.id),
};
// Never clear genuine manual/local/temporary control because a disabled Flow
// happened to be the previous owner of the same thermostat.
let flow_owned = zone.control_source == "automation.flow"
|| (zone.control_source == "automation.device" && zone.flow_control_id.is_some());
let stale_control = flow_owned
&& !zone.device_manual_override && zone.local_thermostat_power.is_none()
&& zone.temporary_quick_thermostat.is_none()
&& !active_owner;
if stale_simple_heat {
zone.simple_heat = None;
zone.simple_heat_runtime = SimpleHeatRuntime::default();
}
if stale_control {
zone.manual_preset = None;
zone.manual_setpoint = None;
zone.manual_override_until = None;
zone.flow_control_id = None;
zone.flow_control_node_id = None;
zone.control_source = "automation".into();
zone.control_reason = "Disabled/deleted Flow released thermostat control".into();
zone.control_since = Some(Utc::now());
zone.demand = false;
zone.demand_since = None;
zone.heat_coasting = false;
clear_compressor_pending(zone, true);
}
stale_control || stale_simple_heat
}
fn active_flow_zone_action_ids(state: &AppState) -> Result<HashSet<(String, String, String)>> {
let zone_by_device: HashMap<String, String> = state.db.list_zones()?
.into_iter().map(|zone| (zone.device_id, zone.id)).collect();
let mut owners = HashSet::new();
for rule in state.db.list_automations()?.into_iter().filter(|item| item.enabled) {
let (Some(flow_id), Some(node_id)) = (rule.flow_id, rule.flow_node_id) else { continue; };
// Both explicit Flow thermostat actions and Flow commands addressed to the
// underlying GREE device can own durable thermostat state.
let zone_id = rule.action_zone_id.or_else(|| {
if rule.action_group_id.is_some() || rule.action_ha_domain.is_some() {
None
} else {
zone_by_device.get(&rule.action_device_id).cloned()
}
});
if let Some(zone_id) = zone_id {
owners.insert((flow_id, node_id, zone_id));
}
}
Ok(owners)
}
/// Call only with the automation, schedule and thermostat-cycle locks held, in
/// that order (as in the Flow update/delete handlers).
pub(crate) async fn release_inactive_flow_intents(state: &AppState) -> Result<()> {
let active_actions = active_flow_zone_action_ids(state)?;
for item in state.db.list_zones()? {
if item.control_source != "automation.flow"
&& !(item.control_source == "automation.device" && item.flow_control_id.is_some())
&& item.simple_heat.is_none()
{
continue;
}
let _zone_guard = state.lock_zone_operation(&item.id).await;
let Some(mut zone) = state.db.get_zone(&item.id)? else { continue; };
if !release_inactive_flow_zone_intent(&mut zone, &active_actions) { continue; }
zone.revision = zone.revision.saturating_add(1);
zone.updated_at = Utc::now();
state.db.save_zone(&zone)?;
state.broadcast("zone.updated", serde_json::to_value(&zone)?);
state.log("info", "flow.zone_released", "Inactive Flow released thermostat intent", json!({"zone_id": zone.id}));
}
Ok(())
}
async fn control_zones(state: &AppState) -> Result<()> { async fn control_zones(state: &AppState) -> Result<()> {
let _cycle_guard = state.lock_zone_control_cycle().await; let _cycle_guard = state.lock_zone_control_cycle().await;
let settings = state.settings.read().await.clone(); let settings = state.settings.read().await.clone();
@@ -541,17 +637,8 @@ async fn control_zones(state: &AppState) -> Result<()> {
} }
let schedules = state.db.list_schedules()?; let schedules = state.db.list_schedules()?;
let mut zone_snapshot = state.db.list_zones()?; let mut zone_snapshot = state.db.list_zones()?;
// Only an enabled, compiled Flow action is allowed to retain simple heating // Flow ownership is valid only while its exact compiled action is enabled.
// ownership. Disabling, deleting or recompiling a Flow immediately removes let active_flow_actions = active_flow_zone_action_ids(state)?;
// stale heating profiles in the next control cycle.
let enabled_heat_actions: HashSet<(String, String, String)> = state.db.list_automations()?
.into_iter()
.filter(|item| item.enabled)
.filter_map(|item| {
let profile = item.action_zone_simple_heat?;
Some((profile.flow_id, profile.flow_node_id, item.action_zone_id?))
})
.collect();
// Local/temporary thermostat ownership has its own deadlines. Expire and activate sessions independently. // Local/temporary thermostat ownership has its own deadlines. Expire and activate sessions independently.
expire_local_thermostat_overrides(state, &mut zone_snapshot, &schedules, &settings.house_mode) expire_local_thermostat_overrides(state, &mut zone_snapshot, &schedules, &settings.house_mode)
.await?; .await?;
@@ -587,15 +674,13 @@ async fn control_zones(state: &AppState) -> Result<()> {
continue; continue;
}; };
let cycle_started_at = zone.updated_at.clone(); let cycle_started_at = zone.updated_at.clone();
if zone.simple_heat.as_ref().is_some_and(|profile| { if release_inactive_flow_zone_intent(&mut zone, &active_flow_actions) {
!enabled_heat_actions.contains(&(profile.flow_id.clone(), profile.flow_node_id.clone(), zone.id.clone())) // Persist even if this device is currently unavailable and the control
}) { // cycle would otherwise return early without saving the zone.
zone.simple_heat = None; zone.revision = zone.revision.saturating_add(1);
zone.simple_heat_runtime = SimpleHeatRuntime::default(); zone.updated_at = Utc::now();
if zone.control_source == "automation.flow" { state.db.save_zone(&zone)?;
zone.control_source = "automation".into(); state.broadcast("zone.updated", serde_json::to_value(&zone)?);
zone.control_reason = "Simple heating Flow disabled; thermostat idle".into();
}
} }
let outdoor_temperature = zone_outdoor_sensor_results let outdoor_temperature = zone_outdoor_sensor_results
.remove(&zone.id) .remove(&zone.id)
+6
View File
@@ -166,6 +166,12 @@ pub struct Zone {
pub control_owner: String, pub control_owner: String,
#[serde(default)] #[serde(default)]
pub control_source: String, pub control_source: String,
/// The last Flow action owning this zone. Missing on pre-0.16.4 databases.
/// Used to release durable thermostat intent when this exact action is disabled.
#[serde(default)]
pub flow_control_id: Option<String>,
#[serde(default)]
pub flow_control_node_id: Option<String>,
#[serde(default)] #[serde(default)]
pub control_since: Option<DateTime<Utc>>, pub control_since: Option<DateTime<Utc>>,
#[serde(default)] #[serde(default)]
+10 -4
View File
@@ -169,6 +169,12 @@ function planFlowTraceActual(rule, item, node) {
return JSON.stringify(actual); return JSON.stringify(actual);
} }
function planRuleEffectivelyEnabled(rule) {
if (!rule.enabled) return false;
// A stale pushed plan must never advertise a disabled/missing parent Flow.
return !rule.flow_id || (app.flows || []).some(flow => flow.id === rule.flow_id && flow.enabled && !flow.draft);
}
function planFlowTriggerTrace(rule, expanded = false) { function planFlowTriggerTrace(rule, expanded = false) {
const trace = Array.isArray(rule.last_trigger_trace) ? rule.last_trigger_trace : []; const trace = Array.isArray(rule.last_trigger_trace) ? rule.last_trigger_trace : [];
if (rule.trigger_kind !== 'flow' || !trace.length) return ''; if (rule.trigger_kind !== 'flow' || !trace.length) return '';
@@ -195,7 +201,7 @@ function planReasonCard(plan) {
}).join(''); }).join('');
const recentRules = (plan.rules || []) const recentRules = (plan.rules || [])
.filter(rule => rule.enabled && rule.last_fired_at) .filter(rule => planRuleEffectivelyEnabled(rule) && rule.last_fired_at)
.sort((a, b) => new Date(b.last_fired_at) - new Date(a.last_fired_at)) .sort((a, b) => new Date(b.last_fired_at) - new Date(a.last_fired_at))
.slice(0, 4) .slice(0, 4)
.map((rule, index) => { .map((rule, index) => {
@@ -284,7 +290,7 @@ function renderControlPlan() {
return `<article class="panel plan-card plan-zone ${planZoneEffectiveEnabled(zone) ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(scope)}</span><h3>${esc(zone.zone_name)}</h3></div><span class="badge ${zone.device_manual_override ? 'manual-override' : (planZoneEffectiveEnabled(zone) && zone.demand ? 'active' : '')}">${esc(planState)}</span></div><div class="plan-temp"><span>${fmtTemp(zone.current_temperature)}</span><b>→</b><strong>${esc(target)}<small>°C</small></strong></div><p>${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}</p><ul class="plan-events">${events.map(planEventMarkup).join('')}</ul></article>`; return `<article class="panel plan-card plan-zone ${planZoneEffectiveEnabled(zone) ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(scope)}</span><h3>${esc(zone.zone_name)}</h3></div><span class="badge ${zone.device_manual_override ? 'manual-override' : (planZoneEffectiveEnabled(zone) && zone.demand ? 'active' : '')}">${esc(planState)}</span></div><div class="plan-temp"><span>${fmtTemp(zone.current_temperature)}</span><b>→</b><strong>${esc(target)}<small>°C</small></strong></div><p>${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}</p><ul class="plan-events">${events.map(planEventMarkup).join('')}</ul></article>`;
}).join(''); }).join('');
const rules = (plan.rules || []).filter(rule => rule.enabled); const rules = (plan.rules || []).filter(planRuleEffectivelyEnabled);
const ruleCard = rules.length ? `<article class="panel plan-card plan-rules ${plan.emergency_stop_active ? 'disabled' : ''}"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.rules'))}</span><h3>${esc(tr(plan.emergency_stop_active ? 'plan.ruleCountPaused' : 'plan.ruleCount', { count: rules.length }))}</h3></div></div><ul class="plan-events">${rules.slice(0, 3).map(rule => { const ruleCard = rules.length ? `<article class="panel plan-card plan-rules ${plan.emergency_stop_active ? 'disabled' : ''}"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.rules'))}</span><h3>${esc(tr(plan.emergency_stop_active ? 'plan.ruleCountPaused' : 'plan.ruleCount', { count: rules.length }))}</h3></div></div><ul class="plan-events">${rules.slice(0, 3).map(rule => {
const automation = (app.automations || []).find(item => item.id === rule.id); const automation = (app.automations || []).find(item => item.id === rule.id);
const flow = automation?.flow_id ? (app.flows || []).find(item => item.id === automation.flow_id) : null; const flow = automation?.flow_id ? (app.flows || []).find(item => item.id === automation.flow_id) : null;
@@ -537,7 +543,7 @@ function renderSimulationPage() {
{ label: tr('simulation.houseMode'), value: houseModeLabel(plan.house_mode || 'off'), note: tr('simulation.strategy', { strategy: (plan.control_strategy || 'setpoint') === 'setpoint' ? tr('plan.strategySetpoint') : (plan.control_strategy || 'setpoint') }) }, { label: tr('simulation.houseMode'), value: houseModeLabel(plan.house_mode || 'off'), note: tr('simulation.strategy', { strategy: (plan.control_strategy || 'setpoint') === 'setpoint' ? tr('plan.strategySetpoint') : (plan.control_strategy || 'setpoint') }) },
{ label: tr('settings.nightMode'), value: nightOn ? tr('common.active') : (app.settings?.night_mode?.enabled ? tr('simulation.scheduled') : tr('common.disabled')), note: `${plan.night_mode_start || '22:00'} → ${plan.night_mode_end || '06:00'} · ${tr('simulation.fanMax')} ${fanLabel(plan.night_mode_max_fan_speed || 1)}` }, { label: tr('settings.nightMode'), value: nightOn ? tr('common.active') : (app.settings?.night_mode?.enabled ? tr('simulation.scheduled') : tr('common.disabled')), note: `${plan.night_mode_start || '22:00'} → ${plan.night_mode_end || '06:00'} · ${tr('simulation.fanMax')} ${fanLabel(plan.night_mode_max_fan_speed || 1)}` },
{ label: tr('simulation.outdoor'), value: plan.outdoor_temperature == null ? '—' : fmtTemp(plan.outdoor_temperature), note: tr('simulation.generatedAt', { time: dateTime(plan.generated_at) }) }, { label: tr('simulation.outdoor'), value: plan.outdoor_temperature == null ? '—' : fmtTemp(plan.outdoor_temperature), note: tr('simulation.generatedAt', { time: dateTime(plan.generated_at) }) },
{ label: tr('simulation.activeZones'), value: String(enabledZones.length), note: `${tr('simulation.requestingZones', { count: demandingZones.length })} · ${tr('simulation.rulesLabel')}: ${(plan.rules || []).filter(rule => rule.enabled && simulationRuleVisible(rule, zones)).length}` }, { label: tr('simulation.activeZones'), value: String(enabledZones.length), note: `${tr('simulation.requestingZones', { count: demandingZones.length })} · ${tr('simulation.rulesLabel')}: ${(plan.rules || []).filter(rule => planRuleEffectivelyEnabled(rule) && simulationRuleVisible(rule, zones)).length}` },
].map(card => `<article class="panel simulation-summary-card"><small>${esc(card.label)}</small><strong>${esc(card.value)}</strong><span>${esc(card.note)}</span></article>`).join(''); ].map(card => `<article class="panel simulation-summary-card"><small>${esc(card.label)}</small><strong>${esc(card.value)}</strong><span>${esc(card.note)}</span></article>`).join('');
const nodeW = 190, nodeH = 106, rowH = 190, topY = 155; const nodeW = 190, nodeH = 106, rowH = 190, topY = 155;
@@ -607,7 +613,7 @@ function renderSimulationPage() {
return `<article class="simulation-timeline-item"><time>${esc(simulationTime(item.event.at))}</time><div><strong>${esc(item.scope)}</strong><p>${esc(item.event.label)}${esc(target)}${item.device ? ` · ${esc(item.device)}` : ''}</p></div></article>`; return `<article class="simulation-timeline-item"><time>${esc(simulationTime(item.event.at))}</time><div><strong>${esc(item.scope)}</strong><p>${esc(item.event.label)}${esc(target)}${item.device ? ` · ${esc(item.device)}` : ''}</p></div></article>`;
}).join('') : `<div class="empty">${esc(tr('plan.noEvents'))}</div>`; }).join('') : `<div class="empty">${esc(tr('plan.noEvents'))}</div>`;
const activeRules = (plan.rules || []).filter(rule => rule.enabled && simulationRuleVisible(rule, zones)); const activeRules = (plan.rules || []).filter(rule => planRuleEffectivelyEnabled(rule) && simulationRuleVisible(rule, zones));
rulesHost.innerHTML = activeRules.length ? activeRules.map(rule => `<article class="simulation-rule-card"><div class="simulation-rule-head"><div><span class="eyebrow">${esc(tr('common.automation'))}</span><h3>${esc(rule.name)}</h3></div><span class="badge active">${esc(automationTriggerLabel(rule))}</span></div><p>${esc(tr('simulation.ruleOnDevice', { device: rule.action_device_name || tr('common.noDevice') }))}</p><div class="simulation-rule-action">${esc(simulationRuleAction(rule))}</div><div class="simulation-rule-meta"><small>${esc(tr('automations.last'))}: ${esc(dateTime(rule.last_fired_at))}</small><small>${esc(tr('simulation.nextReady'))}: ${esc(dateTime(rule.next_ready_at))}</small></div></article>`).join('') : `<div class="empty">${esc(tr('simulation.noRules'))}</div>`; rulesHost.innerHTML = activeRules.length ? activeRules.map(rule => `<article class="simulation-rule-card"><div class="simulation-rule-head"><div><span class="eyebrow">${esc(tr('common.automation'))}</span><h3>${esc(rule.name)}</h3></div><span class="badge active">${esc(automationTriggerLabel(rule))}</span></div><p>${esc(tr('simulation.ruleOnDevice', { device: rule.action_device_name || tr('common.noDevice') }))}</p><div class="simulation-rule-action">${esc(simulationRuleAction(rule))}</div><div class="simulation-rule-meta"><small>${esc(tr('automations.last'))}: ${esc(dateTime(rule.last_fired_at))}</small><small>${esc(tr('simulation.nextReady'))}: ${esc(dateTime(rule.next_ready_at))}</small></div></article>`).join('') : `<div class="empty">${esc(tr('simulation.noRules'))}</div>`;
} }
+2 -2
View File
@@ -96,8 +96,8 @@ async function handleWebSocketMessage(event) {
else if (message.event === 'schedule.template_applied') { try { app.schedules = await api('/api/schedules'); renderHouseClimate(); renderSchedules(); fillSelects(); } catch (_) { } scheduleControlPlanLoad(); } else if (message.event === 'schedule.template_applied') { try { app.schedules = await api('/api/schedules'); renderHouseClimate(); renderSchedules(); fillSelects(); } catch (_) { } scheduleControlPlanLoad(); }
else if (['automation.updated', 'automation.created'].includes(message.event)) { const i = app.automations.findIndex(v => v.id === data.id); if (i >= 0) app.automations[i] = data; else app.automations.push(data); renderHouseClimate(); renderAutomations(); fillSelects(); scheduleControlPlanLoad(); } else if (['automation.updated', 'automation.created'].includes(message.event)) { const i = app.automations.findIndex(v => v.id === data.id); if (i >= 0) app.automations[i] = data; else app.automations.push(data); renderHouseClimate(); renderAutomations(); fillSelects(); scheduleControlPlanLoad(); }
else if (message.event === 'automation.deleted') { app.automations = app.automations.filter(v => v.id !== data.id); renderHouseClimate(); renderAutomations(); fillSelects(); scheduleControlPlanLoad(); } else if (message.event === 'automation.deleted') { app.automations = app.automations.filter(v => v.id !== data.id); renderHouseClimate(); renderAutomations(); fillSelects(); scheduleControlPlanLoad(); }
else if (['flow.updated', 'flow.created'].includes(message.event)) { const i = app.flows.findIndex(v => v.id === data.id); if (i >= 0) app.flows[i] = data; else app.flows.push(data); renderHouseClimate(); renderFlows(); scheduleControlPlanLoad(); } else if (['flow.updated', 'flow.created'].includes(message.event)) { const i = app.flows.findIndex(v => v.id === data.id); if (i >= 0) app.flows[i] = data; else app.flows.push(data); renderHouseClimate(); renderFlows(); renderControlPlan(); renderSimulationPage(); scheduleControlPlanLoad(); }
else if (message.event === 'flow.deleted') { app.flows = app.flows.filter(v => v.id !== data.id); renderHouseClimate(); renderFlows(); scheduleControlPlanLoad(); } else if (message.event === 'flow.deleted') { app.flows = app.flows.filter(v => v.id !== data.id); renderHouseClimate(); renderFlows(); renderControlPlan(); renderSimulationPage(); scheduleControlPlanLoad(); }
else if (message.event === 'settings.application.updated') { applySettingsSection('application', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); renderSimulationModeBanner(); renderSystemInfo(); } else if (message.event === 'settings.application.updated') { applySettingsSection('application', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); renderSimulationModeBanner(); renderSystemInfo(); }
else if (message.event === 'settings.gree.updated') { applySettingsSection('gree', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); scheduleControlPlanLoad(); } else if (message.event === 'settings.gree.updated') { applySettingsSection('gree', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); scheduleControlPlanLoad(); }
else if (message.event === 'settings.gree_cloud.updated') { applySettingsSection('greeCloud', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); } else if (message.event === 'settings.gree_cloud.updated') { applySettingsSection('greeCloud', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); }