This commit is contained in:
Mateusz Gruszczyński
2026-09-19 21:37:46 +02:00
parent 044adef401
commit ac2dd8b62a
21 changed files with 288 additions and 97 deletions
Generated
+1 -1
View File
@@ -719,7 +719,7 @@ dependencies = [
[[package]] [[package]]
name = "gree-controller" name = "gree-controller"
version = "0.15.8" version = "0.15.9"
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.15.8" version = "0.15.9"
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"
+20 -20
View File
@@ -1,14 +1,14 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
ddda584a029e67d057e3afbe93246795e6579d7efd2f3e9ebe4cfd2bb164e907 ./Cargo.lock 0c5cd71516b15e7b51a39fd694d8532a38485d8a8d358caab8ae850274a41127 ./Cargo.lock
d09e3c8438adcfac6aa9d16de9aade898a1fa6f64f7edf17bcd70afb714db12f ./Cargo.toml e3b77008eebcb6db5b188102f788cd06f5159c1a66577bb212d96d125aa72510 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
5b5586d2e3e7b80c0ffe2d241517f8f7a8c4d7e80169a6cb3cf46221024f54e9 ./README.md d5e5fdafaf8d777e3848b5c8f3aacfd0cf4c80735fbbb6bda14e86468329de88 ./README.md
54e2716acb43f0813c6b979df802b8a0f7e3f8f03b1e42b45c746945ed21d098 ./SOURCE_MANIFEST.sha256 f4806d66acea0baaf5b1457afb708771b2765f7c08b4c4824c5ff1eace42c83d ./SOURCE_MANIFEST.sha256
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
70a8217b46a12ae47e6b461f6f619fa19facb0b422fd8d850208b55237abc752 ./docs/API.md 96144a5c5f3688854ab19dcd97794bd74da8ed26133c1468d91f6659a54faa4b ./docs/API.md
c6b1aded8ef6f4c840a2b1f2ffa3f0b22eddc080a4750adb34ee950f12f64382 ./docs/FLOW.md 0884be80021ff93826525f0b5bd5c81418f0c9c4eadec9583386ec61ae8e82ba ./docs/FLOW.md
9debc0d7d3f663c2a46ab17fd385a37aa732757e6bb7c75d6d60281ea89092b5 ./docs/openapi.json d883d4a58b19b10b5b33dacd4a58b0a4eb69dcc133a478524407027956ed9f70 ./docs/openapi.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
@@ -22,7 +22,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
00fc79f5195f43cd8d076b3d9a7a782263f0bde2a6b58b310c2d7a8b307a9f88 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json 62e7bb884b342349bb175ce874d11ccb845e8219512ff6cf4d6c9ed03cb12294 ./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
@@ -30,10 +30,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
2976822c9663e505d22297e2749192b4ca119bb4e46ccc1c29c813522c84b3c5 ./ha-addon/repository/gree-controller/CHANGELOG.md 9cb2e827567e8cb06050e24fd3811dda6f51350d8bec9b605189fa8ab85b331a ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
5f0bd0d1ea4aca47844f9e870e4ba1676ad8b90369577db75a1bb0d00228a9da ./ha-addon/repository/gree-controller/README.md 0bd253135a67d295dfca355b735b350dbf36ed396a6c8e30387d5218d4e5afcf ./ha-addon/repository/gree-controller/README.md
f43723e7e356c7cc637b36b5ade0e34cfb69c50c4cf653cca7fc013e926cd7c9 ./ha-addon/repository/gree-controller/config.yaml cb348473b59a76dfd29adc04d281128a9b8848b92107b928073aac4e047fdc4b ./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
@@ -43,8 +43,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
37d24f8de343b4796a0a8cfff3a839e55bdb779add0c8119799787d87ad8b953 ./lang/en.json e9f8da620e017a3c06363f9b3d8709d9abaee9bbac4e53e60f27db7c072bb647 ./lang/en.json
1610da0b2bbb9424006647adfb95e1318b7670aaed5a2798db1251db82635317 ./lang/pl.json efc35994018e526ed829ee26b3cb1c74df2ac3655433e3b9bd0bf12f087e3223 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -85,7 +85,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
3a3287a4ca516f91a1b1bf14257aeef7b1b3027d592200c0a6da1cc70afa9c32 ./scripts/api_dev_test.py 863f087740abee259d51dc5935c6b60dfe25b31a6a9fe64657d25a05d200a6fa ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -136,7 +136,7 @@ d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/sched
28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs 28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs 0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs
76dbf72600a48a6ba5b2273777bd8f2c8a6bd212aa236d419cc59eedcb8ab22b ./src/engine/automations.rs a0d60ad81d024488dfec586b0d9e9f447e9a3485985604415465cacba59d645f ./src/engine/automations.rs
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs 1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs 357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
ad53cd7be24deb759e03cbc6472522b69ebd36d8fac35398a9b19c899d3b17f2 ./src/engine/control_plan.rs ad53cd7be24deb759e03cbc6472522b69ebd36d8fac35398a9b19c899d3b17f2 ./src/engine/control_plan.rs
@@ -152,7 +152,7 @@ c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/r
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs 682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs 833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
af4edaf12faf294f44a8992d120a99b09edd1cbdd66981390e027d13fd6b6790 ./src/engine/tests.rs 0404e040c5d6f388682edb3d103a658e0b963a402e4e847fc92fd0aea1c0e773 ./src/engine/tests.rs
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
fb169bcc20f95abe9892367df6dd3a683584bdbc721792d6ffb9311b81ca30d9 ./src/engine/zone_control.rs fb169bcc20f95abe9892367df6dd3a683584bdbc721792d6ffb9311b81ca30d9 ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
@@ -163,7 +163,7 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb
67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs 67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs
62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs 62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs
23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs 23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs
920e028d96a14cca9f74a964bb296e68a109cfead314d3164c180201e606d151 ./src/models/automation.rs e7b6e49d369aaf665a8aea2464950fd795957de0ad7528a002e11c55c7983f35 ./src/models/automation.rs
fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/control_plan.rs fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/control_plan.rs
06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs 06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs
698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs 698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs
@@ -199,10 +199,10 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs 8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
1043dbcfb24a474ce088acb6aa86479e84db74688fb4802febeb3f3f564ca65f ./web/css/styles.css c60667f270dc66ee463f425ca5eda676363fcd2390e5d652220f6b4af9ded6c3 ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
cc1509fd76f0b0bd4c069db63e1e104210c2bed05f32a3eebac7642f94ef8a99 ./web/index.html ca887343ec089cbbb83c4ce5685edbade1266c89a29b5570c02f75c9c50b1c10 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
@@ -210,7 +210,7 @@ e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynam
752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js 752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js
6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js 6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js
b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynamic/events.js b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynamic/events.js
879a92e95575ace4775b2f9a7f189e78ad0082ca5c12d012ef456e6477010886 ./web/js-dynamic/flows.js e0f604f54eafb9a6d6ac1c6c24eb27f3e3c1e31ed80ead57ac0340c7228318b8 ./web/js-dynamic/flows.js
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js 3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.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.15.8** **Version: 0.15.9**
Version 0.15.8 improves Visual Flow portability and editor ergonomics: exported Flows carry the Shared Input definitions they use, imports can create or safely remap missing Shared Inputs, and the Interpretation / Flow cycle panel is collapsed by default on every viewport. Version 0.15.9 strengthens Visual Flow execution and editing: change events remain pending until their action succeeds, rolling mean/median blocks wait for a complete configured window, thermostat action summaries show power/mode explicitly, and the canvas adds box selection plus copy/paste/duplicate for groups of blocks.
> [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`.
+19 -19
View File
@@ -1,13 +1,13 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
ddda584a029e67d057e3afbe93246795e6579d7efd2f3e9ebe4cfd2bb164e907 ./Cargo.lock 0c5cd71516b15e7b51a39fd694d8532a38485d8a8d358caab8ae850274a41127 ./Cargo.lock
d09e3c8438adcfac6aa9d16de9aade898a1fa6f64f7edf17bcd70afb714db12f ./Cargo.toml e3b77008eebcb6db5b188102f788cd06f5159c1a66577bb212d96d125aa72510 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
5b5586d2e3e7b80c0ffe2d241517f8f7a8c4d7e80169a6cb3cf46221024f54e9 ./README.md d5e5fdafaf8d777e3848b5c8f3aacfd0cf4c80735fbbb6bda14e86468329de88 ./README.md
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
70a8217b46a12ae47e6b461f6f619fa19facb0b422fd8d850208b55237abc752 ./docs/API.md 96144a5c5f3688854ab19dcd97794bd74da8ed26133c1468d91f6659a54faa4b ./docs/API.md
c6b1aded8ef6f4c840a2b1f2ffa3f0b22eddc080a4750adb34ee950f12f64382 ./docs/FLOW.md 0884be80021ff93826525f0b5bd5c81418f0c9c4eadec9583386ec61ae8e82ba ./docs/FLOW.md
9debc0d7d3f663c2a46ab17fd385a37aa732757e6bb7c75d6d60281ea89092b5 ./docs/openapi.json d883d4a58b19b10b5b33dacd4a58b0a4eb69dcc133a478524407027956ed9f70 ./docs/openapi.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
@@ -21,7 +21,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
00fc79f5195f43cd8d076b3d9a7a782263f0bde2a6b58b310c2d7a8b307a9f88 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json 62e7bb884b342349bb175ce874d11ccb845e8219512ff6cf4d6c9ed03cb12294 ./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
@@ -29,10 +29,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
2976822c9663e505d22297e2749192b4ca119bb4e46ccc1c29c813522c84b3c5 ./ha-addon/repository/gree-controller/CHANGELOG.md 9cb2e827567e8cb06050e24fd3811dda6f51350d8bec9b605189fa8ab85b331a ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
5f0bd0d1ea4aca47844f9e870e4ba1676ad8b90369577db75a1bb0d00228a9da ./ha-addon/repository/gree-controller/README.md 0bd253135a67d295dfca355b735b350dbf36ed396a6c8e30387d5218d4e5afcf ./ha-addon/repository/gree-controller/README.md
f43723e7e356c7cc637b36b5ade0e34cfb69c50c4cf653cca7fc013e926cd7c9 ./ha-addon/repository/gree-controller/config.yaml cb348473b59a76dfd29adc04d281128a9b8848b92107b928073aac4e047fdc4b ./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
@@ -42,8 +42,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
37d24f8de343b4796a0a8cfff3a839e55bdb779add0c8119799787d87ad8b953 ./lang/en.json e9f8da620e017a3c06363f9b3d8709d9abaee9bbac4e53e60f27db7c072bb647 ./lang/en.json
1610da0b2bbb9424006647adfb95e1318b7670aaed5a2798db1251db82635317 ./lang/pl.json efc35994018e526ed829ee26b3cb1c74df2ac3655433e3b9bd0bf12f087e3223 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -84,7 +84,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
3a3287a4ca516f91a1b1bf14257aeef7b1b3027d592200c0a6da1cc70afa9c32 ./scripts/api_dev_test.py 863f087740abee259d51dc5935c6b60dfe25b31a6a9fe64657d25a05d200a6fa ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -135,7 +135,7 @@ d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/sched
28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs 28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs 0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs
76dbf72600a48a6ba5b2273777bd8f2c8a6bd212aa236d419cc59eedcb8ab22b ./src/engine/automations.rs a0d60ad81d024488dfec586b0d9e9f447e9a3485985604415465cacba59d645f ./src/engine/automations.rs
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs 1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs 357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
ad53cd7be24deb759e03cbc6472522b69ebd36d8fac35398a9b19c899d3b17f2 ./src/engine/control_plan.rs ad53cd7be24deb759e03cbc6472522b69ebd36d8fac35398a9b19c899d3b17f2 ./src/engine/control_plan.rs
@@ -151,7 +151,7 @@ c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/r
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs 682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs 833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
af4edaf12faf294f44a8992d120a99b09edd1cbdd66981390e027d13fd6b6790 ./src/engine/tests.rs 0404e040c5d6f388682edb3d103a658e0b963a402e4e847fc92fd0aea1c0e773 ./src/engine/tests.rs
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
fb169bcc20f95abe9892367df6dd3a683584bdbc721792d6ffb9311b81ca30d9 ./src/engine/zone_control.rs fb169bcc20f95abe9892367df6dd3a683584bdbc721792d6ffb9311b81ca30d9 ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
@@ -162,7 +162,7 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb
67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs 67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs
62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs 62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs
23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs 23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs
920e028d96a14cca9f74a964bb296e68a109cfead314d3164c180201e606d151 ./src/models/automation.rs e7b6e49d369aaf665a8aea2464950fd795957de0ad7528a002e11c55c7983f35 ./src/models/automation.rs
fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/control_plan.rs fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/control_plan.rs
06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs 06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs
698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs 698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs
@@ -198,10 +198,10 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs 8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
1043dbcfb24a474ce088acb6aa86479e84db74688fb4802febeb3f3f564ca65f ./web/css/styles.css c60667f270dc66ee463f425ca5eda676363fcd2390e5d652220f6b4af9ded6c3 ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
cc1509fd76f0b0bd4c069db63e1e104210c2bed05f32a3eebac7642f94ef8a99 ./web/index.html ca887343ec089cbbb83c4ce5685edbade1266c89a29b5570c02f75c9c50b1c10 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
@@ -209,7 +209,7 @@ e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynam
752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js 752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js
6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js 6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js
b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynamic/events.js b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynamic/events.js
879a92e95575ace4775b2f9a7f189e78ad0082ca5c12d012ef456e6477010886 ./web/js-dynamic/flows.js e0f604f54eafb9a6d6ac1c6c24eb27f3e3c1e31ed80ead57ac0340c7228318b8 ./web/js-dynamic/flows.js
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js 3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
+3 -3
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference # GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.15.8**. HTTP and WebSocket API for GREE Controller **0.15.9**.
[← Main documentation](../README.md) [← Main documentation](../README.md)
@@ -241,7 +241,7 @@ Response:
{ {
"status": "ok", "status": "ok",
"name": "gree-controller", "name": "gree-controller",
"version": "0.15.8", "version": "0.15.9",
"uptime_seconds": 1234, "uptime_seconds": 1234,
"control_ready": true, "control_ready": true,
"time": "2026-08-30T06:54:00Z" "time": "2026-08-30T06:54:00Z"
@@ -304,7 +304,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.15.8", "version": "0.15.9",
"uptime_seconds": 1234, "uptime_seconds": 1234,
"auth_required": false, "auth_required": false,
"control_ready": true, "control_ready": true,
+2 -2
View File
@@ -60,7 +60,7 @@ Dry-run never mutates thermostats, devices, groups, schedules or automations. Fo
## Blocks ## Blocks
Condition/source blocks include weekday, time range, date range, optional 5-field CRON triggers, application Night mode, outdoor/device/zone temperature, house mode, device state, thermostat/zone state, group state, Home Assistant state/numeric/attribute/availability, rolling mean/median windows, numeric oscillation detection, a diagnostic constant, and `shared_input`. Stateful gates include `stable_for`, bounded `state_duration`, `on_change` (condition-result or raw-value edge), `rate_limit` (maximum successful executions in a rolling period), and `delay`. Their runtime state, together with rolling statistics and oscillation samples, is persisted in the generated automation payload so restarts do not silently reset timing/history. Editing a stateful block resets only that block's incompatible runtime state. Flow state continues to be observed while the action itself is in cooldown, preventing stale continuity/change state. A `rate_limit` block must be placed directly before an action and consumes quota only after a successful execution; suppressed or failed actions do not consume it. Shared inputs are configured centrally in Home Assistant / Sensors and referenced by ID. Shared inputs store reusable value sources only; operators and comparison values are configured exclusively in each Flow reference. Logic blocks are AND, OR and NOT. Condition/source blocks include weekday, time range, date range, optional 5-field CRON triggers, application Night mode, outdoor/device/zone temperature, house mode, device state, thermostat/zone state, group state, Home Assistant state/numeric/attribute/availability, rolling mean/median windows, numeric oscillation detection, a diagnostic constant, and `shared_input`. Stateful gates include `stable_for`, bounded `state_duration`, `on_change` (condition-result or raw-value edge), `rate_limit` (maximum successful executions in a rolling period), and `delay`. Their runtime state, together with rolling statistics and oscillation samples, is persisted in the generated automation payload so restarts do not silently reset timing/history. Editing a stateful block resets only that block's incompatible runtime state. Flow state continues to be observed while the action itself is in cooldown. `on_change` latches a detected event while its upstream condition remains true and clears it only after a successful downstream action, so cooldown, ownership suppression or same-cycle target arbitration cannot silently consume the edge. Rolling mean/median blocks wait for one complete configured time window before matching and restart that warm-up after their source becomes unavailable. A `rate_limit` block must be placed directly before an action and consumes quota only after a successful execution; suppressed or failed actions do not consume it. Shared inputs are configured centrally in Home Assistant / Sensors and referenced by ID. Shared inputs store reusable value sources only; operators and comparison values are configured exclusively in each Flow reference. Logic blocks are AND, OR and NOT.
Actions include thermostat zone, full GREE device command, single GREE function, climate group, and generic Home Assistant service calls (`domain`, `service`, optional `entity_id`, JSON service data). Direct GREE actions expose power, HVAC mode, target temperature, fan speed, vertical/horizontal louver position, quiet, turbo, light, air, xfan, health and sleep. The single-function block writes exactly one of those fields. Actions include thermostat zone, full GREE device command, single GREE function, climate group, and generic Home Assistant service calls (`domain`, `service`, optional `entity_id`, JSON service data). Direct GREE actions expose power, HVAC mode, target temperature, fan speed, vertical/horizontal louver position, quiet, turbo, light, air, xfan, health and sleep. The single-function block writes exactly one of those fields.
@@ -72,7 +72,7 @@ Shared Flow inputs show the Flows that reference them and link directly to those
Presets are data files, not JavaScript definitions. Every preset lives in `presets/<id>.json` and contains `id`, `category`, localized `name`, localized `description`, and the complete `flow` graph (`nodes` + `edges`). `build.rs` validates and embeds the directory, exposes `/presets/index.json` plus `/presets/<file>.json`, and the Flow editor loads that library dynamically. Device/zone/group placeholders such as `$zone1`, `$zone2`, `$device1` and `$group1` are resolved when a preset is applied. Adding a preset therefore does not require adding graph-building code to `web/js-dynamic/flows.js`. Presets are data files, not JavaScript definitions. Every preset lives in `presets/<id>.json` and contains `id`, `category`, localized `name`, localized `description`, and the complete `flow` graph (`nodes` + `edges`). `build.rs` validates and embeds the directory, exposes `/presets/index.json` plus `/presets/<file>.json`, and the Flow editor loads that library dynamically. Device/zone/group placeholders such as `$zone1`, `$zone2`, `$device1` and `$group1` are resolved when a preset is applied. Adding a preset therefore does not require adding graph-building code to `web/js-dynamic/flows.js`.
The canvas supports multi-selection (Shift/Ctrl/Cmd), `Ctrl/Cmd+A`, Select all / Clear selection toolbar actions, and dragging the whole selected group while preserving relative positions. The canvas supports drag-box multi-selection, additive Shift/Ctrl/Cmd selection, `Ctrl/Cmd+A`, Select all / Clear selection toolbar actions, and dragging the whole selected group while preserving relative positions. Selected blocks can be duplicated from the toolbar or with `Ctrl/Cmd+D`; `Ctrl/Cmd+C` and `Ctrl/Cmd+V` copy/paste the selected subgraph and preserve connections whose endpoints are both selected.
## Good next stateful blocks ## Good next stateful blocks
+2 -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.15.8", "version": "0.15.9",
"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": {
@@ -5967,7 +5967,7 @@
}, },
"version": { "version": {
"type": "string", "type": "string",
"example": "0.15.8" "example": "0.15.9"
}, },
"uptime_seconds": { "uptime_seconds": {
"type": "integer", "type": "integer",
@@ -1,7 +1,7 @@
{ {
"domain": "gree_controller", "domain": "gree_controller",
"name": "GREE Controller", "name": "GREE Controller",
"version": "0.15.8", "version": "0.15.9",
"config_flow": true, "config_flow": true,
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
@@ -1,5 +1,13 @@
# Changelog # Changelog
## 0.15.9
- Visual Flow `on_change` events are now latched while their upstream condition remains true and are acknowledged only after the downstream action executes successfully, so cooldowns, ownership suppression and same-cycle target conflicts no longer silently consume a change event.
- Rolling mean/median blocks now require one complete configured time window after their runtime window starts or restarts before they can match; unavailable sources reset the rolling window instead of reusing partial history.
- Thermostat action summaries now show explicit power and HVAC mode alongside the preset/target, so `power=false` is displayed as OFF instead of looking like a preset-only Auto action.
- The Flow canvas adds drag-box multi-selection, keeps Shift/Ctrl/Cmd additive selection and moves the whole selection together.
- Selected Flow blocks can be copied/pasted or duplicated with internal connections preserved; Ctrl/Cmd+C, Ctrl/Cmd+V and Ctrl/Cmd+D are supported.
## 0.15.8 ## 0.15.8
- Visual Flow exports now include the Shared Input definitions referenced by the graph. - Visual Flow exports now include the Shared Input definitions referenced by the graph.
@@ -2,7 +2,7 @@
Local GREE HVAC controller packaged as a Home Assistant add-on, with Web UI, HTTP/WebSocket API, built-in Swagger API documentation, UDP discovery/control, `amd64` and `aarch64` support, ingress, and database backup from `/data/gree-controller.db`. Local GREE HVAC controller packaged as a Home Assistant add-on, with Web UI, HTTP/WebSocket API, built-in Swagger API documentation, UDP discovery/control, `amd64` and `aarch64` support, ingress, and database backup from `/data/gree-controller.db`.
Version 0.15.8 makes Visual Flow exports portable with referenced Shared Inputs and keeps the Interpretation / Flow cycle details collapsed until opened. Version 0.15.9 strengthens Visual Flow execution and editing with latched change events, full rolling-stat windows, clearer thermostat action summaries, box selection and multi-block copy/paste/duplicate.
![Home Assistant OS + GREE + VLAN topology](https://git.linuxiarz.pl/gru/gree-controller-ha-addon/raw/branch/master/gree-controller/topology-ha-vlan-gree-en.png) ![Home Assistant OS + GREE + VLAN topology](https://git.linuxiarz.pl/gru/gree-controller-ha-addon/raw/branch/master/gree-controller/topology-ha-vlan-gree-en.png)
@@ -1,5 +1,5 @@
name: "GREE Controller" name: "GREE Controller"
version: "0.15.8" version: "0.15.9"
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/"
+4
View File
@@ -1019,6 +1019,10 @@
"flow.selectAll": "Select all", "flow.selectAll": "Select all",
"flow.clearSelection": "Clear selection", "flow.clearSelection": "Clear selection",
"flow.selectedCount": "Selected: {count}", "flow.selectedCount": "Selected: {count}",
"flow.duplicateSelection": "Duplicate",
"flow.blocksCopied": "Copied blocks: {count}",
"flow.blocksPasted": "Pasted blocks: {count}",
"flow.blocksDuplicated": "Duplicated blocks: {count}",
"flow.node.sharedInput": "Shared input", "flow.node.sharedInput": "Shared input",
"flow.sharedInputMissing": "Shared input missing", "flow.sharedInputMissing": "Shared input missing",
"flow.sharedInputTitle": "Shared Flow input", "flow.sharedInputTitle": "Shared Flow input",
+4
View File
@@ -1019,6 +1019,10 @@
"flow.selectAll": "Zaznacz wszystko", "flow.selectAll": "Zaznacz wszystko",
"flow.clearSelection": "Wyczyść zaznaczenie", "flow.clearSelection": "Wyczyść zaznaczenie",
"flow.selectedCount": "Zaznaczono: {count}", "flow.selectedCount": "Zaznaczono: {count}",
"flow.duplicateSelection": "Duplikuj",
"flow.blocksCopied": "Skopiowano bloki: {count}",
"flow.blocksPasted": "Wklejono bloki: {count}",
"flow.blocksDuplicated": "Zduplikowano bloki: {count}",
"flow.node.sharedInput": "Wspólne wejście", "flow.node.sharedInput": "Wspólne wejście",
"flow.sharedInputMissing": "Brak wspólnego wejścia", "flow.sharedInputMissing": "Brak wspólnego wejścia",
"flow.sharedInputTitle": "Wspólne wejście Flow", "flow.sharedInputTitle": "Wspólne wejście Flow",
+3 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Developer smoke/integration tests for GREE Controller API 0.15.8. """Developer smoke/integration tests for GREE Controller API 0.15.9.
Default mode is read-only and safe to run against a real controller. Default mode is read-only and safe to run against a real controller.
Use --settings-write to additionally round-trip all split settings resources and Use --settings-write to additionally round-trip all split settings resources and
@@ -26,7 +26,7 @@ from typing import Any, Callable, Iterable, Optional
DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787") DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787")
DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "") DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "")
DEFAULT_EXPECTED_VERSION = "0.15.8" DEFAULT_EXPECTED_VERSION = "0.15.9"
SETTINGS_PATHS: dict[str, set[str]] = { SETTINGS_PATHS: dict[str, set[str]] = {
"/api/settings/application": {"simulator_enabled"}, "/api/settings/application": {"simulator_enabled"},
@@ -599,7 +599,7 @@ def run_suite(args: argparse.Namespace) -> int:
print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n") print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n")
runner.run("public health", lambda: test_health(client, args.expected_version)) runner.run("public health", lambda: test_health(client, args.expected_version))
runner.run("OpenAPI 0.15.8 contract", lambda: test_openapi(client, args.expected_version)) runner.run("OpenAPI 0.15.9 contract", lambda: test_openapi(client, args.expected_version))
runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client)) runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client))
runner.run("protected API requires auth", lambda: test_protected_auth(client)) runner.run("protected API requires auth", lambda: test_protected_auth(client))
+34 -6
View File
@@ -157,6 +157,7 @@ async fn run_automations(state: &AppState) -> Result<()> {
Ok(true) => { Ok(true) => {
for device_id in target_devices { claimed_devices.insert(device_id); } for device_id in target_devices { claimed_devices.insert(device_id); }
let fired_at = Utc::now(); let fired_at = Utc::now();
flow_acknowledge_change_gates(&mut item);
flow_record_rate_limited_execution(&mut item, fired_at.clone()); flow_record_rate_limited_execution(&mut item, fired_at.clone());
item.last_fired_at = Some(fired_at); item.last_fired_at = Some(fired_at);
item.updated_at = Utc::now(); item.updated_at = Utc::now();
@@ -405,10 +406,27 @@ fn flow_state_duration_update(
(within_min && within_max, elapsed) (within_min && within_max, elapsed)
} }
fn flow_change_gate_update(state: &mut crate::models::FlowRuntimeNodeState, current: Value) -> bool { fn flow_change_gate_update(
state: &mut crate::models::FlowRuntimeNodeState,
current: Value,
input: bool,
) -> (bool, bool) {
let changed = state.last_value.as_ref().is_some_and(|previous| previous != &current); let changed = state.last_value.as_ref().is_some_and(|previous| previous != &current);
state.last_value = Some(current); state.last_value = Some(current);
changed if !input {
state.pending = false;
} else if changed {
state.pending = true;
}
(changed, state.pending)
}
fn flow_acknowledge_change_gates(item: &mut Automation) {
for condition in item.flow_conditions.iter().filter(|condition| condition.kind == "on_change") {
if let Some(state) = item.flow_runtime.get_mut(&condition.id) {
state.pending = false;
}
}
} }
fn flow_rate_limit_status( fn flow_rate_limit_status(
@@ -444,9 +462,13 @@ fn flow_rolling_stat_update(
statistic: &str, statistic: &str,
now: DateTime<Utc>, now: DateTime<Utc>,
) -> Option<f64> { ) -> Option<f64> {
let started_at = state.since.get_or_insert_with(|| now.clone()).clone();
let cutoff = now.clone() - chrono::Duration::seconds(window_seconds as i64); let cutoff = now.clone() - chrono::Duration::seconds(window_seconds as i64);
state.samples.retain(|item| item.at >= cutoff); state.samples.retain(|item| item.at >= cutoff);
state.samples.push(crate::models::FlowRuntimeSample { at: now, value: sample }); state.samples.push(crate::models::FlowRuntimeSample { at: now.clone(), value: sample });
if now.signed_duration_since(started_at).num_seconds().max(0) < window_seconds as i64 {
return None;
}
let mut values = state.samples.iter().map(|item| item.value).collect::<Vec<_>>(); let mut values = state.samples.iter().map(|item| item.value).collect::<Vec<_>>();
if values.is_empty() { return None; } if values.is_empty() { return None; }
if statistic == "median" { if statistic == "median" {
@@ -730,13 +752,14 @@ pub async fn evaluate_flow_conditions_trace(
let mode = condition.config.get("mode").and_then(Value::as_str).unwrap_or("result"); let mode = condition.config.get("mode").and_then(Value::as_str).unwrap_or("result");
let observed = if mode == "value" { actual_values.get(&input_id).cloned() } else { Some(json!(input)) }; let observed = if mode == "value" { actual_values.get(&input_id).cloned() } else { Some(json!(input)) };
let mut changed = false; let mut changed = false;
let mut pending = false;
let mut previous = None; let mut previous = None;
if let (Some(current), Some(map)) = (observed.clone(), runtime.as_deref_mut()) { if let (Some(current), Some(map)) = (observed.clone(), runtime.as_deref_mut()) {
let state = map.entry(condition.id.clone()).or_default(); let state = map.entry(condition.id.clone()).or_default();
previous = state.last_value.clone(); previous = state.last_value.clone();
changed = flow_change_gate_update(state, current); (changed, pending) = flow_change_gate_update(state, current, input);
} }
(input && changed, json!({"input": input, "mode": mode, "previous": previous, "current": observed, "changed": changed})) (input && pending, json!({"input": input, "mode": mode, "previous": previous, "current": observed, "changed": changed, "pending": pending}))
} }
"rate_limit" => { "rate_limit" => {
let input = condition.inputs.len() == 1 && values.get(&condition.inputs[0]).copied().unwrap_or(false); let input = condition.inputs.len() == 1 && values.get(&condition.inputs[0]).copied().unwrap_or(false);
@@ -772,12 +795,17 @@ pub async fn evaluate_flow_conditions_trace(
let window = condition.config.get("window_seconds").and_then(Value::as_u64).unwrap_or(60); let window = condition.config.get("window_seconds").and_then(Value::as_u64).unwrap_or(60);
let mut aggregate = None; let mut aggregate = None;
let mut count = 0usize; let mut count = 0usize;
if let (Some(sample), Some(map)) = (sample, runtime.as_deref_mut()) { if let Some(map) = runtime.as_deref_mut() {
let node_state = map.entry(condition.id.clone()).or_default(); let node_state = map.entry(condition.id.clone()).or_default();
if let Some(sample) = sample {
aggregate = flow_rolling_stat_update( aggregate = flow_rolling_stat_update(
node_state, sample, window, condition.config.get("statistic").and_then(Value::as_str).unwrap_or("mean"), now.with_timezone(&Utc), node_state, sample, window, condition.config.get("statistic").and_then(Value::as_str).unwrap_or("mean"), now.with_timezone(&Utc),
); );
count = node_state.samples.len(); count = node_state.samples.len();
} else {
node_state.since = None;
node_state.samples.clear();
}
} }
let expected = condition.config.get("value").and_then(Value::as_f64); let expected = condition.config.get("value").and_then(Value::as_f64);
let matched = predecessors_match && aggregate.zip(expected).map(|(a,e)| flow_compare(a, condition.config.get("operator").and_then(Value::as_str).unwrap_or("lt"), e)).unwrap_or(false); let matched = predecessors_match && aggregate.zip(expected).map(|(a,e)| flow_compare(a, condition.config.get("operator").and_then(Value::as_str).unwrap_or("lt"), e)).unwrap_or(false);
+17 -27
View File
@@ -306,12 +306,15 @@ mod tests {
} }
#[test] #[test]
fn flow_change_gate_ignores_first_observation_and_detects_later_change() { fn flow_change_gate_latches_until_action_ack_or_input_reset() {
let mut state = crate::models::FlowRuntimeNodeState::default(); let mut state = crate::models::FlowRuntimeNodeState::default();
assert!(!flow_change_gate_update(&mut state, json!("off"))); assert_eq!(flow_change_gate_update(&mut state, json!(false), false), (false, false));
assert!(!flow_change_gate_update(&mut state, json!("off"))); assert_eq!(flow_change_gate_update(&mut state, json!(true), true), (true, true));
assert!(flow_change_gate_update(&mut state, json!("on"))); assert_eq!(flow_change_gate_update(&mut state, json!(true), true), (false, true));
assert!(!flow_change_gate_update(&mut state, json!("on"))); state.pending = false;
assert_eq!(flow_change_gate_update(&mut state, json!(true), true), (false, false));
state.pending = true;
assert_eq!(flow_change_gate_update(&mut state, json!(false), false), (true, false));
} }
#[test] #[test]
@@ -337,30 +340,27 @@ mod tests {
} }
#[test] #[test]
fn flow_rolling_statistics_compute_mean_median_and_prune_window() { fn flow_rolling_statistics_require_a_full_window_and_prune_samples() {
let mut state = crate::models::FlowRuntimeNodeState::default(); let mut state = crate::models::FlowRuntimeNodeState::default();
let start = Utc.with_ymd_and_hms(2026, 9, 2, 12, 0, 0).unwrap(); let start = Utc.with_ymd_and_hms(2026, 9, 2, 12, 0, 0).unwrap();
assert_eq!( assert_eq!(flow_rolling_stat_update(&mut state, 10.0, 60, "mean", start), None);
flow_rolling_stat_update(&mut state, 10.0, 60, "mean", start),
Some(10.0)
);
assert_eq!( assert_eq!(
flow_rolling_stat_update( flow_rolling_stat_update(
&mut state, &mut state,
20.0, 20.0,
60, 60,
"mean", "mean",
start + chrono::Duration::seconds(10) start + chrono::Duration::seconds(30)
), ),
Some(15.0) None
); );
assert_eq!( assert_eq!(
flow_rolling_stat_update( flow_rolling_stat_update(
&mut state, &mut state,
30.0, 30.0,
60, 60,
"median", "mean",
start + chrono::Duration::seconds(20) start + chrono::Duration::seconds(60)
), ),
Some(20.0) Some(20.0)
); );
@@ -370,21 +370,11 @@ mod tests {
40.0, 40.0,
60, 60,
"median", "median",
start + chrono::Duration::seconds(30) start + chrono::Duration::seconds(90)
), ),
Some(25.0) Some(30.0)
); );
assert_eq!( assert_eq!(state.samples.len(), 3);
flow_rolling_stat_update(
&mut state,
100.0,
60,
"mean",
start + chrono::Duration::seconds(100)
),
Some(100.0)
);
assert_eq!(state.samples.len(), 1);
} }
#[test] #[test]
+3
View File
@@ -34,6 +34,9 @@ pub struct FlowRuntimeNodeState {
/// Last observed raw/result value for edge/change detection blocks. /// Last observed raw/result value for edge/change detection blocks.
#[serde(default)] #[serde(default)]
pub last_value: Option<Value>, pub last_value: Option<Value>,
/// Latched change event waiting for its downstream action to execute successfully.
#[serde(default)]
pub pending: bool,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+9
View File
@@ -6270,6 +6270,15 @@ textarea[aria-invalid="true"] {
box-shadow: 0 0 0 2px var(--accent-soft), 0 12px 30px rgba(0, 0, 0, .22); box-shadow: 0 0 0 2px var(--accent-soft), 0 12px 30px rgba(0, 0, 0, .22);
} }
.flow-selection-marquee {
position: absolute;
z-index: 4;
pointer-events: none;
border: 1px solid var(--accent);
border-radius: 4px;
background: var(--accent-soft);
}
.flow-editor .flow-node-time { .flow-editor .flow-node-time {
border-top: 3px solid var(--blue); border-top: 3px solid var(--blue);
} }
+3
View File
@@ -998,6 +998,8 @@
data-i18n="flow.addBlock">Add data-i18n="flow.addBlock">Add
block</span></button><button type="button" class="secondary flow-selection-action" block</span></button><button type="button" class="secondary flow-selection-action"
data-action="flow-select-all" data-i18n="flow.selectAll">Zaznacz wszystko</button><button type="button" data-action="flow-select-all" data-i18n="flow.selectAll">Zaznacz wszystko</button><button type="button"
class="secondary flow-selection-action" data-action="flow-duplicate-selection"
data-i18n="flow.duplicateSelection" disabled>Duplikuj</button><button type="button"
class="secondary flow-selection-action" data-action="flow-clear-selection" class="secondary flow-selection-action" data-action="flow-clear-selection"
data-i18n="flow.clearSelection">Wyczyść zaznaczenie</button><span id="flowSelectionCount" data-i18n="flow.clearSelection">Wyczyść zaznaczenie</button><span id="flowSelectionCount"
class="badge"></span></div> class="badge"></span></div>
@@ -1015,6 +1017,7 @@
<div id="flowWorkspace" class="flow-workspace" tabindex="0"> <div id="flowWorkspace" class="flow-workspace" tabindex="0">
<div id="flowCanvas" class="flow-canvas"><svg id="flowEdges" class="flow-edges" aria-hidden="true"></svg> <div id="flowCanvas" class="flow-canvas"><svg id="flowEdges" class="flow-edges" aria-hidden="true"></svg>
<div id="flowNodes" class="flow-nodes"></div> <div id="flowNodes" class="flow-nodes"></div>
<div id="flowSelectionMarquee" class="flow-selection-marquee" hidden aria-hidden="true"></div>
</div> </div>
<div id="flowEmptyHint" class="flow-empty-hint"><strong data-i18n="flow.emptyCanvas">Start by adding <div id="flowEmptyHint" class="flow-empty-hint"><strong data-i18n="flow.emptyCanvas">Start by adding
blocks</strong><span data-i18n="flow.emptyCanvasHint">Conditions go on the left, actions on the blocks</strong><span data-i18n="flow.emptyCanvasHint">Conditions go on the left, actions on the
+146 -4
View File
@@ -357,7 +357,13 @@ function flowNodeSummary(node) {
if (node.kind === 'logic_and') return tr('flow.allConditions'); if (node.kind === 'logic_and') return tr('flow.allConditions');
if (node.kind === 'logic_or') return tr('flow.anyCondition'); if (node.kind === 'logic_or') return tr('flow.anyCondition');
if (node.kind === 'logic_not') return tr('flow.invertCondition'); if (node.kind === 'logic_not') return tr('flow.invertCondition');
if (node.kind === 'zone_thermostat') { const zone = app.zones.find(z => z.id === c.zone_id); return `${zone?.name || tr('common.noZone')} · ${c.preset === 'custom' ? fmtTemp(c.setpoint) : zonePresetLabel(c.preset || 'comfort')}`; } if (node.kind === 'zone_thermostat') {
const zone = app.zones.find(z => z.id === c.zone_id);
const power = c.power == null ? null : (c.power ? tr('common.on') : tr('common.off'));
const mode = c.mode ? (c.mode === 'auto' ? 'Auto' : modeLabel(c.mode)) : null;
const target = c.preset === 'custom' ? fmtTemp(c.setpoint) : zonePresetLabel(c.preset || 'comfort');
return [zone?.name || tr('common.noZone'), power, mode, target].filter(Boolean).join(' · ');
}
if (node.kind === 'device_action') { const d = app.devices.find(v => v.id === c.device_id); return `${d?.name || tr('common.noDevice')} · ${c.power == null ? tr('actions.noChange') : c.power ? tr('common.on') : tr('common.off')}`; } if (node.kind === 'device_action') { const d = app.devices.find(v => v.id === c.device_id); return `${d?.name || tr('common.noDevice')} · ${c.power == null ? tr('actions.noChange') : c.power ? tr('common.on') : tr('common.off')}`; }
if (node.kind === 'device_feature_action') { const d = app.devices.find(v => v.id === c.device_id); return `${d?.name || tr('common.noDevice')} · ${deviceCommandFieldLabel(c.feature || 'light')}${flowDeviceFeatureValueLabel(c.feature, c.value)}`; } if (node.kind === 'device_feature_action') { const d = app.devices.find(v => v.id === c.device_id); return `${d?.name || tr('common.noDevice')} · ${deviceCommandFieldLabel(c.feature || 'light')}${flowDeviceFeatureValueLabel(c.feature, c.value)}`; }
if (node.kind === 'group_action') { const g = app.groups.find(v => v.id === c.group_id); return `${g?.name || tr('groups.group')} · ${c.power == null ? tr('actions.noChange') : c.power ? tr('common.on') : tr('common.off')}`; } if (node.kind === 'group_action') { const g = app.groups.find(v => v.id === c.group_id); return `${g?.name || tr('groups.group')} · ${c.power == null ? tr('actions.noChange') : c.power ? tr('common.on') : tr('common.off')}`; }
@@ -470,7 +476,9 @@ function renderFlowEditor() {
</article>`; </article>`;
}).join(''); }).join('');
$('#flowEmptyHint').hidden = draft.nodes.length > 0; $('#flowEmptyHint').hidden = draft.nodes.length > 0;
const selectionCount = $('#flowSelectionCount'); if (selectionCount) selectionCount.textContent = (app.flowSelectedNodeIds || []).length ? tr('flow.selectedCount', { count: (app.flowSelectedNodeIds || []).length }) : ''; const selectedCount = (app.flowSelectedNodeIds || []).length;
const selectionCount = $('#flowSelectionCount'); if (selectionCount) selectionCount.textContent = selectedCount ? tr('flow.selectedCount', { count: selectedCount }) : '';
const duplicateButton = $('[data-action="flow-duplicate-selection"]'); if (duplicateButton) duplicateButton.disabled = !selectedCount;
renderFlowNameMode(); renderFlowEnabledLabel(); setFlowZoom(app.flowZoom || 1, { render: false }); renderFlowEdges(); renderFlowInspector(); renderFlowInterpretation(); renderFlowRuntimeInfo(); renderFlowSaveStatus(); refreshFlowSharedInputCurrentValues(); renderFlowNameMode(); renderFlowEnabledLabel(); setFlowZoom(app.flowZoom || 1, { render: false }); renderFlowEdges(); renderFlowInspector(); renderFlowInterpretation(); renderFlowRuntimeInfo(); renderFlowSaveStatus(); refreshFlowSharedInputCurrentValues();
const status = $('#flowCompileStatus'); const status = $('#flowCompileStatus');
status.textContent = draft.draft ? tr('flow.draftStatus') : tr('flow.compileCount', { schedules: draft.compiled_schedule_ids?.length || 0, automations: draft.compiled_automation_ids?.length || 0 }); status.textContent = draft.draft ? tr('flow.draftStatus') : tr('flow.compileCount', { schedules: draft.compiled_schedule_ids?.length || 0, automations: draft.compiled_automation_ids?.length || 0 });
@@ -1189,6 +1197,59 @@ function selectAllFlowNodes() {
function clearFlowSelection() { setFlowSelection([], null); } function clearFlowSelection() { setFlowSelection([], null); }
let flowClipboard = null;
let flowClipboardPasteCount = 0;
function copySelectedFlowNodes({ notify = true } = {}) {
if (!app.flowDraft || !(app.flowSelectedNodeIds || []).length) return false;
const selected = new Set(app.flowSelectedNodeIds);
const nodes = app.flowDraft.nodes
.filter(node => selected.has(node.id))
.map(node => ({ ...node, config: JSON.parse(JSON.stringify(node.config || {})) }));
const edges = app.flowDraft.edges
.filter(edge => selected.has(edge.from) && selected.has(edge.to))
.map(edge => ({ ...edge }));
if (!nodes.length) return false;
flowClipboard = { nodes, edges };
flowClipboardPasteCount = 0;
if (notify) toast(tr('flow.blocksCopied', { count: nodes.length }));
return true;
}
function pasteFlowNodes({ notify = true } = {}) {
if (!app.flowDraft || !flowClipboard?.nodes?.length) return false;
flowClipboardPasteCount += 1;
const offset = 36 * flowClipboardPasteCount;
const idMap = new Map(flowClipboard.nodes.map(node => [node.id, newFlowId('node')]));
const copies = flowClipboard.nodes.map(node => ({
...node,
id: idMap.get(node.id),
config: JSON.parse(JSON.stringify(node.config || {})),
x: Math.max(12, Number(node.x || 0) + offset),
y: Math.max(12, Number(node.y || 0) + offset),
}));
const copiedEdges = flowClipboard.edges.map(edge => ({
...edge,
id: newFlowId('edge'),
from: idMap.get(edge.from),
to: idMap.get(edge.to),
}));
app.flowDraft.nodes.push(...copies);
app.flowDraft.edges.push(...copiedEdges);
app.flowSelectedNodeIds = copies.map(node => node.id);
app.flowSelectedNodeId = copies[copies.length - 1]?.id || null;
app.flowDirty = true;
renderFlowEditor();
if (notify) toast(tr('flow.blocksPasted', { count: copies.length }));
return true;
}
function duplicateSelectedFlowNodes() {
const count = (app.flowSelectedNodeIds || []).length;
if (!count || !copySelectedFlowNodes({ notify: false })) return;
if (pasteFlowNodes({ notify: false })) toast(tr('flow.blocksDuplicated', { count }));
}
function removeSelectedFlowNodes() { function removeSelectedFlowNodes() {
if (!app.flowDraft || !(app.flowSelectedNodeIds || []).length) return; if (!app.flowDraft || !(app.flowSelectedNodeIds || []).length) return;
const selected = new Set(app.flowSelectedNodeIds); const selected = new Set(app.flowSelectedNodeIds);
@@ -1250,11 +1311,68 @@ function updateFlowConfig(input) {
} }
let flowDrag = null; let flowDrag = null;
let flowMarquee = null;
function flowCanvasPoint(event) {
const workspace = $('#flowWorkspace');
if (!workspace) return { x: 0, y: 0 };
const rect = workspace.getBoundingClientRect();
const zoom = app.flowZoom || 1;
return {
x: (event.clientX - rect.left + workspace.scrollLeft) / zoom,
y: (event.clientY - rect.top + workspace.scrollTop) / zoom,
};
}
function updateFlowMarqueeSelection(event) {
if (!flowMarquee || !app.flowDraft) return;
const current = flowCanvasPoint(event);
const left = Math.min(flowMarquee.start.x, current.x);
const top = Math.min(flowMarquee.start.y, current.y);
const right = Math.max(flowMarquee.start.x, current.x);
const bottom = Math.max(flowMarquee.start.y, current.y);
const marquee = $('#flowSelectionMarquee');
if (marquee) {
marquee.hidden = false;
marquee.style.left = `${left}px`;
marquee.style.top = `${top}px`;
marquee.style.width = `${right - left}px`;
marquee.style.height = `${bottom - top}px`;
}
const hits = app.flowDraft.nodes.filter(node => {
const el = $(`[data-flow-node="${CSS.escape(node.id)}"]`);
const nodeLeft = Number(node.x || 0), nodeTop = Number(node.y || 0);
const nodeRight = nodeLeft + Number(el?.offsetWidth || 170);
const nodeBottom = nodeTop + Number(el?.offsetHeight || 96);
return nodeRight >= left && nodeLeft <= right && nodeBottom >= top && nodeTop <= bottom;
}).map(node => node.id);
const selected = [...new Set([...flowMarquee.baseSelection, ...hits])];
app.flowSelectedNodeIds = selected;
app.flowSelectedNodeId = selected[selected.length - 1] || null;
$$('[data-flow-node]').forEach(el => el.classList.toggle('selected', selected.includes(el.dataset.flowNode)));
const count = $('#flowSelectionCount'); if (count) count.textContent = selected.length ? tr('flow.selectedCount', { count: selected.length }) : '';
}
document.addEventListener('pointerdown', event => { document.addEventListener('pointerdown', event => {
if ($('#flowEditor')?.hidden) return; if ($('#flowEditor')?.hidden) return;
if (event.button !== 0) return;
const nodeEl = event.target.closest?.('[data-flow-node]'); const nodeEl = event.target.closest?.('[data-flow-node]');
if (!nodeEl) { if (!nodeEl) {
if (event.target.closest?.('#flowWorkspace') && !event.target.closest('button,input,select,label')) clearFlowSelection(); const workspace = event.target.closest?.('#flowWorkspace');
if (!workspace || event.target.closest?.('button,input,select,label,[data-flow-edge]')) return;
const additive = event.shiftKey || event.ctrlKey || event.metaKey;
flowMarquee = {
start: flowCanvasPoint(event),
clientX: event.clientX,
clientY: event.clientY,
baseSelection: additive ? [...(app.flowSelectedNodeIds || [])] : [],
additive,
moved: false,
pointerId: event.pointerId,
};
workspace.setPointerCapture?.(event.pointerId);
const marquee = $('#flowSelectionMarquee'); if (marquee) marquee.hidden = true;
event.preventDefault();
return; return;
} }
if (event.target.closest('button,input,select,label')) return; if (event.target.closest('button,input,select,label')) return;
@@ -1271,6 +1389,12 @@ document.addEventListener('pointerdown', event => {
nodeEl.setPointerCapture?.(event.pointerId); event.preventDefault(); nodeEl.setPointerCapture?.(event.pointerId); event.preventDefault();
}); });
document.addEventListener('pointermove', event => { document.addEventListener('pointermove', event => {
if (flowMarquee && app.flowDraft) {
flowMarquee.moved = flowMarquee.moved || Math.abs(event.clientX - flowMarquee.clientX) > 3 || Math.abs(event.clientY - flowMarquee.clientY) > 3;
if (flowMarquee.moved) updateFlowMarqueeSelection(event);
event.preventDefault();
return;
}
if (!flowDrag || !app.flowDraft) return; if (!flowDrag || !app.flowDraft) return;
let dx = (event.clientX - flowDrag.x) / (app.flowZoom || 1), dy = (event.clientY - flowDrag.y) / (app.flowZoom || 1); let dx = (event.clientX - flowDrag.x) / (app.flowZoom || 1), dy = (event.clientY - flowDrag.y) / (app.flowZoom || 1);
const minLeft = Math.min(...flowDrag.starts.map(item => item.left)); const minLeft = Math.min(...flowDrag.starts.map(item => item.left));
@@ -1283,7 +1407,21 @@ document.addEventListener('pointermove', event => {
}); });
app.flowDirty = true; renderFlowSaveStatus(); renderFlowEdges(); app.flowDirty = true; renderFlowSaveStatus(); renderFlowEdges();
}); });
document.addEventListener('pointerup', () => { flowDrag = null; }); document.addEventListener('pointerup', event => {
if (flowMarquee) {
if (!flowMarquee.moved) {
app.flowSelectedNodeIds = flowMarquee.additive ? [...flowMarquee.baseSelection] : [];
app.flowSelectedNodeId = app.flowSelectedNodeIds[app.flowSelectedNodeIds.length - 1] || null;
}
const workspace = $('#flowWorkspace'); workspace?.releasePointerCapture?.(flowMarquee.pointerId);
const marquee = $('#flowSelectionMarquee'); if (marquee) marquee.hidden = true;
flowMarquee = null;
renderFlowEditor();
event.preventDefault();
return;
}
flowDrag = null;
});
document.addEventListener('keydown', event => { document.addEventListener('keydown', event => {
const flowCard = event.target.closest?.('[data-flow-card-id]'); const flowCard = event.target.closest?.('[data-flow-card-id]');
@@ -1291,6 +1429,9 @@ document.addEventListener('keydown', event => {
if ($('#flowEditor')?.hidden || !app.flowDraft) return; if ($('#flowEditor')?.hidden || !app.flowDraft) return;
if (event.target.closest?.('input,select,textarea,[contenteditable="true"]')) return; if (event.target.closest?.('input,select,textarea,[contenteditable="true"]')) return;
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') { event.preventDefault(); selectAllFlowNodes(); return; } if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') { event.preventDefault(); selectAllFlowNodes(); return; }
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c') { event.preventDefault(); copySelectedFlowNodes(); return; }
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v') { event.preventDefault(); pasteFlowNodes(); return; }
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'd') { event.preventDefault(); duplicateSelectedFlowNodes(); return; }
if ((event.key === 'Delete' || event.key === 'Backspace') && (app.flowSelectedNodeIds || []).length) { event.preventDefault(); removeSelectedFlowNodes(); } if ((event.key === 'Delete' || event.key === 'Backspace') && (app.flowSelectedNodeIds || []).length) { event.preventDefault(); removeSelectedFlowNodes(); }
}); });
@@ -1333,6 +1474,7 @@ document.addEventListener('click', event => {
else if (action === 'run-flow-dry-run') runFlowDryRun(); else if (action === 'run-flow-dry-run') runFlowDryRun();
else if (action === 'flow-logs') openFlowLogs(); else if (action === 'flow-logs') openFlowLogs();
else if (action === 'flow-select-all') selectAllFlowNodes(); else if (action === 'flow-select-all') selectAllFlowNodes();
else if (action === 'flow-duplicate-selection') duplicateSelectedFlowNodes();
else if (action === 'flow-clear-selection') clearFlowSelection(); else if (action === 'flow-clear-selection') clearFlowSelection();
else if (action === 'edit-flow-name') editFlowName(); else if (action === 'edit-flow-name') editFlowName();
else if (action === 'flow-shared-input-settings') { else if (action === 'flow-shared-input-settings') {