v0.16.4
This commit is contained in:
1 parent
05185a94f7
commit
bb5caa769f
28 files changed
+592
-130
No files matched your search
Generated
+1
-1
@@ -719,7 +719,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.16.3"
|
||||
version = "0.16.4"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.16.3"
|
||||
version = "0.16.4"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+33
-32
@@ -1,17 +1,17 @@
|
||||
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
|
||||
2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore
|
||||
2f663bd03e41e40d584183074dca077c8963afc8308bd66e7d33ea6aed6ec1e5 ./Cargo.lock
|
||||
8f4f035398326b1beec96d2c0d4b8fcf4b7f1542e39fe719743cc95ecca740e1 ./Cargo.toml
|
||||
147eed97b308e0c27d6a5ee738a0ee710fa6ba2c6bbf7503df0fe484e0c7f842 ./Cargo.lock
|
||||
c604af0060aef4e79d2e11fd1e1222a2444cebc835bc50629cff3d2373c5a05a ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
eaea820d2578f56442792e6fd6403eb1253686e50dbfcd8e75179c98574e1859 ./README.md
|
||||
2cc1518c6484c340037700ef258e27cdf71a7aa5233aefcb51cfa7c199c3f3a7 ./SOURCE_MANIFEST.sha256
|
||||
75a6652611273ae458cb3509401cdb196d1084db1e49496ecdce448f490e6249 ./README.md
|
||||
eedafc7faaa9eb95b564a249a4fef82d5f4cc32697eb0e370b96264f3c47b69c ./SOURCE_MANIFEST.sha256
|
||||
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
|
||||
e23bf048051f522364d213b0bd0ddfaff82d311a84542e20568dcf3f6aec7ffc ./docs/API.md
|
||||
c64564481d1a7bb4657f12b2fe0cbddf86050a4c6a2fc600ad8f02051387aa27 ./docs/FLOW.md
|
||||
bfb0bba73a02e71aba78c0f5de1fd41d51d70812f6e08eaf2eaceb1295908c9d ./docs/openapi.json
|
||||
a5ad73f7933031dfaa99aa92e27f10223d53fb84b14ee0a378eb5f10eb80e8c8 ./flows/simple-heating/igor-simple-heat-v5.flow.json
|
||||
9743f4328a18bc9d89d10a9011d13ca201a4f352db2119335a66a2147a9a486d ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json
|
||||
2d804831a72577e80dc1ec3cf4f86dc8bcf00b1d8c80929896f11da0230b2b07 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json
|
||||
bb9840a0dc2b89d057d1461ae581ee61ebf856e3b6d38bc7d1bdd111eef7c550 ./docs/API.md
|
||||
4f9826f1bf3b42ed2d62d8a8e9221b50175b3a9fdda7e9d219351f7a33039257 ./docs/FLOW.md
|
||||
52c8dc48aed987fd28b2b543af1e5a782efb2a44104ccb0fd6b1740255cf384e ./docs/openapi.json
|
||||
d8a1b3e34cf8a79ac3296bf600ad47826d639cdef4c1c2c099f8b6b21a391ebd ./flows/simple-heating/igor-simple-heat-v5.flow.json
|
||||
bd9a07b8ca4b090ad0089e9037492cc25c47f3456b97ec08152947314ea904a6 ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json
|
||||
c4e6e3e8df5eb7f9730a79fb90edd155f0510f7bb4862a04b9f35ba649c22376 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json
|
||||
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
|
||||
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
|
||||
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
|
||||
@@ -25,7 +25,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
|
||||
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
|
||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
|
||||
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
|
||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
|
||||
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
|
||||
@@ -33,10 +33,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
|
||||
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
|
||||
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
|
||||
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
|
||||
1b32f617ed09f37d58fd8532107cfcbf2fb92c4434bd173b0fc73464664ce927 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
904b21f54c7af0f59d91016d8513baaea5e927a349e18207f4290c51d7f2be8a ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
0e92e538aa03b442f8fc2c21562cc7aa19d8ae346f78cecca1eec131288edade ./ha-addon/repository/gree-controller/README.md
|
||||
e48d172951db6df8afe97b0d463902e744c22bd3e677041bf5e6ee5624646f25 ./ha-addon/repository/gree-controller/config.yaml
|
||||
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md
|
||||
efe06cb35044b19474ae11e5ef42e3201787ff1d164087cdbdb845a5ad6a16d8 ./ha-addon/repository/gree-controller/config.yaml
|
||||
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
|
||||
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
|
||||
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
|
||||
@@ -101,19 +101,20 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
36396ecfcfd30f50016b17a83cd76cbccd8debaacef45a0d6b4e61946fef098d ./scripts/smoke.sh
|
||||
b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js
|
||||
e3825cfd9e6e149fd227c521d886dc116170f2553d4d7639d61874267f257b6e ./scripts/test_flow_disable_ui.js
|
||||
2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
14f07768257945ed52c218f3bf03c25cb55053e7a6683c8ff17f395e16d28f2e ./scripts/verify_simple_heating.py
|
||||
c260a6d291ad913f6aca60f23a0acf6928d68cfb0545b2a698d58c5a95fcf934 ./scripts/verify_simple_heating.py
|
||||
c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs
|
||||
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
|
||||
1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs
|
||||
34378c387d34d67483030d1016b874a74115c1ef5f268f854180c25146fdb08b ./src/api/automations.rs
|
||||
69ce9a3e0cc3b805e605beb811ee8fd80c37cc38a552896500bf522dccdbb23e ./src/api/automations.rs
|
||||
fffad7874465739cd6b508e9d7791387b864fa88420ce0f7e2fc933d129a2751 ./src/api/configuration.rs
|
||||
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
|
||||
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
|
||||
8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs
|
||||
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
|
||||
8c5d46ccaa45950f4dbaae63e68ad6efe393ff0904e46420f5e7950f043fb691 ./src/api/flows.rs
|
||||
c7a01af66fe69de6dda6ca0d3bf4489ab3f7c1181853bd394340cdc8dc65c7fc ./src/api/flows.rs
|
||||
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs
|
||||
@@ -122,11 +123,11 @@ b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/hist
|
||||
7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs
|
||||
9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs
|
||||
b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs
|
||||
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
|
||||
1d51fe0def463fd0d1a9a97f66f362f3f8bd24daafb14993f3105b1a8ffed5b6 ./src/api/schedules.rs
|
||||
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
|
||||
7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs
|
||||
3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs
|
||||
7c65692070e51af1ebd66d98b087b7f7a241ed31f8ff2f41baf77e46e027688c ./src/api/zones.rs
|
||||
eaa4070afbc19686d5e9bdb2d24fb2c7fc4bf1f90dc1c4f12fef17017d4cb975 ./src/api/zones.rs
|
||||
32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs
|
||||
de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs
|
||||
87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs
|
||||
@@ -135,17 +136,17 @@ a8fe3636ac79df191e2f2c9d8c1eec1e5181e048ef8c9036cfa18f84aef1a462 ./src/db/confi
|
||||
4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs
|
||||
7bc79d9d33726fbdd33b485c8c2e839cedbb8fe29fee872611da978ed149d6a0 ./src/db/energy_history.rs
|
||||
96aac6510ddda8c18fd7e29802a937b96e8a74badf504980846c80cd7933a4c6 ./src/db/events_tokens.rs
|
||||
bba6e3fb9f4800ae06d0ca67772b06a8c389d308243306719442d0c1f2623420 ./src/db/flows.rs
|
||||
a951422c51073ad8131edb9c0d264ce04c7cd71e62da7412619d67b774472ecf ./src/db/flows.rs
|
||||
5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs
|
||||
4fc007c08304e96818ad1d78997a5b2458e31aa9c48ad68b5be2a007533924bb ./src/db/network_history.rs
|
||||
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
|
||||
28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs
|
||||
253b0c77897a848f93452ddf809c794373ec4942203838a51976805f50e2aa1b ./src/db/schedules_automations.rs
|
||||
51ecf9c0a2554bfd599ee9881ff17a88f184d3cfa2cd57f3166bbb1dd544be2d ./src/db/tests.rs
|
||||
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
|
||||
7b29709d2a97e201a6f7baab9c3a72ea3792488499c47e426ebb93369fcbb3fc ./src/engine.rs
|
||||
4412770793e4905d490a56e0f46cc678a5e28044fc0d136912ef1b81da3e3e3f ./src/engine/automations.rs
|
||||
e6d6bf39e254bd2779d3bcb31d966aa3f6688ebfa91b4088b70fd796cad66596 ./src/engine/automations.rs
|
||||
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
|
||||
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
|
||||
dbf73f1459185836487653d05e73ffd8314fb0244d23878fc84232fd10935b8a ./src/engine/control_plan.rs
|
||||
62a15ea757c53796149e670863dae1b4e1a16ceb6ef88ed84ebd3089c0b9880d ./src/engine/control_plan.rs
|
||||
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
|
||||
519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs
|
||||
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
|
||||
@@ -159,9 +160,9 @@ c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/r
|
||||
2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs
|
||||
d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs
|
||||
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
|
||||
6f81f1b93f653b8450b9e086ed4728eca90b3d5d7639b7f4fb4fa2f42c514a28 ./src/engine/tests.rs
|
||||
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
|
||||
2cbfc34ad0868591e60e3110d8ae346dff07a58c894e87598e4b7b5749fb4613 ./src/engine/zone_control.rs
|
||||
4160811f47eca6e75970fe6c6b95198f86a4b97b7050531c19a19efb3d76d97d ./src/engine/tests.rs
|
||||
07c3c462884344f018654267de3d454d895343b4699e34635e4d1ec88546df0a ./src/engine/zone_actions.rs
|
||||
e246b6d6e3126941b989735a27e5fbe88dcaf351420aaaa060bb3761620d8244 ./src/engine/zone_control.rs
|
||||
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
|
||||
d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs
|
||||
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
|
||||
@@ -180,7 +181,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f
|
||||
17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs
|
||||
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
|
||||
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
|
||||
5229bdb5ce1018601b081654d16cee9ac28afdfda6c67effe1b14d44b14b3257 ./src/models/zone.rs
|
||||
79cb16a9f12e8a20fcf0acc8649d771b8a76a05503c0372443d29003536c1bcb ./src/models/zone.rs
|
||||
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
|
||||
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
|
||||
578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
|
||||
@@ -206,15 +207,15 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
282ef3c84f053cfce36fa85828365cac2cb6dfa4375993345ff3bee05c8a884f ./web/css/styles.css
|
||||
b030e61b30435495f492e15fa9daf228968cc1de2982e8f40a626452dbf5b81a ./web/css/styles.css
|
||||
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
e76b1e0bbdad0297d5ae2e79e40e658e0263f038ab70db81d330a342e01a0a8b ./web/index.html
|
||||
bbe640e99447f632383b2eb4b52da580ab95d60cd45cda47f6fae1c2380e5b8a ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
|
||||
e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js
|
||||
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
|
||||
07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js
|
||||
008a85cddf6a5357eff35764eabddf1d7baaecf949b481d195124de50431e0d2 ./web/js-dynamic/dashboard.js
|
||||
ef20fb1e706105d0afdc2fa505d92ea52e43d93c9a687f2a09d381227a464564 ./web/js-dynamic/dashboard.js
|
||||
3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./web/js-dynamic/entities.js
|
||||
bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js
|
||||
3e2a5f1aab56e918624e6e7af064eecb9e94e554fd4d185715f8ecf3747a6895 ./web/js-dynamic/flows.js
|
||||
@@ -222,7 +223,7 @@ bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynam
|
||||
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
|
||||
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.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
|
||||
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
|
||||
bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
|
||||
|
||||
**Version: 0.16.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.
|
||||
> Built-in API: `/api/*` and `/ws`; interactive Swagger docs: `/api-docs`; OpenAPI JSON: `/api-docs/openapi.json`.
|
||||
|
||||
+32
-31
@@ -1,16 +1,16 @@
|
||||
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
|
||||
2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore
|
||||
2f663bd03e41e40d584183074dca077c8963afc8308bd66e7d33ea6aed6ec1e5 ./Cargo.lock
|
||||
8f4f035398326b1beec96d2c0d4b8fcf4b7f1542e39fe719743cc95ecca740e1 ./Cargo.toml
|
||||
147eed97b308e0c27d6a5ee738a0ee710fa6ba2c6bbf7503df0fe484e0c7f842 ./Cargo.lock
|
||||
c604af0060aef4e79d2e11fd1e1222a2444cebc835bc50629cff3d2373c5a05a ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
eaea820d2578f56442792e6fd6403eb1253686e50dbfcd8e75179c98574e1859 ./README.md
|
||||
75a6652611273ae458cb3509401cdb196d1084db1e49496ecdce448f490e6249 ./README.md
|
||||
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
|
||||
e23bf048051f522364d213b0bd0ddfaff82d311a84542e20568dcf3f6aec7ffc ./docs/API.md
|
||||
c64564481d1a7bb4657f12b2fe0cbddf86050a4c6a2fc600ad8f02051387aa27 ./docs/FLOW.md
|
||||
bfb0bba73a02e71aba78c0f5de1fd41d51d70812f6e08eaf2eaceb1295908c9d ./docs/openapi.json
|
||||
a5ad73f7933031dfaa99aa92e27f10223d53fb84b14ee0a378eb5f10eb80e8c8 ./flows/simple-heating/igor-simple-heat-v5.flow.json
|
||||
9743f4328a18bc9d89d10a9011d13ca201a4f352db2119335a66a2147a9a486d ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json
|
||||
2d804831a72577e80dc1ec3cf4f86dc8bcf00b1d8c80929896f11da0230b2b07 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json
|
||||
bb9840a0dc2b89d057d1461ae581ee61ebf856e3b6d38bc7d1bdd111eef7c550 ./docs/API.md
|
||||
4f9826f1bf3b42ed2d62d8a8e9221b50175b3a9fdda7e9d219351f7a33039257 ./docs/FLOW.md
|
||||
52c8dc48aed987fd28b2b543af1e5a782efb2a44104ccb0fd6b1740255cf384e ./docs/openapi.json
|
||||
d8a1b3e34cf8a79ac3296bf600ad47826d639cdef4c1c2c099f8b6b21a391ebd ./flows/simple-heating/igor-simple-heat-v5.flow.json
|
||||
bd9a07b8ca4b090ad0089e9037492cc25c47f3456b97ec08152947314ea904a6 ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json
|
||||
c4e6e3e8df5eb7f9730a79fb90edd155f0510f7bb4862a04b9f35ba649c22376 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json
|
||||
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
|
||||
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
|
||||
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
|
||||
@@ -24,7 +24,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
|
||||
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
|
||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
|
||||
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
|
||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
|
||||
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
|
||||
@@ -32,10 +32,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
|
||||
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
|
||||
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
|
||||
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
|
||||
1b32f617ed09f37d58fd8532107cfcbf2fb92c4434bd173b0fc73464664ce927 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
904b21f54c7af0f59d91016d8513baaea5e927a349e18207f4290c51d7f2be8a ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
0e92e538aa03b442f8fc2c21562cc7aa19d8ae346f78cecca1eec131288edade ./ha-addon/repository/gree-controller/README.md
|
||||
e48d172951db6df8afe97b0d463902e744c22bd3e677041bf5e6ee5624646f25 ./ha-addon/repository/gree-controller/config.yaml
|
||||
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md
|
||||
efe06cb35044b19474ae11e5ef42e3201787ff1d164087cdbdb845a5ad6a16d8 ./ha-addon/repository/gree-controller/config.yaml
|
||||
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
|
||||
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
|
||||
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
|
||||
@@ -100,19 +100,20 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
36396ecfcfd30f50016b17a83cd76cbccd8debaacef45a0d6b4e61946fef098d ./scripts/smoke.sh
|
||||
b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js
|
||||
e3825cfd9e6e149fd227c521d886dc116170f2553d4d7639d61874267f257b6e ./scripts/test_flow_disable_ui.js
|
||||
2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
14f07768257945ed52c218f3bf03c25cb55053e7a6683c8ff17f395e16d28f2e ./scripts/verify_simple_heating.py
|
||||
c260a6d291ad913f6aca60f23a0acf6928d68cfb0545b2a698d58c5a95fcf934 ./scripts/verify_simple_heating.py
|
||||
c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs
|
||||
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
|
||||
1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs
|
||||
34378c387d34d67483030d1016b874a74115c1ef5f268f854180c25146fdb08b ./src/api/automations.rs
|
||||
69ce9a3e0cc3b805e605beb811ee8fd80c37cc38a552896500bf522dccdbb23e ./src/api/automations.rs
|
||||
fffad7874465739cd6b508e9d7791387b864fa88420ce0f7e2fc933d129a2751 ./src/api/configuration.rs
|
||||
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
|
||||
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
|
||||
8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs
|
||||
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
|
||||
8c5d46ccaa45950f4dbaae63e68ad6efe393ff0904e46420f5e7950f043fb691 ./src/api/flows.rs
|
||||
c7a01af66fe69de6dda6ca0d3bf4489ab3f7c1181853bd394340cdc8dc65c7fc ./src/api/flows.rs
|
||||
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs
|
||||
@@ -121,11 +122,11 @@ b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/hist
|
||||
7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs
|
||||
9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs
|
||||
b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs
|
||||
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
|
||||
1d51fe0def463fd0d1a9a97f66f362f3f8bd24daafb14993f3105b1a8ffed5b6 ./src/api/schedules.rs
|
||||
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
|
||||
7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs
|
||||
3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs
|
||||
7c65692070e51af1ebd66d98b087b7f7a241ed31f8ff2f41baf77e46e027688c ./src/api/zones.rs
|
||||
eaa4070afbc19686d5e9bdb2d24fb2c7fc4bf1f90dc1c4f12fef17017d4cb975 ./src/api/zones.rs
|
||||
32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs
|
||||
de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs
|
||||
87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs
|
||||
@@ -134,17 +135,17 @@ a8fe3636ac79df191e2f2c9d8c1eec1e5181e048ef8c9036cfa18f84aef1a462 ./src/db/confi
|
||||
4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs
|
||||
7bc79d9d33726fbdd33b485c8c2e839cedbb8fe29fee872611da978ed149d6a0 ./src/db/energy_history.rs
|
||||
96aac6510ddda8c18fd7e29802a937b96e8a74badf504980846c80cd7933a4c6 ./src/db/events_tokens.rs
|
||||
bba6e3fb9f4800ae06d0ca67772b06a8c389d308243306719442d0c1f2623420 ./src/db/flows.rs
|
||||
a951422c51073ad8131edb9c0d264ce04c7cd71e62da7412619d67b774472ecf ./src/db/flows.rs
|
||||
5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs
|
||||
4fc007c08304e96818ad1d78997a5b2458e31aa9c48ad68b5be2a007533924bb ./src/db/network_history.rs
|
||||
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
|
||||
28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs
|
||||
253b0c77897a848f93452ddf809c794373ec4942203838a51976805f50e2aa1b ./src/db/schedules_automations.rs
|
||||
51ecf9c0a2554bfd599ee9881ff17a88f184d3cfa2cd57f3166bbb1dd544be2d ./src/db/tests.rs
|
||||
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
|
||||
7b29709d2a97e201a6f7baab9c3a72ea3792488499c47e426ebb93369fcbb3fc ./src/engine.rs
|
||||
4412770793e4905d490a56e0f46cc678a5e28044fc0d136912ef1b81da3e3e3f ./src/engine/automations.rs
|
||||
e6d6bf39e254bd2779d3bcb31d966aa3f6688ebfa91b4088b70fd796cad66596 ./src/engine/automations.rs
|
||||
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
|
||||
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
|
||||
dbf73f1459185836487653d05e73ffd8314fb0244d23878fc84232fd10935b8a ./src/engine/control_plan.rs
|
||||
62a15ea757c53796149e670863dae1b4e1a16ceb6ef88ed84ebd3089c0b9880d ./src/engine/control_plan.rs
|
||||
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
|
||||
519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs
|
||||
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
|
||||
@@ -158,9 +159,9 @@ c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/r
|
||||
2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs
|
||||
d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs
|
||||
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
|
||||
6f81f1b93f653b8450b9e086ed4728eca90b3d5d7639b7f4fb4fa2f42c514a28 ./src/engine/tests.rs
|
||||
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
|
||||
2cbfc34ad0868591e60e3110d8ae346dff07a58c894e87598e4b7b5749fb4613 ./src/engine/zone_control.rs
|
||||
4160811f47eca6e75970fe6c6b95198f86a4b97b7050531c19a19efb3d76d97d ./src/engine/tests.rs
|
||||
07c3c462884344f018654267de3d454d895343b4699e34635e4d1ec88546df0a ./src/engine/zone_actions.rs
|
||||
e246b6d6e3126941b989735a27e5fbe88dcaf351420aaaa060bb3761620d8244 ./src/engine/zone_control.rs
|
||||
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
|
||||
d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs
|
||||
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
|
||||
@@ -179,7 +180,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f
|
||||
17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs
|
||||
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
|
||||
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
|
||||
5229bdb5ce1018601b081654d16cee9ac28afdfda6c67effe1b14d44b14b3257 ./src/models/zone.rs
|
||||
79cb16a9f12e8a20fcf0acc8649d771b8a76a05503c0372443d29003536c1bcb ./src/models/zone.rs
|
||||
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
|
||||
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
|
||||
578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
|
||||
@@ -205,15 +206,15 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
282ef3c84f053cfce36fa85828365cac2cb6dfa4375993345ff3bee05c8a884f ./web/css/styles.css
|
||||
b030e61b30435495f492e15fa9daf228968cc1de2982e8f40a626452dbf5b81a ./web/css/styles.css
|
||||
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
e76b1e0bbdad0297d5ae2e79e40e658e0263f038ab70db81d330a342e01a0a8b ./web/index.html
|
||||
bbe640e99447f632383b2eb4b52da580ab95d60cd45cda47f6fae1c2380e5b8a ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
|
||||
e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js
|
||||
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
|
||||
07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js
|
||||
008a85cddf6a5357eff35764eabddf1d7baaecf949b481d195124de50431e0d2 ./web/js-dynamic/dashboard.js
|
||||
ef20fb1e706105d0afdc2fa505d92ea52e43d93c9a687f2a09d381227a464564 ./web/js-dynamic/dashboard.js
|
||||
3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./web/js-dynamic/entities.js
|
||||
bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js
|
||||
3e2a5f1aab56e918624e6e7af064eecb9e94e554fd4d185715f8ecf3747a6895 ./web/js-dynamic/flows.js
|
||||
@@ -221,7 +222,7 @@ bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynam
|
||||
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
|
||||
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.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
|
||||
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
|
||||
bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
# 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.
|
||||
|
||||
@@ -248,7 +248,7 @@ Response:
|
||||
{
|
||||
"status": "ok",
|
||||
"name": "gree-controller",
|
||||
"version": "0.16.3",
|
||||
"version": "0.16.4",
|
||||
"uptime_seconds": 1234,
|
||||
"control_ready": true,
|
||||
"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_revision": 42,
|
||||
"system": {
|
||||
"version": "0.16.3",
|
||||
"version": "0.16.4",
|
||||
"uptime_seconds": 1234,
|
||||
"auth_required": false,
|
||||
"control_ready": true,
|
||||
|
||||
@@ -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.
|
||||
- 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
|
||||
|
||||
Structural Flow writes use the global order:
|
||||
|
||||
+16
-2
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "GREE Controller API",
|
||||
"version": "0.16.3",
|
||||
"version": "0.16.4",
|
||||
"summary": "Local HTTP/WebSocket API for GREE Controller",
|
||||
"description": "Local API used by the GREE Controller Web UI and Home Assistant integration.",
|
||||
"license": {
|
||||
@@ -6079,7 +6079,7 @@
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "0.16.3"
|
||||
"example": "0.16.4"
|
||||
},
|
||||
"uptime_seconds": {
|
||||
"type": "integer",
|
||||
@@ -7380,6 +7380,20 @@
|
||||
"heat_coasting": {
|
||||
"type": "boolean",
|
||||
"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": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.16.3",
|
||||
"version": "0.16.4",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# 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
|
||||
|
||||
- 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"
|
||||
version: "0.16.3"
|
||||
version: "0.16.4"
|
||||
slug: "gree_controller"
|
||||
description: "Local GREE HVAC controller with Web UI, API, and Home Assistant integration"
|
||||
url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/"
|
||||
|
||||
@@ -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');
|
||||
@@ -78,7 +78,7 @@ def main() -> None:
|
||||
for value in ("zone_heat_cycle", "boost_after_minutes", "day_off_after_minutes"):
|
||||
assert value in rust and value in ui, value
|
||||
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 '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()
|
||||
|
||||
@@ -243,11 +243,12 @@ async fn get_automation(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<Automation>, AppError> {
|
||||
state
|
||||
.db
|
||||
.get_automation(&id)?
|
||||
.map(Json)
|
||||
.ok_or_else(|| AppError::NotFound(format!("automation {id}")))
|
||||
let mut item = state.db.get_automation(&id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("automation {id}")))?;
|
||||
if !state.db.flow_output_enabled(item.flow_id.as_deref())? {
|
||||
item.enabled = false;
|
||||
}
|
||||
Ok(Json(item))
|
||||
}
|
||||
async fn create_automation(
|
||||
State(state): State<AppState>,
|
||||
|
||||
+15
-7
@@ -1365,6 +1365,11 @@ fn compile_flow(
|
||||
let zones = state.db.list_zones()?;
|
||||
let groups = state.db.list_groups()?;
|
||||
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
|
||||
.db
|
||||
.list_schedules()?
|
||||
@@ -1504,13 +1509,13 @@ fn compile_flow(
|
||||
.get(&id)
|
||||
.map(|item| item.created_at.clone())
|
||||
.unwrap_or_else(|| now.clone());
|
||||
let last_fired_at = previous_automations
|
||||
.get(&id)
|
||||
.and_then(|item| item.last_fired_at.clone());
|
||||
let last_trigger_trace = previous_automations
|
||||
.get(&id)
|
||||
.map(|item| item.last_trigger_trace.clone())
|
||||
.unwrap_or_default();
|
||||
let last_fired_at = if resumed { None } else {
|
||||
previous_automations.get(&id).and_then(|item| item.last_fired_at.clone())
|
||||
};
|
||||
let last_trigger_trace = if reset_flow_runtime { Vec::new() } else {
|
||||
previous_automations.get(&id)
|
||||
.map(|item| item.last_trigger_trace.clone()).unwrap_or_default()
|
||||
};
|
||||
let flow_runtime = previous_automations
|
||||
.get(&id)
|
||||
.map(|item| {
|
||||
@@ -1547,6 +1552,7 @@ fn compile_flow(
|
||||
runtime
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let flow_runtime = if reset_flow_runtime { Default::default() } else { flow_runtime };
|
||||
let mut item = Automation {
|
||||
id,
|
||||
name: generated_flow_name(&flow.id, &action_node.id),
|
||||
@@ -1975,6 +1981,7 @@ async fn update_flow(
|
||||
state
|
||||
.db
|
||||
.replace_flow_outputs(&flow, &schedules, &automations)?;
|
||||
engine::release_inactive_flow_intents(&state).await?;
|
||||
let mut zone_ids = old_zone_ids;
|
||||
zone_ids.extend(schedules.iter().map(|s| s.zone_id.clone()));
|
||||
for zone_id in zone_ids {
|
||||
@@ -2008,6 +2015,7 @@ async fn delete_flow(
|
||||
if !state.db.delete_flow(&id)? {
|
||||
return Err(AppError::NotFound(format!("flow {id}")));
|
||||
}
|
||||
engine::release_inactive_flow_intents(&state).await?;
|
||||
for zone_id in zone_ids {
|
||||
refresh_zone_override_boundary(&state, &zone_id).await?;
|
||||
}
|
||||
|
||||
@@ -158,11 +158,12 @@ async fn get_schedule(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<Schedule>, AppError> {
|
||||
state
|
||||
.db
|
||||
.get_schedule(&id)?
|
||||
.map(Json)
|
||||
.ok_or_else(|| AppError::NotFound(format!("schedule {id}")))
|
||||
let mut item = state.db.get_schedule(&id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("schedule {id}")))?;
|
||||
if !state.db.flow_output_enabled(item.flow_id.as_deref())? {
|
||||
item.enabled = false;
|
||||
}
|
||||
Ok(Json(item))
|
||||
}
|
||||
async fn create_schedule(
|
||||
State(state): State<AppState>,
|
||||
|
||||
@@ -246,6 +246,8 @@ impl ZoneInput {
|
||||
revision: 1,
|
||||
control_owner: "automation".into(),
|
||||
control_source: "automation".into(),
|
||||
flow_control_id: None,
|
||||
flow_control_node_id: None,
|
||||
control_since: Some(Utc::now()),
|
||||
control_resume_at: None,
|
||||
control_reason: "zone created".into(),
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
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>> {
|
||||
self.list_payloads(queries::LIST_FLOWS)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,16 @@ impl Db {
|
||||
}
|
||||
|
||||
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>> {
|
||||
@@ -57,13 +66,46 @@ impl Db {
|
||||
}
|
||||
|
||||
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>> {
|
||||
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(¤t)?],
|
||||
)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn delete_automation(&self, id: &str) -> Result<bool> {
|
||||
self.delete_by_id("automations", id)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,105 @@ mod tests {
|
||||
use super::*;
|
||||
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]
|
||||
fn history_compaction_keeps_one_sample_per_old_bucket() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -26,6 +26,8 @@ fn flow_off_physical_mode(action: &DeviceCommand) -> Option<String> {
|
||||
|
||||
fn flow_zone_action_already_satisfied(
|
||||
zone: &Zone,
|
||||
flow_id: Option<&str>,
|
||||
flow_node_id: Option<&str>,
|
||||
device_power: bool,
|
||||
device_swing_vertical: u8,
|
||||
device_swing_horizontal: u8,
|
||||
@@ -35,9 +37,18 @@ fn flow_zone_action_already_satisfied(
|
||||
standby_offset_c: Option<f64>,
|
||||
simple_heat: Option<&SimpleHeatSettings>,
|
||||
) -> 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 {
|
||||
return zone.simple_heat.as_ref() == Some(profile)
|
||||
&& zone.control_source == "automation.flow";
|
||||
return zone.simple_heat.as_ref() == Some(profile);
|
||||
}
|
||||
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)
|
||||
@@ -115,7 +126,13 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
"flow" => {
|
||||
let before_runtime = item.flow_runtime.clone();
|
||||
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 {
|
||||
Ok((value, trace)) => {
|
||||
trigger_trace = trace;
|
||||
@@ -146,7 +163,11 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
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;
|
||||
let mut effective_action = item.action.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 flow_zone_action_already_satisfied(
|
||||
zone,
|
||||
item.flow_id.as_deref(),
|
||||
item.flow_node_id.as_deref(),
|
||||
device.power,
|
||||
device.swing_vertical,
|
||||
device.swing_horizontal,
|
||||
@@ -212,7 +235,9 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
let before_runtime = item.flow_runtime.clone();
|
||||
flow_acknowledge_change_gates(&mut item);
|
||||
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;
|
||||
}
|
||||
@@ -296,6 +321,7 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
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_simple_heat.as_ref(),
|
||||
item.flow_id.as_deref(), item.flow_node_id.as_deref(),
|
||||
).await
|
||||
} 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() });
|
||||
@@ -306,7 +332,10 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
setpoint: effective_action.target_temperature,
|
||||
}, "automation.group").await.map(|value| !value.get("suppressed").and_then(Value::as_bool).unwrap_or(false))
|
||||
} 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(None) => {
|
||||
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,
|
||||
heat_anticipation_c: Option<f64>, standby_offset_c: Option<f64>,
|
||||
simple_heat: Option<&SimpleHeatSettings>,
|
||||
flow_id: Option<&str>, flow_node_id: Option<&str>,
|
||||
) -> Result<bool, AppError> {
|
||||
let _schedule_guard = state.lock_schedule_operation().await;
|
||||
// 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.control_owner = "automation".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();
|
||||
let schedules = state.db.list_schedules()?;
|
||||
let runtime_settings = state.settings.read().await;
|
||||
|
||||
@@ -311,9 +311,10 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
.as_deref()
|
||||
.and_then(|id| devices.iter().find(|device| device.id == id))
|
||||
.map(|device| device.name.clone());
|
||||
let next_ready_at = item
|
||||
.last_fired_at
|
||||
.map(|last| last + chrono::Duration::seconds(item.cooldown_seconds as i64));
|
||||
let next_ready_at = if item.enabled {
|
||||
item.last_fired_at
|
||||
.map(|last| last + chrono::Duration::seconds(item.cooldown_seconds as i64))
|
||||
} else { None };
|
||||
if item.enabled && item.trigger_kind == "time" {
|
||||
if let Some(event) = next_time_automation_event(&item, &action_name, now) {
|
||||
house_events.push(event);
|
||||
|
||||
@@ -3,6 +3,54 @@ mod tests {
|
||||
use super::*;
|
||||
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]
|
||||
fn heat_coasting_modulates_setpoint_without_power_cycle() {
|
||||
assert!(!heat_coasting_next_state(false, "heat", 19.8, 20.8, 0.9));
|
||||
@@ -241,6 +289,9 @@ mod tests {
|
||||
#[test]
|
||||
fn flow_zone_action_noop_detects_persisted_on_intent_and_real_off_state() {
|
||||
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.mode = "heat".into();
|
||||
zone.manual_preset = Some("custom".into());
|
||||
@@ -254,6 +305,7 @@ mod tests {
|
||||
};
|
||||
assert!(flow_zone_action_already_satisfied(
|
||||
&zone,
|
||||
Some("flow-test"), Some("action-test"),
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
@@ -265,6 +317,7 @@ mod tests {
|
||||
));
|
||||
assert!(!flow_zone_action_already_satisfied(
|
||||
&zone,
|
||||
Some("flow-test"), Some("action-test"),
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
@@ -274,10 +327,19 @@ mod tests {
|
||||
Some(5.0),
|
||||
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.standby_offset_c = 5.0;
|
||||
assert!(flow_zone_action_already_satisfied(
|
||||
&zone,
|
||||
Some("flow-test"), Some("action-test"),
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
@@ -301,6 +363,7 @@ mod tests {
|
||||
};
|
||||
assert!(flow_zone_action_already_satisfied(
|
||||
&zone,
|
||||
Some("flow-test"), Some("action-test"),
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
@@ -312,6 +375,7 @@ mod tests {
|
||||
));
|
||||
assert!(!flow_zone_action_already_satisfied(
|
||||
&zone,
|
||||
Some("flow-test"), Some("action-test"),
|
||||
true,
|
||||
0,
|
||||
0,
|
||||
@@ -326,6 +390,9 @@ mod tests {
|
||||
#[test]
|
||||
fn flow_zone_action_noop_keeps_auxiliary_swing_changes_executable() {
|
||||
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.mode = "heat".into();
|
||||
zone.manual_preset = Some("custom".into());
|
||||
@@ -339,6 +406,7 @@ mod tests {
|
||||
};
|
||||
assert!(!flow_zone_action_already_satisfied(
|
||||
&zone,
|
||||
Some("flow-test"), Some("action-test"),
|
||||
true,
|
||||
0,
|
||||
0,
|
||||
@@ -350,6 +418,7 @@ mod tests {
|
||||
));
|
||||
assert!(flow_zone_action_already_satisfied(
|
||||
&zone,
|
||||
Some("flow-test"), Some("action-test"),
|
||||
true,
|
||||
1,
|
||||
0,
|
||||
@@ -717,6 +786,8 @@ mod tests {
|
||||
revision: 1,
|
||||
control_owner: "automation".into(),
|
||||
control_source: "automation".into(),
|
||||
flow_control_id: None,
|
||||
flow_control_node_id: None,
|
||||
control_since: Some(Utc::now()),
|
||||
control_resume_at: None,
|
||||
control_reason: "test".into(),
|
||||
|
||||
@@ -180,6 +180,7 @@ async fn apply_automatic_device_action(
|
||||
state: &AppState,
|
||||
device_id: &str,
|
||||
command: DeviceCommand,
|
||||
flow_source: Option<(&str, &str)>,
|
||||
) -> Result<Option<Device>, AppError> {
|
||||
// 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.
|
||||
@@ -249,6 +250,8 @@ async fn apply_automatic_device_action(
|
||||
zone.revision = zone.revision.saturating_add(1);
|
||||
zone.updated_at = Utc::now();
|
||||
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.broadcast("zone.updated", serde_json::to_value(&zone)?);
|
||||
state.wake_zone_control();
|
||||
|
||||
+105
-20
@@ -533,6 +533,102 @@ async fn handle_zone_pre_control_state(
|
||||
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<()> {
|
||||
let _cycle_guard = state.lock_zone_control_cycle().await;
|
||||
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 mut zone_snapshot = state.db.list_zones()?;
|
||||
// Only an enabled, compiled Flow action is allowed to retain simple heating
|
||||
// ownership. Disabling, deleting or recompiling a Flow immediately removes
|
||||
// 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();
|
||||
// Flow ownership is valid only while its exact compiled action is enabled.
|
||||
let active_flow_actions = active_flow_zone_action_ids(state)?;
|
||||
// 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)
|
||||
.await?;
|
||||
@@ -587,15 +674,13 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
continue;
|
||||
};
|
||||
let cycle_started_at = zone.updated_at.clone();
|
||||
if zone.simple_heat.as_ref().is_some_and(|profile| {
|
||||
!enabled_heat_actions.contains(&(profile.flow_id.clone(), profile.flow_node_id.clone(), zone.id.clone()))
|
||||
}) {
|
||||
zone.simple_heat = None;
|
||||
zone.simple_heat_runtime = SimpleHeatRuntime::default();
|
||||
if zone.control_source == "automation.flow" {
|
||||
zone.control_source = "automation".into();
|
||||
zone.control_reason = "Simple heating Flow disabled; thermostat idle".into();
|
||||
}
|
||||
if release_inactive_flow_zone_intent(&mut zone, &active_flow_actions) {
|
||||
// Persist even if this device is currently unavailable and the control
|
||||
// cycle would otherwise return early without saving the zone.
|
||||
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)?);
|
||||
}
|
||||
let outdoor_temperature = zone_outdoor_sensor_results
|
||||
.remove(&zone.id)
|
||||
|
||||
@@ -166,6 +166,12 @@ pub struct Zone {
|
||||
pub control_owner: String,
|
||||
#[serde(default)]
|
||||
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)]
|
||||
pub control_since: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -169,6 +169,12 @@ function planFlowTraceActual(rule, item, node) {
|
||||
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) {
|
||||
const trace = Array.isArray(rule.last_trigger_trace) ? rule.last_trigger_trace : [];
|
||||
if (rule.trigger_kind !== 'flow' || !trace.length) return '';
|
||||
@@ -195,7 +201,7 @@ function planReasonCard(plan) {
|
||||
}).join('');
|
||||
|
||||
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))
|
||||
.slice(0, 4)
|
||||
.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>`;
|
||||
}).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 automation = (app.automations || []).find(item => item.id === rule.id);
|
||||
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('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.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('');
|
||||
|
||||
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>`;
|
||||
}).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>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (['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 (['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 (message.event === 'flow.deleted') { app.flows = app.flows.filter(v => v.id !== data.id); 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(); renderControlPlan(); renderSimulationPage(); scheduleControlPlanLoad(); }
|
||||
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_cloud.updated') { applySettingsSection('greeCloud', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); }
|
||||
|
||||
Reference in new issue
Block a user