v0.16.3
This commit is contained in:
1 parent
f376f0a7b6
commit
8059e16d12
36 files changed
+1123
-176
No files matched your search
Generated
+1
-1
@@ -719,7 +719,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.16.1"
|
||||
version = "0.16.3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.16.1"
|
||||
version = "0.16.3"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+38
-35
@@ -1,18 +1,17 @@
|
||||
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
|
||||
2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore
|
||||
1ece15d35e56470fbc0e9f0d9b1963f518c81a7b80270f2d2d91e53a4eb463ce ./Cargo.lock
|
||||
8ad83bc5b719a3c0e058d0d6d28cc5af834f7a6d07d8fdc89c4c8ae804e1eb25 ./Cargo.toml
|
||||
2f663bd03e41e40d584183074dca077c8963afc8308bd66e7d33ea6aed6ec1e5 ./Cargo.lock
|
||||
8f4f035398326b1beec96d2c0d4b8fcf4b7f1542e39fe719743cc95ecca740e1 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
3da89b24afe096e116b5725a279c2e84b493ea2ead4aa24895647e083c02298f ./README.md
|
||||
9d39a66e1b041fef40a180e918ebfc676b9bf0406af3237327201b2b60ab53e8 ./SOURCE_MANIFEST.sha256
|
||||
eaea820d2578f56442792e6fd6403eb1253686e50dbfcd8e75179c98574e1859 ./README.md
|
||||
2cc1518c6484c340037700ef258e27cdf71a7aa5233aefcb51cfa7c199c3f3a7 ./SOURCE_MANIFEST.sha256
|
||||
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
|
||||
a1f77b4c93063376a8568667713fc708d8e59541260834f9bb353ca44f1670cc ./docs/API.md
|
||||
dd90e743bb9d3743ea677300a2ce0276f87f02fec2db0a3ffdcf5cc1bdf6cc60 ./docs/FLOW.md
|
||||
7f8afa5a07a021f3f0fd49b1c65b14724e34229620ff58565b73dfe1fe7722a5 ./docs/openapi.json
|
||||
716523299bfdf791276cc7c590caa3ea54530207c5e05cebb45e2ba6ef9da915 ./flows/multisplit/v4/INSTRUKCJA.txt
|
||||
0a2f74baec4202b45708aecd7456d7339e4dbbe6c65fa497fb8b89c9d0e1d7c9 ./flows/multisplit/v4/igor-multisplit-soft-coast-v4.flow.json
|
||||
1e91ca707015988ea01b944570b88fb27fa9a7b8aca48a8493885969ce69b452 ./flows/multisplit/v4/jan-multisplit-soft-coast-v4.flow.json
|
||||
ef649edd25d41c2cf348149896003d21143bcadd599665c21322f6ee0d52ed36 ./flows/multisplit/v4/sypialnia-multisplit-soft-coast-v4.flow.json
|
||||
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
|
||||
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
|
||||
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
|
||||
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
|
||||
@@ -26,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
|
||||
8df63d9ae43ac72e47e1004ece2afc397f777c169f5715ad6148e3bfebedfc36 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
|
||||
ebe14ca743c50cc0cc02f94621b17119984704f702df04cd94e1e5b86635a014 ./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
|
||||
@@ -34,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
|
||||
eaca083a91d15b31ed3415171f9bf4773312245ebd80ef0e9873d4ea11266986 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
1b32f617ed09f37d58fd8532107cfcbf2fb92c4434bd173b0fc73464664ce927 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
f7038a752048ecb1d2004e9dcccb9b43e5534bae4046331ceb33322316ad83b4 ./ha-addon/repository/gree-controller/README.md
|
||||
878d3d369c458bc0c2d261b339be1843379e9f77f28141e3db0f19b77cd11105 ./ha-addon/repository/gree-controller/config.yaml
|
||||
0e92e538aa03b442f8fc2c21562cc7aa19d8ae346f78cecca1eec131288edade ./ha-addon/repository/gree-controller/README.md
|
||||
e48d172951db6df8afe97b0d463902e744c22bd3e677041bf5e6ee5624646f25 ./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
|
||||
@@ -47,8 +46,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
|
||||
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
|
||||
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
271aa0ff32672f7888f0cfcb12dc1543ab22640549d5a43c4f3db9d6d45b64ee ./lang/en.json
|
||||
0f6a09664d90b2a5edf9b7c33d77baf1ea39e94392ae3cc868d68169b4a8ad4f ./lang/pl.json
|
||||
4bf92ee5627be67b66d76a6106b224159f95071f0aa2a727cd964e113bc3b32a ./lang/en.json
|
||||
1e056cd915ba2e808db1287c99c06bbbbb3c98e5008481471b895e6ec20ea43a ./lang/pl.json
|
||||
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
||||
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
|
||||
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
|
||||
@@ -88,8 +87,8 @@ ae21459a261712bcb8d57594528b1648432e8d03a8a38502234829c0dbfef774 ./presets/week
|
||||
fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/window_available_guard.json
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
|
||||
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
|
||||
532ed10e2b6b52ad07685e90858ead075d51124b2cba4da57d19221224bfca19 ./scripts/api_dev_test.py
|
||||
437ac712fe194ecbd6e2299c834894887ab860a5a563df9a7eea4b0a2f6818c7 ./scripts/README.md
|
||||
971b899ff544c215b754598f77f8f50b108868a119ec741d2abebfb6fb833ddf ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
@@ -101,17 +100,20 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
36396ecfcfd30f50016b17a83cd76cbccd8debaacef45a0d6b4e61946fef098d ./scripts/smoke.sh
|
||||
b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js
|
||||
2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
ea84f5230da97447c60dfb4f8dadd0747c92fd1f7a74d5a65d6fdb6f23cf4b6d ./src/api.rs
|
||||
14f07768257945ed52c218f3bf03c25cb55053e7a6683c8ff17f395e16d28f2e ./scripts/verify_simple_heating.py
|
||||
c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs
|
||||
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
|
||||
1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs
|
||||
36ad26dfc488e74359355069e5ea3309527b3d03bf879abf51c5d28497389901 ./src/api/automations.rs
|
||||
34378c387d34d67483030d1016b874a74115c1ef5f268f854180c25146fdb08b ./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
|
||||
a4bd4d09ee1f6ea6039d2eb563e446142aab6b28f7de510ca8a27ba3465abb92 ./src/api/flows.rs
|
||||
8c5d46ccaa45950f4dbaae63e68ad6efe393ff0904e46420f5e7950f043fb691 ./src/api/flows.rs
|
||||
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs
|
||||
@@ -124,7 +126,7 @@ b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/publ
|
||||
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
|
||||
7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs
|
||||
3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs
|
||||
95e636288b46cdeb00f9fbf34ac2dbd226184e0eca5a3a111f188a305b0a5258 ./src/api/zones.rs
|
||||
7c65692070e51af1ebd66d98b087b7f7a241ed31f8ff2f41baf77e46e027688c ./src/api/zones.rs
|
||||
32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs
|
||||
de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs
|
||||
87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs
|
||||
@@ -139,11 +141,11 @@ bba6e3fb9f4800ae06d0ca67772b06a8c389d308243306719442d0c1f2623420 ./src/db/flows
|
||||
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
|
||||
28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs
|
||||
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
|
||||
0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs
|
||||
0bad57d36f6f58c40b0be595c3fe47d981460d61736fb48f389f649ccd4bea3b ./src/engine/automations.rs
|
||||
7b29709d2a97e201a6f7baab9c3a72ea3792488499c47e426ebb93369fcbb3fc ./src/engine.rs
|
||||
4412770793e4905d490a56e0f46cc678a5e28044fc0d136912ef1b81da3e3e3f ./src/engine/automations.rs
|
||||
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
|
||||
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
|
||||
828657b3c13b68792991b5ca2a26ae0b1d5c417191c83c4e5f650f62b6440096 ./src/engine/control_plan.rs
|
||||
dbf73f1459185836487653d05e73ffd8314fb0244d23878fc84232fd10935b8a ./src/engine/control_plan.rs
|
||||
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
|
||||
519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs
|
||||
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
|
||||
@@ -153,12 +155,13 @@ e61d7dea4b60e06fd8d08daaceecf53bd71b13ea2b0ad462692049f3cdaa5cef ./src/engine/o
|
||||
5fafacadbc4a3d7e375a8079f9ae1b80eb013c2fb651b8fcc65f5f17cf023426 ./src/engine/polling.rs
|
||||
c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/runtime.rs
|
||||
48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs
|
||||
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
|
||||
9adfcb3c3dffbaa870b314904bd1995e7b78c3f12febd69e23433179e4bf3fba ./src/engine/simple_heat.rs
|
||||
2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs
|
||||
d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs
|
||||
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
|
||||
ca5fbcdf44842766dd0741d2b728656535ff3a00f4a8a6da989d7183ceda51b6 ./src/engine/tests.rs
|
||||
6f81f1b93f653b8450b9e086ed4728eca90b3d5d7639b7f4fb4fa2f42c514a28 ./src/engine/tests.rs
|
||||
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
|
||||
edee10929eb89d7a774474cd9054890ee53b8c1fdf54e3d6f143265dead8f6b8 ./src/engine/zone_control.rs
|
||||
2cbfc34ad0868591e60e3110d8ae346dff07a58c894e87598e4b7b5749fb4613 ./src/engine/zone_control.rs
|
||||
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
|
||||
d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs
|
||||
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
|
||||
@@ -167,7 +170,7 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb
|
||||
67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs
|
||||
62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs
|
||||
23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs
|
||||
75bb9db327ae4ab1f29a5aa888e130be3a8e75b714fb5dbad4307bebee583c66 ./src/models/automation.rs
|
||||
de10221fb1cb7c9eb2f658bc07d710f0b03ed573d994c0df31851cf7c49c4605 ./src/models/automation.rs
|
||||
e4195a40135f408302a78cb200a54e6ef1c0e6031fb069a2431d068e5dbbdd8d ./src/models/control_plan.rs
|
||||
06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs
|
||||
698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs
|
||||
@@ -177,7 +180,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f
|
||||
17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs
|
||||
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
|
||||
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
|
||||
94ed531788a118f2f3fb5dd1748b2f416480a5945d8c7c7a722c201ce03212dd ./src/models/zone.rs
|
||||
5229bdb5ce1018601b081654d16cee9ac28afdfda6c67effe1b14d44b14b3257 ./src/models/zone.rs
|
||||
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
|
||||
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
|
||||
578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
|
||||
@@ -203,18 +206,18 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
6a30a5ecee99d2cf40381b9f770ed574e9e85b8b50e3425a77e6f64283faeee6 ./web/css/styles.css
|
||||
282ef3c84f053cfce36fa85828365cac2cb6dfa4375993345ff3bee05c8a884f ./web/css/styles.css
|
||||
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
477de38c1cfbf2b925059795ef1b30522813d70908c36d3a471faf16da6935c8 ./web/index.html
|
||||
e76b1e0bbdad0297d5ae2e79e40e658e0263f038ab70db81d330a342e01a0a8b ./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
|
||||
c58dddbd1238ed9dfb590fc810e569c9a8408113cd85623e86190874591079e6 ./web/js-dynamic/entities.js
|
||||
3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./web/js-dynamic/entities.js
|
||||
bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js
|
||||
c4e14ebc7ddcec3c7346a037d488ceacd19606f76e84004b4f7c7dbd5fef646f ./web/js-dynamic/flows.js
|
||||
3e2a5f1aab56e918624e6e7af064eecb9e94e554fd4d185715f8ecf3747a6895 ./web/js-dynamic/flows.js
|
||||
1d54ae8919edb61414f9ffc46f85394b29c3c355827120264352cccb32f65e68 ./web/js-dynamic/forms.js
|
||||
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
|
||||
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.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.1**
|
||||
**Version: 0.16.3**
|
||||
|
||||
Version 0.16.1 adds configurable early heat coasting without unit ON/OFF cycling, per-Flow settings for coasting and deep standby, and separate Flow/manual status in the Web UI. Existing compressor protection and shared multisplit OFF logic are unchanged.
|
||||
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.
|
||||
|
||||
> [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`.
|
||||
@@ -61,6 +61,9 @@ Useful development commands:
|
||||
./scripts/dev.sh --release # optimized local build
|
||||
./scripts/dev.sh --reset # recreate the local SQLite database
|
||||
./scripts/smoke.sh # API smoke test against an isolated simulated instance
|
||||
python3 scripts/verify_simple_heating.py # JSON/JS/import/metadata consistency (no Rust required)
|
||||
node scripts/test_simple_heating_ui.js # Flow vs manual UI status scenarios
|
||||
cargo test simple_heat_tests # six Rust temperature-regulation scenarios
|
||||
```
|
||||
|
||||
The default panel is available at `http://127.0.0.1:8787` when the process is bound locally.
|
||||
@@ -506,3 +509,7 @@ The editor supports optimistic revisions, so saving an older copy from another b
|
||||
Diagnostics include a non-mutating dry-run endpoint/UI with a selectable simulation time, optional per-block sensor/state overrides, a per-node condition trace and an ownership/block reason. The Flow list also exposes a simpler scenario simulator beside the editor button: it uses live values by default and reduces the result to clear will-run / will-not-run / blocked outcomes with readable condition checks. Shared Home Assistant Flow inputs can also be tested directly in their editor against the current HA entity state. Flow-scoped execution and dry-run events can be viewed from the editor.
|
||||
|
||||
Individual Flows can be exported/imported as versioned `gree-controller-flow` JSON. A saved Flow can be exported directly from its card in the Flow list without opening the editor, and its description can be edited there through a quick modal; the editor export remains available for saved and unsaved drafts. Generated schedules and automations are intentionally excluded from the portable document and are regenerated from the source graph on import. Incomplete Flows can also be saved as disabled drafts; drafts preserve the editor graph but intentionally generate no schedules or automations until completed and saved normally. Shared inputs show which Flows reference them and link directly to those editors. The preset library includes 37 categorized scenarios for comfort, energy, safety, night, reliability, Home Assistant heat-source coordination and advanced multi-branch logic.
|
||||
|
||||
### Proste grzanie Flow (0.16.2)
|
||||
|
||||
Dla sypialni, Jana i Igora gotowe proste Flow (po 2 bloki) znajdują się w `flows/simple-heating/`. Zastępują złożone grafy v4; po imporcie należy dezaktywować stare v4. Osobne cele nocne: sypialnia 20.5 °C, Jan 20.8 °C, Igor 20.8 °C (można edytować w bloku). Dzienny cel 20.0 °C, próg uruchomienia 19.7 °C, zatrzymanie po 30 min stabilności. Wymaga włączonego harmonogramu **Trybu nocnego**. Działanie i progi: `docs/FLOW.md`.
|
||||
+37
-34
@@ -1,17 +1,16 @@
|
||||
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
|
||||
2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore
|
||||
1ece15d35e56470fbc0e9f0d9b1963f518c81a7b80270f2d2d91e53a4eb463ce ./Cargo.lock
|
||||
8ad83bc5b719a3c0e058d0d6d28cc5af834f7a6d07d8fdc89c4c8ae804e1eb25 ./Cargo.toml
|
||||
2f663bd03e41e40d584183074dca077c8963afc8308bd66e7d33ea6aed6ec1e5 ./Cargo.lock
|
||||
8f4f035398326b1beec96d2c0d4b8fcf4b7f1542e39fe719743cc95ecca740e1 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
3da89b24afe096e116b5725a279c2e84b493ea2ead4aa24895647e083c02298f ./README.md
|
||||
eaea820d2578f56442792e6fd6403eb1253686e50dbfcd8e75179c98574e1859 ./README.md
|
||||
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
|
||||
a1f77b4c93063376a8568667713fc708d8e59541260834f9bb353ca44f1670cc ./docs/API.md
|
||||
dd90e743bb9d3743ea677300a2ce0276f87f02fec2db0a3ffdcf5cc1bdf6cc60 ./docs/FLOW.md
|
||||
7f8afa5a07a021f3f0fd49b1c65b14724e34229620ff58565b73dfe1fe7722a5 ./docs/openapi.json
|
||||
716523299bfdf791276cc7c590caa3ea54530207c5e05cebb45e2ba6ef9da915 ./flows/multisplit/v4/INSTRUKCJA.txt
|
||||
0a2f74baec4202b45708aecd7456d7339e4dbbe6c65fa497fb8b89c9d0e1d7c9 ./flows/multisplit/v4/igor-multisplit-soft-coast-v4.flow.json
|
||||
1e91ca707015988ea01b944570b88fb27fa9a7b8aca48a8493885969ce69b452 ./flows/multisplit/v4/jan-multisplit-soft-coast-v4.flow.json
|
||||
ef649edd25d41c2cf348149896003d21143bcadd599665c21322f6ee0d52ed36 ./flows/multisplit/v4/sypialnia-multisplit-soft-coast-v4.flow.json
|
||||
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
|
||||
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
|
||||
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
|
||||
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
|
||||
@@ -25,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
|
||||
8df63d9ae43ac72e47e1004ece2afc397f777c169f5715ad6148e3bfebedfc36 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
|
||||
ebe14ca743c50cc0cc02f94621b17119984704f702df04cd94e1e5b86635a014 ./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 +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
|
||||
eaca083a91d15b31ed3415171f9bf4773312245ebd80ef0e9873d4ea11266986 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
1b32f617ed09f37d58fd8532107cfcbf2fb92c4434bd173b0fc73464664ce927 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
f7038a752048ecb1d2004e9dcccb9b43e5534bae4046331ceb33322316ad83b4 ./ha-addon/repository/gree-controller/README.md
|
||||
878d3d369c458bc0c2d261b339be1843379e9f77f28141e3db0f19b77cd11105 ./ha-addon/repository/gree-controller/config.yaml
|
||||
0e92e538aa03b442f8fc2c21562cc7aa19d8ae346f78cecca1eec131288edade ./ha-addon/repository/gree-controller/README.md
|
||||
e48d172951db6df8afe97b0d463902e744c22bd3e677041bf5e6ee5624646f25 ./ha-addon/repository/gree-controller/config.yaml
|
||||
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
|
||||
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
|
||||
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
|
||||
@@ -46,8 +45,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
|
||||
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
|
||||
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
271aa0ff32672f7888f0cfcb12dc1543ab22640549d5a43c4f3db9d6d45b64ee ./lang/en.json
|
||||
0f6a09664d90b2a5edf9b7c33d77baf1ea39e94392ae3cc868d68169b4a8ad4f ./lang/pl.json
|
||||
4bf92ee5627be67b66d76a6106b224159f95071f0aa2a727cd964e113bc3b32a ./lang/en.json
|
||||
1e056cd915ba2e808db1287c99c06bbbbb3c98e5008481471b895e6ec20ea43a ./lang/pl.json
|
||||
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
||||
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
|
||||
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
|
||||
@@ -87,8 +86,8 @@ ae21459a261712bcb8d57594528b1648432e8d03a8a38502234829c0dbfef774 ./presets/week
|
||||
fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/window_available_guard.json
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
|
||||
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
|
||||
532ed10e2b6b52ad07685e90858ead075d51124b2cba4da57d19221224bfca19 ./scripts/api_dev_test.py
|
||||
437ac712fe194ecbd6e2299c834894887ab860a5a563df9a7eea4b0a2f6818c7 ./scripts/README.md
|
||||
971b899ff544c215b754598f77f8f50b108868a119ec741d2abebfb6fb833ddf ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
@@ -100,17 +99,20 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
36396ecfcfd30f50016b17a83cd76cbccd8debaacef45a0d6b4e61946fef098d ./scripts/smoke.sh
|
||||
b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js
|
||||
2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
ea84f5230da97447c60dfb4f8dadd0747c92fd1f7a74d5a65d6fdb6f23cf4b6d ./src/api.rs
|
||||
14f07768257945ed52c218f3bf03c25cb55053e7a6683c8ff17f395e16d28f2e ./scripts/verify_simple_heating.py
|
||||
c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs
|
||||
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
|
||||
1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs
|
||||
36ad26dfc488e74359355069e5ea3309527b3d03bf879abf51c5d28497389901 ./src/api/automations.rs
|
||||
34378c387d34d67483030d1016b874a74115c1ef5f268f854180c25146fdb08b ./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
|
||||
a4bd4d09ee1f6ea6039d2eb563e446142aab6b28f7de510ca8a27ba3465abb92 ./src/api/flows.rs
|
||||
8c5d46ccaa45950f4dbaae63e68ad6efe393ff0904e46420f5e7950f043fb691 ./src/api/flows.rs
|
||||
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs
|
||||
@@ -123,7 +125,7 @@ b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/publ
|
||||
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
|
||||
7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs
|
||||
3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs
|
||||
95e636288b46cdeb00f9fbf34ac2dbd226184e0eca5a3a111f188a305b0a5258 ./src/api/zones.rs
|
||||
7c65692070e51af1ebd66d98b087b7f7a241ed31f8ff2f41baf77e46e027688c ./src/api/zones.rs
|
||||
32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs
|
||||
de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs
|
||||
87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs
|
||||
@@ -138,11 +140,11 @@ bba6e3fb9f4800ae06d0ca67772b06a8c389d308243306719442d0c1f2623420 ./src/db/flows
|
||||
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
|
||||
28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs
|
||||
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
|
||||
0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs
|
||||
0bad57d36f6f58c40b0be595c3fe47d981460d61736fb48f389f649ccd4bea3b ./src/engine/automations.rs
|
||||
7b29709d2a97e201a6f7baab9c3a72ea3792488499c47e426ebb93369fcbb3fc ./src/engine.rs
|
||||
4412770793e4905d490a56e0f46cc678a5e28044fc0d136912ef1b81da3e3e3f ./src/engine/automations.rs
|
||||
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
|
||||
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
|
||||
828657b3c13b68792991b5ca2a26ae0b1d5c417191c83c4e5f650f62b6440096 ./src/engine/control_plan.rs
|
||||
dbf73f1459185836487653d05e73ffd8314fb0244d23878fc84232fd10935b8a ./src/engine/control_plan.rs
|
||||
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
|
||||
519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs
|
||||
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
|
||||
@@ -152,12 +154,13 @@ e61d7dea4b60e06fd8d08daaceecf53bd71b13ea2b0ad462692049f3cdaa5cef ./src/engine/o
|
||||
5fafacadbc4a3d7e375a8079f9ae1b80eb013c2fb651b8fcc65f5f17cf023426 ./src/engine/polling.rs
|
||||
c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/runtime.rs
|
||||
48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs
|
||||
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
|
||||
9adfcb3c3dffbaa870b314904bd1995e7b78c3f12febd69e23433179e4bf3fba ./src/engine/simple_heat.rs
|
||||
2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs
|
||||
d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs
|
||||
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
|
||||
ca5fbcdf44842766dd0741d2b728656535ff3a00f4a8a6da989d7183ceda51b6 ./src/engine/tests.rs
|
||||
6f81f1b93f653b8450b9e086ed4728eca90b3d5d7639b7f4fb4fa2f42c514a28 ./src/engine/tests.rs
|
||||
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
|
||||
edee10929eb89d7a774474cd9054890ee53b8c1fdf54e3d6f143265dead8f6b8 ./src/engine/zone_control.rs
|
||||
2cbfc34ad0868591e60e3110d8ae346dff07a58c894e87598e4b7b5749fb4613 ./src/engine/zone_control.rs
|
||||
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
|
||||
d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs
|
||||
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
|
||||
@@ -166,7 +169,7 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb
|
||||
67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs
|
||||
62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs
|
||||
23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs
|
||||
75bb9db327ae4ab1f29a5aa888e130be3a8e75b714fb5dbad4307bebee583c66 ./src/models/automation.rs
|
||||
de10221fb1cb7c9eb2f658bc07d710f0b03ed573d994c0df31851cf7c49c4605 ./src/models/automation.rs
|
||||
e4195a40135f408302a78cb200a54e6ef1c0e6031fb069a2431d068e5dbbdd8d ./src/models/control_plan.rs
|
||||
06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs
|
||||
698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs
|
||||
@@ -176,7 +179,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f
|
||||
17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs
|
||||
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
|
||||
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
|
||||
94ed531788a118f2f3fb5dd1748b2f416480a5945d8c7c7a722c201ce03212dd ./src/models/zone.rs
|
||||
5229bdb5ce1018601b081654d16cee9ac28afdfda6c67effe1b14d44b14b3257 ./src/models/zone.rs
|
||||
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
|
||||
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
|
||||
578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
|
||||
@@ -202,18 +205,18 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
6a30a5ecee99d2cf40381b9f770ed574e9e85b8b50e3425a77e6f64283faeee6 ./web/css/styles.css
|
||||
282ef3c84f053cfce36fa85828365cac2cb6dfa4375993345ff3bee05c8a884f ./web/css/styles.css
|
||||
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
477de38c1cfbf2b925059795ef1b30522813d70908c36d3a471faf16da6935c8 ./web/index.html
|
||||
e76b1e0bbdad0297d5ae2e79e40e658e0263f038ab70db81d330a342e01a0a8b ./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
|
||||
c58dddbd1238ed9dfb590fc810e569c9a8408113cd85623e86190874591079e6 ./web/js-dynamic/entities.js
|
||||
3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./web/js-dynamic/entities.js
|
||||
bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js
|
||||
c4e14ebc7ddcec3c7346a037d488ceacd19606f76e84004b4f7c7dbd5fef646f ./web/js-dynamic/flows.js
|
||||
3e2a5f1aab56e918624e6e7af064eecb9e94e554fd4d185715f8ecf3747a6895 ./web/js-dynamic/flows.js
|
||||
1d54ae8919edb61414f9ffc46f85394b29c3c355827120264352cccb32f65e68 ./web/js-dynamic/forms.js
|
||||
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
|
||||
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
|
||||
|
||||
+14
-4
@@ -1,8 +1,10 @@
|
||||
# GREE Controller API reference
|
||||
|
||||
HTTP and WebSocket API for GREE Controller **0.16.1**.
|
||||
HTTP and WebSocket API for GREE Controller **0.16.3**.
|
||||
|
||||
Version 0.16.1: Zone configuration exposes `heat_anticipation_c` (0..2.5°C, default 0), runtime Zone exposes `heat_coasting`, and optional Flow `zone_thermostat` node configuration supports `heat_anticipation_c` and `standby_offset_c`. These settings alter the GREE temperature target, not physical power.
|
||||
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.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.
|
||||
|
||||
|
||||
[← Main documentation](../README.md)
|
||||
@@ -246,7 +248,7 @@ Response:
|
||||
{
|
||||
"status": "ok",
|
||||
"name": "gree-controller",
|
||||
"version": "0.16.1",
|
||||
"version": "0.16.3",
|
||||
"uptime_seconds": 1234,
|
||||
"control_ready": true,
|
||||
"time": "2026-08-30T06:54:00Z"
|
||||
@@ -309,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.1",
|
||||
"version": "0.16.3",
|
||||
"uptime_seconds": 1234,
|
||||
"auth_required": false,
|
||||
"control_ready": true,
|
||||
@@ -1734,6 +1736,14 @@ Runtime settings expose `compressor_protection_enabled` and `compressor_protecti
|
||||
|
||||
Cancellation suppresses the same pending intent until a new explicit thermostat/group/house command re-arms it, a scheduled Temporary Quick Thermostat session takes ownership, or a different mode/target creates a new intent. Safety OFF commands are not delayed by compressor protection. Direct technical device commands remain immediate manual-control operations.
|
||||
|
||||
### Simple two-node heating Flow (0.16.2)
|
||||
|
||||
Create a `constant` condition with `{"value":true}` and connect it to a `zone_heat_cycle` action. The action configuration takes `zone_id` (zone UUID), `day_target`, `night_target`, `day_min`, `boost_after_minutes` (3–30) and `day_off_after_minutes` (10–120). Day/night boundaries come from global Night Mode. The generated Flow automation owns an independent per-zone temperature policy; this is **automatic** ownership (`control_source=automation.flow`), not a manual `custom` preset. The measured temperature comes from that zone's configured GREE/HA/combined sensor.
|
||||
|
||||
GREE modulation at a night room target of 20.6 °C: 21 °C for a moderate dip, 23 °C for a deep dip or no rise after the configured trial, 20 °C at 20.5 °C in the room, 19 °C at/above 20.6 °C. Nighttime physical power remains ON. During day the indoor unit is OFF while above `day_min`; after falling below it the unit heats and powers OFF after the configured continuous time at `day_target`. Compressor-protection settings still apply. Disabling/removing the Flow clears its thermostat policy on the next zone cycle.
|
||||
|
||||
Ready-to-import examples are in `flows/simple-heating/`. Disable older Flow v4 before importing to prevent conflicting rules.
|
||||
|
||||
### Flow portability and diagnostics
|
||||
|
||||
`GET /api/flows/:id/export` returns a versioned `gree-controller-flow` JSON document containing the source graph plus definitions of every referenced Shared Input. Generated schedules/automations are not exported. The existing envelope may include an optional top-level `shared_inputs` array while leaving the `flow` object unchanged.
|
||||
|
||||
@@ -97,3 +97,29 @@ These need durable state and restart semantics before they should be exposed in
|
||||
### Display name / alias
|
||||
|
||||
The Flow name edited in `/flows` is a presentation alias. Renaming it changes labels shown in the UI (including the dashboard) but does not change the stable Flow ID or generated `flow-<unique-id>` schedule/automation names.
|
||||
|
||||
## Proste grzanie dzień/noc (od 0.16.2)
|
||||
|
||||
Do jednego pokoju potrzebne są **tylko dwa bloki**: `Stała: Tak` → `Proste grzanie dzień/noc`.
|
||||
Blok ustawiasz na odpowiednią strefę oraz:
|
||||
|
||||
- `day_target` – komfort w dzień (np. 20.0 °C), `night_target` – komfort w nocy (osobny dla każdego pokoju),
|
||||
- `day_min` – próg alarmowy w dzień (np. 19.7 °C); powyżej niego jednostka pozostaje **OFF** dopóki wcześniej nie zaczęła ratunkowego dogrzewania,
|
||||
- `boost_after_minutes` – czas próby delikatnego grzania (np. 10 min; Jan: 8 min),
|
||||
- `day_off_after_minutes` – czas ciągłego utrzymywania dziennego celu przed wyłączeniem (np. 30 min).
|
||||
|
||||
Przebieg (dla celu 20.6 °C): noc: **20.6 °C → GREE 19 °C**, od 20.5 °C wyhamowanie **20 °C**, poniżej tego delikatnie **21 °C**; poniżej 19.8 °C lub po 10 minutach bez wzrostu o 0.1 °C grzanie **23 °C**. Przy celu innego pokoju nastawy GREE zmieniają się proporcjonalnie do celu, z zaokrągleniem do całych stopni. Jednostka pozostaje włączona w nocy (niższa nastawa pozwala jej się samoczynnie uspokoić).
|
||||
|
||||
Dzień: przy celu 20.0 °C łagodna faza ustawia GREE na 21 °C, a pełna faza na 22 °C. Nad `day_min` jednostka jest fizycznie wyłączona. Przy spadku poniżej `day_min` podejmuje grzanie; po osiągnięciu `day_target` przez nieprzerwane `day_off_after_minutes` wyłącza jednostkę, z zachowaniem zabezpieczeń sprężarki. Jeśli w tym czasie cel zostaje utracony, odliczanie zaczyna się od nowa.
|
||||
|
||||
**Ważne:** Ustawienia > **Tryb nocny** muszą mieć włączony harmonogram i prawidłowe godziny, ponieważ ten sam kalendarz ustala dzień/noc dla wszystkich stref. Temperatura pokoju pochodzi ze skonfigurowanego czujnika termostatu strefy (w tym czujnika HA/combined), a nie z osobnego warunku Flow. Pole `control_source=automation.flow` oznacza automatyczną kontrolę (nie ręczną), również gdy GREE jest w uśpieniu. Rzeczywisty pilot/ręczne sterowanie nadal ma pierwszeństwo.
|
||||
|
||||
Gotowe importy: `flows/simple-heating/`. Po imporcie **wyłącz lub usuń oryginalne Flow v4**, żeby uniknąć konkurujących poleceń. Wyłączenie nowego Flow usuwa jego profil sterowania w następnym cyklu regulatora.
|
||||
|
||||
### Pomoc w samych blokach (0.16.3)
|
||||
|
||||
Po kliknięciu bloku **Stała testowa** edytor wyjaśnia, że wartość **Tak** jest bezwarunkowym sygnałem wejściowym w każdym cyklu, a **Nie** blokuje akcję. W bibliotece bloków oba nowe opisy są widoczne pod nazwami; w palecie i na schemacie pojawiają się jako podpowiedzi po najechaniu.
|
||||
|
||||
W bloku **Proste grzanie dzień/noc** opis jest widoczny nad ustawieniami, a sekcja **Jak działa?** rozwija kolejne fazy: nocne uśpienie, hamowanie, delikatne i mocne grzanie, dzienny próg uruchomienia oraz warunek wyłączenia po stabilizacji. Temperatury przykładowe są wyliczane na podstawie rzeczywistych wartości ustawionych w edytowanym bloku, więc Jan, Igor i sypialnia mają poprawne progi. Opis obejmuje też dobór źródła pomiaru w strefie, zachowanie przy braku danych HA i pierwszeństwo sterowania ręcznego.
|
||||
|
||||
Ta aktualizacja dotyczy **tylko pomocy UI**. Nie zmienia algorytmu, formatu eksportu Flow ani nastaw trzech wcześniejszych importów; nie trzeba ich ponownie importować.
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "GREE Controller API",
|
||||
"version": "0.16.1",
|
||||
"version": "0.16.3",
|
||||
"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.1"
|
||||
"example": "0.16.3"
|
||||
},
|
||||
"uptime_seconds": {
|
||||
"type": "integer",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.16.1",
|
||||
"version": "0.16.3",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## 0.16.3
|
||||
|
||||
- Adds expandable, per-room examples for nighttime 19/21/23°C modulation, daytime OFF/boost/30-minute stability, zone temperature-source fallback and Flow vs manual ownership.
|
||||
- Adds tooltips explaining each day/night heating setting. No changes to runtime heating algorithms, saved Flow JSON schema or existing imported flows.
|
||||
|
||||
## 0.16.2
|
||||
|
||||
- Adds `zone_heat_cycle`: a single day/night room-temperature controller for one zone. Use a constant true condition plus the new block (two Flow nodes total).
|
||||
- Night: low demand first (typically GREE 21 °C), boost to target +2 °C after 8–10 minutes without warming or a deep temperature drop, brake 0.1 °C ahead and standby at ~19 °C when reached. Keep the indoor unit powered without repeated ON/OFF.
|
||||
- Day: physically OFF above the configured room minimum until heating is needed; maintain room temperature, then OFF after 30 continuous minutes at goal with compressor minimum-on protection.
|
||||
- Keeps individual targets for Bedroom, Jan and Igor; includes three ready-to-import two-block Flow definitions.
|
||||
- Correctly identifies Flow-owned thermostats as automatic, including day OFF/pending OFF and night standby; respects genuine manual control.
|
||||
- Adds focused Rust unit cases, UI ownership tests and a static release verification script. Rust tests/build still require a Rust toolchain on the CI/deployment host.
|
||||
|
||||
## 0.16.1
|
||||
|
||||
- Adds hysteretic early-heating coasting: lower the GREE target before reaching room comfort temperature without switching the indoor unit OFF.
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
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.16.1 adds early heating coasting with GREE setpoint modulation, configurable from Visual Flow, plus distinct Flow/manual status. Compressor protection and shared multisplit shutdown logic remain unchanged.
|
||||
|
||||

|
||||
|
||||
For VLAN deployments, configure VLAN interfaces on the Home Assistant OS host. The add-on uses `host_network: true`; with multiple directly attached subnets, `gree_interface` may remain empty. Run broadcast discovery separately for each VLAN/subnet.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: "GREE Controller"
|
||||
version: "0.16.1"
|
||||
version: "0.16.3"
|
||||
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/"
|
||||
|
||||
@@ -1036,6 +1036,37 @@
|
||||
"flow.node.and": "AND",
|
||||
"flow.node.or": "OR",
|
||||
"flow.node.not": "NOT",
|
||||
"flow.node.simpleHeat": "Simple day/night heating",
|
||||
"flow.simpleHeatDay": "Day target",
|
||||
"flow.simpleHeatNight": "Night target",
|
||||
"flow.simpleHeatMinimum": "Day minimum temperature",
|
||||
"flow.simpleHeatBoostMinutes": "Boost after (min)",
|
||||
"flow.simpleHeatOffMinutes": "Turn off after day stability (min)",
|
||||
"flow.simpleHeatHint": "Connect “Test constant: Yes” to this block. Night hours are set in Night mode; detailed rules are below.",
|
||||
"flow.constantDescription": "An always-true/false condition. Set YES so heating runs automatically on each Flow cycle.",
|
||||
"flow.constantHelp": "YES: the connected action is evaluated on every automation cycle. NO: the action is blocked. Day/night decisions, temperatures and timers are handled by the heating action, so no other conditions are needed.",
|
||||
"flow.simpleHeatDescription": "One block controls room temperature by day and night: it selects GREE targets, eases off heating and turns the unit OFF during the day.",
|
||||
"flow.simpleHeatHowItWorks": "How it works — example with these settings",
|
||||
"flow.simpleHeatHelpIntro": "The targets are room temperatures, not direct air-conditioner setpoints. The block calculates GREE whole-degree settings for the selected zone.",
|
||||
"flow.simpleHeatHelpNightTitle": "Night — room target {target}; unit stays ON",
|
||||
"flow.simpleHeatHelpNightSleep": "At {room} or above: GREE {gree} — standby without powering off.",
|
||||
"flow.simpleHeatHelpNightBrake": "From {room} until just below target: GREE {gree} — early braking.",
|
||||
"flow.simpleHeatHelpNightGentle": "Below {room}: GREE {gree} — gentle heating (until boost conditions apply).",
|
||||
"flow.simpleHeatHelpNightBoost": "At {room} or lower, or after {minutes} min without a 0.1°C rise: GREE {gree} — stronger heating.",
|
||||
"flow.simpleHeatHelpDayTitle": "Day — target {target}, minimum room temperature {minimum}",
|
||||
"flow.simpleHeatHelpDayOff": "Keep the unit OFF until room temperature falls below {minimum}.",
|
||||
"flow.simpleHeatHelpDayHeat": "Below the minimum: GREE {gentle}; at {room} or lower, or after {minutes} min without improvement: GREE {boost}.",
|
||||
"flow.simpleHeatHelpDayStop": "After staying at {target} or above for {minutes} continuous minutes: OFF. A drop below target resets the timer.",
|
||||
"flow.simpleHeatHelpSensors": "Sensor: follows the zone setting (HA, GREE or combined). If fresh HA data is missing, the zone may use GREE when its sensor is available. In combined mode, a large disagreement can also select GREE.",
|
||||
"flow.simpleHeatHelpSchedule": "Day/night times: Settings → Night mode. Each room has its own targets in this block.",
|
||||
"flow.simpleHeatHelpControl": "Shown as automatic Flow control, not manual. Genuine remote/manual commands take priority; compressor protections still apply.",
|
||||
"flow.simpleHeatDayTip": "Desired daytime room temperature; this is not the direct GREE setpoint.",
|
||||
"flow.simpleHeatNightTip": "Desired room temperature during the global night-mode window.",
|
||||
"flow.simpleHeatMinimumTip": "The daytime unit stays OFF above this threshold; below it, rescue heating begins.",
|
||||
"flow.simpleHeatBoostTip": "How many minutes to try gentle heating before boosting when the room has not warmed by at least 0.1°C.",
|
||||
"flow.simpleHeatOffTip": "How many uninterrupted minutes the room must remain at or above its daytime target before turning the unit OFF.",
|
||||
"zones.flowDayOff": "Flow · day · off",
|
||||
"zones.flowDayPendingOff": "Flow · day · waiting to switch off",
|
||||
"flow.node.thermostat": "Thermostat",
|
||||
"flow.node.greeDevice": "GREE device",
|
||||
"flow.node.greeFeature": "Single GREE function",
|
||||
|
||||
@@ -1036,6 +1036,37 @@
|
||||
"flow.node.and": "AND",
|
||||
"flow.node.or": "OR",
|
||||
"flow.node.not": "NOT",
|
||||
"flow.node.simpleHeat": "Proste grzanie dzień/noc",
|
||||
"flow.simpleHeatDay": "Cel dzień",
|
||||
"flow.simpleHeatNight": "Cel noc",
|
||||
"flow.simpleHeatMinimum": "Minimalna temperatura dzienna",
|
||||
"flow.simpleHeatBoostMinutes": "Wzmocnienie po (min)",
|
||||
"flow.simpleHeatOffMinutes": "Wyłącz w dzień po stabilizacji (min)",
|
||||
"flow.simpleHeatHint": "Połącz „Stała testowa: Tak” z tym blokiem. Godziny nocy ustawisz w „Trybie nocnym”; szczegóły działania są poniżej.",
|
||||
"flow.constantDescription": "Stały warunek TAK/NIE. Ustaw TAK, aby grzanie działało automatycznie w każdym cyklu Flow.",
|
||||
"flow.constantHelp": "TAK: podłączona akcja jest oceniana w każdym cyklu automatyki. NIE: akcja nie uruchamia się. Dzień, noc i temperaturę obsługuje blok grzania, więc nie trzeba dodawać innych warunków.",
|
||||
"flow.simpleHeatDescription": "Jeden blok steruje temperaturą pokoju w dzień i w nocy. Sam dobiera nastawę GREE, hamuje grzanie i wyłącza urządzenie w dzień.",
|
||||
"flow.simpleHeatHowItWorks": "Jak działa? Przykład dla tych nastaw",
|
||||
"flow.simpleHeatHelpIntro": "Cele oznaczają temperaturę pokoju, nie nastawę klimatyzatora. Blok wylicza całkowite stopnie zadane GREE z celu danej strefy.",
|
||||
"flow.simpleHeatHelpNightTitle": "Noc — cel w pokoju {target}; jednostka pozostaje ON",
|
||||
"flow.simpleHeatHelpNightSleep": "Od {room} wzwyż: GREE {gree} — uśpienie bez wyłączania jednostki.",
|
||||
"flow.simpleHeatHelpNightBrake": "Od {room} do poniżej celu: GREE {gree} — wyhamowanie.",
|
||||
"flow.simpleHeatHelpNightGentle": "Poniżej {room}: GREE {gree} — delikatne dogrzewanie (dopóki nie zajdzie warunek wzmocnienia).",
|
||||
"flow.simpleHeatHelpNightBoost": "Przy {room} lub mniej albo po {minutes} min bez wzrostu o 0,1°C: GREE {gree} — mocniejsze grzanie.",
|
||||
"flow.simpleHeatHelpDayTitle": "Dzień — cel {target}, minimalna temperatura {minimum}",
|
||||
"flow.simpleHeatHelpDayOff": "Jednostka OFF, dopóki temperatura nie spadnie poniżej {minimum}.",
|
||||
"flow.simpleHeatHelpDayHeat": "Po spadku: GREE {gentle}; przy {room} lub mniej albo po {minutes} min bez poprawy: GREE {boost}.",
|
||||
"flow.simpleHeatHelpDayStop": "Po utrzymaniu co najmniej {target} przez ciągłe {minutes} min: OFF. Spadek poniżej celu zeruje licznik.",
|
||||
"flow.simpleHeatHelpSensors": "Czujnik: temperatura z ustawień strefy (HA, GREE lub combined). Przy braku świeżego HA strefa może użyć GREE, o ile jednostka udostępnia odczyt. W combined duża rozbieżność może także wybrać GREE.",
|
||||
"flow.simpleHeatHelpSchedule": "Godziny dnia/nocy: Ustawienia → Tryb nocny. Każdy pokój ma własne cele w tym bloku.",
|
||||
"flow.simpleHeatHelpControl": "Sterowanie widoczne jako automatyczne Flow, nie ręczne. Rzeczywiste polecenia ręczne/pilot mają pierwszeństwo; zabezpieczenia sprężarki pozostają aktywne.",
|
||||
"flow.simpleHeatDayTip": "Temperatura docelowa w pokoju w ciągu dnia; nie jest bezpośrednią nastawą GREE.",
|
||||
"flow.simpleHeatNightTip": "Temperatura docelowa w pokoju w godzinach trybu nocnego.",
|
||||
"flow.simpleHeatMinimumTip": "Gdy jest cieplej, dzienna jednostka pozostaje OFF. Poniżej minimum rozpocznie dogrzewanie.",
|
||||
"flow.simpleHeatBoostTip": "Ile minut próbować delikatnego grzania, zanim przełączy na mocniejsze, jeśli pokój nie ogrzeje się o co najmniej 0,1°C.",
|
||||
"flow.simpleHeatOffTip": "Ile nieprzerwanych minut temperatura ma być na celu lub wyższa, zanim jednostka wyłączy się w dzień.",
|
||||
"zones.flowDayOff": "Flow · dzień · wyłączony",
|
||||
"zones.flowDayPendingOff": "Flow · dzień · oczekiwanie na wyłączenie",
|
||||
"flow.node.thermostat": "Termostat",
|
||||
"flow.node.greeDevice": "Urządzenie GREE",
|
||||
"flow.node.greeFeature": "Pojedyncza funkcja GREE",
|
||||
|
||||
@@ -18,3 +18,7 @@ All operator-facing scripts live in this directory.
|
||||
- `network-debug.sh <interface> [broadcast-ip]` — prints interface/routing diagnostics and a tcpdump command.
|
||||
|
||||
The running controller uses only the committed `web/styles.css` embedded into the Rust binary. There is no frontend dependency installation, CDN, or CSS build step.
|
||||
|
||||
- `verify_simple_heating.py` — static validation for the three simple-heating Flow imports, language files and release versions.
|
||||
- `test_simple_heating_ui.js` — checks automatic Flow vs manual thermostat statuses.
|
||||
- `test_flow_block_help_ui.js` — checks the localized two-block descriptions, dynamically calculated day/night heating examples and the Flow inspector help panel.
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.16.1.
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.16.2.
|
||||
|
||||
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
|
||||
@@ -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_TOKEN = os.environ.get("GREE_API_TOKEN", "")
|
||||
DEFAULT_EXPECTED_VERSION = "0.16.1"
|
||||
DEFAULT_EXPECTED_VERSION = "0.16.2"
|
||||
|
||||
SETTINGS_PATHS: dict[str, set[str]] = {
|
||||
"/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")
|
||||
|
||||
runner.run("public health", lambda: test_health(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.16.1 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.16.2 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client))
|
||||
runner.run("protected API requires auth", lambda: test_protected_auth(client))
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Verify contextual help for the constant and room heating Flow blocks.
|
||||
// Does not need a browser or a running HVAC controller.
|
||||
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 translations = {};
|
||||
for (const language of ['pl', 'en']) {
|
||||
translations[language] = JSON.parse(fs.readFileSync(path.join(root, 'lang', `${language}.json`), 'utf8')).translations;
|
||||
}
|
||||
function localize(language, key, params = {}) {
|
||||
const result = translations[language][key];
|
||||
assert.ok(result, `missing translation ${language}:${key}`);
|
||||
return String(result).replace(/\{([a-zA-Z0-9_]+)\}/g, (_, name) => params[name] ?? `{${name}}`);
|
||||
}
|
||||
const app = {
|
||||
zones: [{ id: 'zone1', name: 'Pokój' }],
|
||||
devices: [], groups: [], flowDraft: null, flowSelectedNodeId: null,
|
||||
};
|
||||
const host = {
|
||||
innerHTML: '',
|
||||
expandedNode: null,
|
||||
classList: { toggle() {}, remove() {} },
|
||||
querySelector(selector) {
|
||||
return selector === '.flow-block-help' && this.expandedNode
|
||||
? { open: true, dataset: { flowHelpNode: this.expandedNode } } : null;
|
||||
},
|
||||
};
|
||||
const ctx = vm.createContext({
|
||||
app,
|
||||
tr: (key, params) => localize(ctx.language, key, params),
|
||||
language: 'pl',
|
||||
esc: value => String(value).replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"'),
|
||||
fmtTemp: value => `${Number(value).toFixed(1)}°C`,
|
||||
uiIcon: () => '',
|
||||
$: selector => selector === '#flowInspector' ? host : null,
|
||||
document: { addEventListener() {} }, window: { addEventListener() {} },
|
||||
});
|
||||
vm.runInContext(fs.readFileSync(path.join(root, 'web/js-dynamic/flows.js'), 'utf8'), ctx);
|
||||
const meta = vm.runInContext('FLOW_NODE_META', ctx);
|
||||
assert.equal(meta.constant.descriptionKey, 'flow.constantDescription');
|
||||
assert.equal(meta.zone_heat_cycle.descriptionKey, 'flow.simpleHeatDescription');
|
||||
|
||||
for (const language of ['pl', 'en']) {
|
||||
ctx.language = language;
|
||||
for (const file of fs.readdirSync(path.join(root, 'flows/simple-heating'))) {
|
||||
if (!file.endsWith('.flow.json')) continue;
|
||||
const json = JSON.parse(fs.readFileSync(path.join(root, 'flows/simple-heating', file), 'utf8'));
|
||||
const setting = json.flow.nodes.find(node => node.kind === 'zone_heat_cycle').config;
|
||||
const text = ctx.flowSimpleHeatHelp(setting, 'room', false);
|
||||
const night = setting.night_target;
|
||||
assert.ok(text.includes(`${night.toFixed(1)}°C`), `${language} ${file} missing room target`);
|
||||
assert.ok(text.includes(`${(night - 0.8).toFixed(1)}°C`), `${language} ${file} missing boost threshold`);
|
||||
assert.ok(text.includes(`${Math.floor(night - 1).toFixed(1)}°C`), `${language} ${file} missing standby`);
|
||||
assert.ok(text.includes(`${Math.floor(night) + 1}.0°C`), `${language} ${file} missing gentle heat`);
|
||||
assert.ok(text.includes(`${Math.ceil(night + 2)}.0°C`), `${language} ${file} missing boost`);
|
||||
assert.ok(text.includes(`${setting.day_min.toFixed(1)}°C`), `${language} ${file} missing daytime minimum`);
|
||||
assert.ok(text.includes('21.0°C') && text.includes('22.0°C'), `${language} ${file} missing daytime setpoints`);
|
||||
assert.ok(text.includes('19.2°C'), `${language} ${file} missing daytime immediate boost threshold`);
|
||||
assert.ok(text.includes(String(setting.day_off_after_minutes)), `${language} ${file} missing off timer`);
|
||||
assert.ok(text.includes(String(setting.boost_after_minutes)), `${language} ${file} missing boost timer`);
|
||||
assert.ok(text.includes(localize(language, 'flow.simpleHeatHelpSensors')));
|
||||
}
|
||||
assert.ok(ctx.flowSimpleHeatHelp({ night_target: 22.4, day_target: 21.3, day_min: 20.6, boost_after_minutes: 12, day_off_after_minutes: 45 }).includes('25.0°C'), 'help must recalculate for edited targets');
|
||||
}
|
||||
ctx.language = 'pl';
|
||||
function inspect(kind, config) {
|
||||
app.flowDraft = { nodes: [{ id: 'selected', kind, config }], edges: [] };
|
||||
app.flowSelectedNodeId = 'selected';
|
||||
ctx.renderFlowInspector();
|
||||
return host.innerHTML;
|
||||
}
|
||||
const constantHtml = inspect('constant', { value: true });
|
||||
assert.ok(constantHtml.includes(localize('pl', 'flow.constantDescription')));
|
||||
assert.ok(constantHtml.includes(localize('pl', 'flow.constantHelp')));
|
||||
assert.ok(constantHtml.includes('data-flow-config="value"'));
|
||||
const heat = { zone_id: 'zone1', day_target: 20, night_target: 20.6, day_min: 19.7, boost_after_minutes: 10, day_off_after_minutes: 30 };
|
||||
const simpleHtml = inspect('zone_heat_cycle', heat);
|
||||
assert.ok(simpleHtml.includes(localize('pl', 'flow.simpleHeatDescription')));
|
||||
assert.ok(simpleHtml.includes('<details class="flow-block-help"'));
|
||||
assert.ok(simpleHtml.includes(localize('pl', 'flow.simpleHeatHowItWorks')));
|
||||
assert.ok(simpleHtml.includes('19.0°C') && simpleHtml.includes('23.0°C'));
|
||||
assert.ok(simpleHtml.includes('data-flow-config="night_target"'));
|
||||
host.expandedNode = 'selected';
|
||||
assert.ok(inspect('zone_heat_cycle', heat).includes('data-flow-help-node="selected" open>'), 'expanded help should stay open when a user edits settings');
|
||||
const htmlFile = fs.readFileSync(path.join(root, 'web/index.html'), 'utf8');
|
||||
assert.ok(htmlFile.includes('data-i18n-title="flow.constantDescription"'));
|
||||
assert.ok(htmlFile.includes('data-i18n-title="flow.simpleHeatDescription"'));
|
||||
console.log('OK: 2 blocks, 3 rooms, PL/EN tips, dynamic temperature examples, inspector and expanded-state behavior');
|
||||
if (process.env.FLOW_HELP_PREVIEW_HTML) {
|
||||
host.expandedNode = 'selected';
|
||||
const content = inspect('zone_heat_cycle', heat);
|
||||
fs.writeFileSync(process.env.FLOW_HELP_PREVIEW_HTML, `<!doctype html><html lang="pl"><head><meta charset="utf-8"><link rel="stylesheet" href="file://${path.join(root, 'web/css/styles.css')}"><style>body{margin:0;padding:28px;background:var(--bg)} .flow-inspector{width:370px;max-height:none;box-sizing:border-box;border:1px solid var(--line-strong);border-radius:14px} .flow-inspector-head button{font-size:11px;padding:7px}</style></head><body><aside class="flow-inspector">${content}</aside></body></html>`);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Focused UI behavior checks, with no browser or controller required.
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
const ctx = vm.createContext({
|
||||
tr: key => key,
|
||||
app: { zones: [], devices: [], groups: [] },
|
||||
});
|
||||
vm.runInContext(fs.readFileSync('web/js-dynamic/entities.js', 'utf8'), ctx);
|
||||
const zone = {
|
||||
enabled: true,
|
||||
control_owner: 'automation',
|
||||
control_source: 'automation.flow',
|
||||
simple_heat: { day_target: 20, night_target: 20.6 },
|
||||
current_temperature: 20.2,
|
||||
demand: false,
|
||||
};
|
||||
function check(fn, expected, ...args) {
|
||||
const value = ctx[fn](...args);
|
||||
if (value !== expected) throw new Error(`${fn}: ${value} != ${expected}`);
|
||||
}
|
||||
check('zoneControlOwnerLabel', 'zones.ownerFlow', zone);
|
||||
check('zoneRuntimeStatusLabel', 'zones.flowDayOff', zone, 'off', { power: false });
|
||||
check('zoneRuntimeStatusLabel', 'zones.flowDayPendingOff', zone, 'off', { power: true });
|
||||
check('zoneRuntimeStatusLabel', 'zones.flowStandby', zone, 'heat', { power: true, enabled: true, online: true });
|
||||
check('zoneRuntimeStatusLabel', 'zones.flowHeating', { ...zone, demand: true }, 'heat', { power: true, enabled: true, online: true, mode: 'heat' });
|
||||
check('zoneRuntimeStatusLabel', 'zones.flowCoasting', { ...zone, heat_coasting: true }, 'heat', { power: true, enabled: true, online: true });
|
||||
check('zoneRuntimeStatusLabel', 'zones.manualDeviceControl', { ...zone, device_manual_override: true }, 'heat', { power: true });
|
||||
check('zoneControlOwnerLabel', 'zones.ownerExternal', { ...zone, control_owner: 'direct_manual', control_source: 'external' });
|
||||
console.log('OK: 8 UI Flow/manual status scenarios');
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Static release checks for the two-node Simple day/night heating Flows.
|
||||
|
||||
Does not replace `cargo test` or hardware verification.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PROFILE_DIR = ROOT / "flows" / "simple-heating"
|
||||
ROOMS = {
|
||||
"sypialnia": ("9a0bb37d-0306-4f87-9b14-fabf3b4c6164", 20.5, 10),
|
||||
"jan-pieczara": ("7feae66a-747e-4205-8cff-91cc0b7924f6", 20.8, 8),
|
||||
"igor": ("dd451f3e-4fc0-4189-b1cf-0c814b65cefc", 20.8, 10),
|
||||
}
|
||||
TRANSLATIONS = [
|
||||
"flow.node.simpleHeat", "flow.simpleHeatDay", "flow.simpleHeatNight",
|
||||
"flow.simpleHeatMinimum", "flow.simpleHeatBoostMinutes",
|
||||
"flow.simpleHeatOffMinutes", "flow.simpleHeatHint", "zones.flowDayOff", "zones.flowDayPendingOff",
|
||||
"flow.constantDescription", "flow.constantHelp", "flow.simpleHeatDescription", "flow.simpleHeatHowItWorks",
|
||||
"flow.simpleHeatHelpIntro", "flow.simpleHeatHelpNightTitle", "flow.simpleHeatHelpNightSleep",
|
||||
"flow.simpleHeatHelpNightBrake", "flow.simpleHeatHelpNightGentle", "flow.simpleHeatHelpNightBoost",
|
||||
"flow.simpleHeatHelpDayTitle", "flow.simpleHeatHelpDayOff", "flow.simpleHeatHelpDayHeat",
|
||||
"flow.simpleHeatHelpDayStop", "flow.simpleHeatHelpSensors", "flow.simpleHeatHelpSchedule",
|
||||
"flow.simpleHeatHelpControl", "flow.simpleHeatDayTip", "flow.simpleHeatNightTip",
|
||||
"flow.simpleHeatMinimumTip", "flow.simpleHeatBoostTip", "flow.simpleHeatOffTip",
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
version = tomllib.loads((ROOT / "Cargo.toml").read_text())["package"]["version"]
|
||||
for name, (zone_id, target, boost) in ROOMS.items():
|
||||
path = PROFILE_DIR / f"{name}-simple-heat-v5.flow.json"
|
||||
doc = json.loads(path.read_text())
|
||||
assert doc["format"] == "gree-controller-flow" and doc["version"] == 1, name
|
||||
graph = doc["flow"]
|
||||
assert graph["enabled"] and not graph["draft"], name
|
||||
assert len(graph["nodes"]) == 2 and len(graph["edges"]) == 1, name
|
||||
conditions = [node for node in graph["nodes"] if node["kind"] == "constant"]
|
||||
actions = [node for node in graph["nodes"] if node["kind"] == "zone_heat_cycle"]
|
||||
assert len(conditions) == len(actions) == 1, name
|
||||
assert conditions[0]["config"]["value"] is True, name
|
||||
assert graph["edges"][0]["from"] == conditions[0]["id"], name
|
||||
assert graph["edges"][0]["to"] == actions[0]["id"], name
|
||||
conf = actions[0]["config"]
|
||||
assert conf["zone_id"] == zone_id, name
|
||||
assert conf["night_target"] == target, name
|
||||
assert conf["day_target"] == 20 and conf["day_min"] == 19.7, name
|
||||
assert conf["boost_after_minutes"] == boost, name
|
||||
assert conf["day_off_after_minutes"] == 30, name
|
||||
assert "Wymaga wersji aplikacji 0.16.2" in graph["description"], name
|
||||
|
||||
for lang in ("pl", "en"):
|
||||
translations = json.loads((ROOT / "lang" / f"{lang}.json").read_text())["translations"]
|
||||
assert all(translations.get(key) for key in TRANSLATIONS), lang
|
||||
|
||||
# Validate every distributable JSON input, not only the three new Flow exports.
|
||||
json_files = list(ROOT.rglob("*.json"))
|
||||
for path in json_files:
|
||||
json.loads(path.read_text())
|
||||
assert len(json_files) > 30, len(json_files)
|
||||
|
||||
js_files = list((ROOT / "web").rglob("*.js"))
|
||||
for path in js_files:
|
||||
subprocess.run(["node", "--check", str(path)], check=True, capture_output=True, text=True)
|
||||
|
||||
rust = (ROOT / "src" / "api" / "flows.rs").read_text()
|
||||
engine = (ROOT / "src" / "engine" / "zone_control.rs").read_text()
|
||||
auto = (ROOT / "src" / "engine" / "automations.rs").read_text()
|
||||
status = (ROOT / "web" / "js-dynamic" / "entities.js").read_text()
|
||||
ui = (ROOT / "web" / "js-dynamic" / "flows.js").read_text()
|
||||
html = (ROOT / "web" / "index.html").read_text()
|
||||
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 "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()
|
||||
|
||||
lock = (ROOT / "Cargo.lock").read_text()
|
||||
assert f'name = "gree-controller"\nversion = "{version}"' in lock
|
||||
for manifest in [
|
||||
ROOT / "ha-addon" / "repository" / "gree-controller" / "config.yaml",
|
||||
ROOT / "ha-addon" / "home-assistant" / "custom_components" / "gree_controller" / "manifest.json",
|
||||
]:
|
||||
assert version in manifest.read_text(), manifest
|
||||
assert json.loads((ROOT / "docs" / "openapi.json").read_text())["info"]["version"] == version
|
||||
|
||||
print(f"OK: {len(ROOMS)} Flow imports, {len(json_files)} JSON files, {len(js_files)} JS scripts, translations, linked controller source, version {version}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except (AssertionError, subprocess.CalledProcessError) as exc:
|
||||
print(f"FAIL: {exc}", file=sys.stderr)
|
||||
raise
|
||||
+1
-1
@@ -12,7 +12,7 @@ use crate::{
|
||||
InfluxDbSettingsUpdate, InfluxDbSettingsView, LocalDiscoveryCandidate, ManualDeviceRequest, NetworkReading,
|
||||
NightModeSettings, NotificationSettings, NotificationSettingsUpdate,
|
||||
NotificationSettingsView, Reading, RuntimeSettings, Schedule, SettingsSnapshot,
|
||||
TemporaryQuickThermostat, TemporaryQuickThermostatRequest, Zone, ZoneControlPatch,
|
||||
SimpleHeatRuntime, TemporaryQuickThermostat, TemporaryQuickThermostatRequest, Zone, ZoneControlPatch,
|
||||
ZoneReading,
|
||||
},
|
||||
notifications,
|
||||
|
||||
@@ -164,6 +164,7 @@ impl AutomationInput {
|
||||
last_trigger_trace: vec![],
|
||||
action_zone_id: None,
|
||||
action_zone_preset: None,
|
||||
action_zone_simple_heat: None,
|
||||
action_zone_heat_anticipation_c: None,
|
||||
action_zone_standby_offset_c: None,
|
||||
action_ha_domain: None,
|
||||
|
||||
+35
-1
@@ -269,6 +269,7 @@ fn flow_action_kind(kind: &str) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
"zone_thermostat"
|
||||
| "zone_heat_cycle"
|
||||
| "device_action"
|
||||
| "device_feature_action"
|
||||
| "group_action"
|
||||
@@ -1563,6 +1564,7 @@ fn compile_flow(
|
||||
last_trigger_trace,
|
||||
action_zone_id: None,
|
||||
action_zone_preset: None,
|
||||
action_zone_simple_heat: None,
|
||||
action_zone_heat_anticipation_c: None,
|
||||
action_zone_standby_offset_c: None,
|
||||
action_ha_domain: None,
|
||||
@@ -1586,6 +1588,38 @@ fn compile_flow(
|
||||
item.cooldown_seconds = cooldown.max(30);
|
||||
}
|
||||
match action_node.kind.as_str() {
|
||||
"zone_heat_cycle" => {
|
||||
if value_binding.is_some() {
|
||||
return Err(AppError::BadRequest("simple heat does not accept a dynamic value connection".into()));
|
||||
}
|
||||
let zone_id = flow_string(&action_node.config, "zone_id")
|
||||
.ok_or_else(|| AppError::BadRequest("simple heat needs a zone".into()))?;
|
||||
if !zones.iter().any(|zone| zone.id == zone_id) {
|
||||
return Err(AppError::BadRequest("simple heat references a missing zone".into()));
|
||||
}
|
||||
let day_target = flow_f64(&action_node.config, "day_target")
|
||||
.ok_or_else(|| AppError::BadRequest("simple heat requires a day target".into()))?;
|
||||
let night_target = flow_f64(&action_node.config, "night_target")
|
||||
.ok_or_else(|| AppError::BadRequest("simple heat requires a night target".into()))?;
|
||||
let day_min = flow_f64(&action_node.config, "day_min")
|
||||
.ok_or_else(|| AppError::BadRequest("simple heat requires a daytime minimum".into()))?;
|
||||
let boost_after_minutes = action_node.config.get("boost_after_minutes")
|
||||
.and_then(Value::as_u64).unwrap_or(10);
|
||||
let day_off_after_minutes = action_node.config.get("day_off_after_minutes")
|
||||
.and_then(Value::as_u64).unwrap_or(30);
|
||||
if ![day_target, night_target, day_min].iter().all(|v| v.is_finite() && (17.0..=28.0).contains(v))
|
||||
|| day_min >= day_target || !(3..=30).contains(&boost_after_minutes)
|
||||
|| !(10..=120).contains(&day_off_after_minutes) {
|
||||
return Err(AppError::BadRequest("invalid simple heat targets or timing (day minimum must be below day target)".into()));
|
||||
}
|
||||
item.action_zone_id = Some(zone_id);
|
||||
item.action_zone_simple_heat = Some(crate::models::SimpleHeatSettings {
|
||||
flow_id: flow.id.clone(), flow_node_id: action_node.id.clone(),
|
||||
day_target, night_target, day_min, boost_after_minutes,
|
||||
day_off_after_minutes,
|
||||
});
|
||||
item.action.mode = Some("heat".into());
|
||||
}
|
||||
"zone_thermostat" => {
|
||||
let zone_id = flow_string(&action_node.config, "zone_id")
|
||||
.ok_or_else(|| AppError::BadRequest("thermostat block needs a zone".into()))?;
|
||||
@@ -2130,7 +2164,7 @@ fn dry_run_block_reason(
|
||||
value_binding: Option<&(String, String)>,
|
||||
) -> Result<Option<String>, AppError> {
|
||||
match action.kind.as_str() {
|
||||
"zone_thermostat" => {
|
||||
"zone_thermostat" | "zone_heat_cycle" => {
|
||||
let Some(zone_id) = flow_string(&action.config, "zone_id") else {
|
||||
return Ok(Some("missing_zone".into()));
|
||||
};
|
||||
|
||||
@@ -214,6 +214,8 @@ impl ZoneInput {
|
||||
min_on_seconds: self.min_on_seconds,
|
||||
min_off_seconds: self.min_off_seconds,
|
||||
min_adjust_seconds: self.min_adjust_seconds,
|
||||
simple_heat: None,
|
||||
simple_heat_runtime: SimpleHeatRuntime::default(),
|
||||
heat_anticipation_c: self.heat_anticipation_c,
|
||||
heat_coasting: false,
|
||||
standby_offset_c: self.standby_offset_c,
|
||||
@@ -382,6 +384,8 @@ async fn update_zone(
|
||||
zone.current_temperature = existing.current_temperature;
|
||||
zone.control_temperature_source = existing.control_temperature_source;
|
||||
zone.active_preset = existing.active_preset;
|
||||
zone.simple_heat = existing.simple_heat.clone();
|
||||
zone.simple_heat_runtime = existing.simple_heat_runtime.clone();
|
||||
zone.manual_preset = existing.manual_preset;
|
||||
zone.manual_setpoint = existing.manual_setpoint;
|
||||
zone.manual_override_until = existing.manual_override_until;
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@ use crate::{
|
||||
ControlPlan, ControlPlanEvent, Device, DeviceCommand, DeviceGroup, EnergyReading,
|
||||
HORIZONTAL_SWING_MAX, VERTICAL_SWING_MAX,
|
||||
EnergySourcePreference, GroupControlPatch, HaReading, NetworkReading, NightModeSettings,
|
||||
Reading, RuntimeSettings, Schedule, TemporaryQuickThermostat, Zone, ZoneControlPlan,
|
||||
Reading, RuntimeSettings, Schedule, SimpleHeatRuntime, SimpleHeatSettings, TemporaryQuickThermostat, Zone, ZoneControlPlan,
|
||||
ZoneReading,
|
||||
},
|
||||
state::{AppState, ControlPlanSnapshot, PendingControllerCommand},
|
||||
@@ -31,6 +31,7 @@ include!("engine/deadlines.rs");
|
||||
include!("engine/ownership.rs");
|
||||
include!("engine/groups.rs");
|
||||
include!("engine/zone_actions.rs");
|
||||
include!("engine/simple_heat.rs");
|
||||
include!("engine/zone_control.rs");
|
||||
include!("engine/history.rs");
|
||||
include!("engine/connectivity.rs");
|
||||
|
||||
@@ -33,7 +33,12 @@ fn flow_zone_action_already_satisfied(
|
||||
action: &DeviceCommand,
|
||||
heat_anticipation_c: Option<f64>,
|
||||
standby_offset_c: Option<f64>,
|
||||
simple_heat: Option<&SimpleHeatSettings>,
|
||||
) -> bool {
|
||||
if let Some(profile) = simple_heat {
|
||||
return zone.simple_heat.as_ref() == Some(profile)
|
||||
&& zone.control_source == "automation.flow";
|
||||
}
|
||||
if heat_anticipation_c.is_some_and(|value| (zone.heat_anticipation_c - value).abs() > 0.001)
|
||||
|| standby_offset_c.is_some_and(|value| (zone.standby_offset_c - value).abs() > 0.001)
|
||||
{
|
||||
@@ -202,6 +207,7 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
&effective_action,
|
||||
item.action_zone_heat_anticipation_c,
|
||||
item.action_zone_standby_offset_c,
|
||||
item.action_zone_simple_heat.as_ref(),
|
||||
) {
|
||||
let before_runtime = item.flow_runtime.clone();
|
||||
flow_acknowledge_change_gates(&mut item);
|
||||
@@ -289,6 +295,7 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
apply_flow_zone_action(
|
||||
state, zone_id, item.action_zone_preset.as_deref(), &effective_action,
|
||||
item.action_zone_heat_anticipation_c, item.action_zone_standby_offset_c,
|
||||
item.action_zone_simple_heat.as_ref(),
|
||||
).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() });
|
||||
@@ -1120,6 +1127,7 @@ fn flow_condition_kind_runtime(kind: &str) -> bool {
|
||||
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>,
|
||||
) -> Result<bool, AppError> {
|
||||
let _schedule_guard = state.lock_schedule_operation().await;
|
||||
// Match the canonical thermostat ordering used by Web/HA zone control. This prevents
|
||||
@@ -1131,13 +1139,29 @@ async fn apply_flow_zone_action(
|
||||
// A disabled Zone is an administrative gate for every Flow thermostat action.
|
||||
// Flow power commands control only the assigned unit runtime; they never change zone.enabled.
|
||||
if !zone.enabled { return Ok(false); }
|
||||
// Explicit manual target wins until Auto is selected again. A previous Flow v4
|
||||
// may have left a custom preset behind: that is *automation*, not manual,
|
||||
// and must be replaceable by the simplified Flow without user intervention.
|
||||
if simple_heat.is_some() && zone.control_source != "automation.flow"
|
||||
&& (zone.manual_preset.is_some() || zone.manual_setpoint.is_some()) {
|
||||
return Ok(false);
|
||||
}
|
||||
let device_id = zone.device_id.clone();
|
||||
if let Some(profile) = simple_heat {
|
||||
if zone.simple_heat.as_ref() != Some(profile) {
|
||||
zone.simple_heat_runtime = SimpleHeatRuntime::default();
|
||||
}
|
||||
zone.simple_heat = Some(profile.clone());
|
||||
} else {
|
||||
zone.simple_heat = None;
|
||||
zone.simple_heat_runtime = SimpleHeatRuntime::default();
|
||||
}
|
||||
if let Some(value) = heat_anticipation_c { zone.heat_anticipation_c = value; }
|
||||
if let Some(value) = standby_offset_c { zone.standby_offset_c = value; }
|
||||
if let Some(mode) = action.mode.as_deref() {
|
||||
match mode { "auto" => zone.inherit_house_mode = true, "heat" | "cool" => { zone.mode = mode.to_string(); zone.inherit_house_mode = false; }, _ => return Err(AppError::BadRequest("unsupported Flow thermostat mode".into())) }
|
||||
}
|
||||
match preset.unwrap_or("auto") {
|
||||
match if simple_heat.is_some() { "auto" } else { preset.unwrap_or("auto") } {
|
||||
"auto" => { zone.manual_preset = None; zone.manual_setpoint = None; zone.manual_override_until = None; }
|
||||
"custom" => {
|
||||
let target = action.target_temperature.ok_or_else(|| AppError::BadRequest("Flow custom thermostat action has no target".into()))?;
|
||||
@@ -1163,7 +1187,17 @@ async fn apply_flow_zone_action(
|
||||
zone.control_source = "automation.flow".into();
|
||||
zone.control_reason = "Visual Flow automation".into();
|
||||
let schedules = state.db.list_schedules()?;
|
||||
let house_mode = state.settings.read().await.house_mode.clone();
|
||||
let runtime_settings = state.settings.read().await;
|
||||
let house_mode = runtime_settings.house_mode.clone();
|
||||
if simple_heat.is_some() {
|
||||
// Show the correct bedroom/child-room night target immediately after
|
||||
// Flow activation, not the old Comfort preset until the next zone tick.
|
||||
zone.simple_heat_runtime.night = Some(night_mode_active(
|
||||
&runtime_settings.night_mode,
|
||||
Local::now().time(),
|
||||
));
|
||||
}
|
||||
drop(runtime_settings);
|
||||
refresh_control_ownership(&mut zone);
|
||||
if zone.control_owner == "automation" {
|
||||
zone.control_source = "automation.flow".into();
|
||||
|
||||
@@ -151,16 +151,25 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
"off"
|
||||
}
|
||||
});
|
||||
let simple_heat_flow = zone.simple_heat.as_ref().filter(|_| {
|
||||
zone.control_owner == "automation" && zone.control_source == "automation.flow"
|
||||
&& zone.manual_preset.is_none() && zone.manual_setpoint.is_none()
|
||||
});
|
||||
let simple_day_off = simple_heat_flow.is_some() && !night_active
|
||||
&& zone.effective_mode == "off" && !zone.demand;
|
||||
let effective_mode = if zone.device_manual_override {
|
||||
manual_device_mode.unwrap_or(configured_effective_mode)
|
||||
} else if automatic_active {
|
||||
configured_effective_mode
|
||||
} else {
|
||||
} else if simple_day_off || !automatic_active {
|
||||
"off"
|
||||
} else {
|
||||
configured_effective_mode
|
||||
};
|
||||
|
||||
let (resolved_preset, resolved_target) =
|
||||
resolve_zone_target(&zone, active_for_target, target_mode);
|
||||
let (resolved_preset, resolved_target) = if let Some(profile) = simple_heat_flow {
|
||||
("auto".into(), if night_active { profile.night_target } else { profile.day_target })
|
||||
} else {
|
||||
resolve_zone_target(&zone, active_for_target, target_mode)
|
||||
};
|
||||
let active = if automatic_active {
|
||||
active_for_target
|
||||
} else {
|
||||
@@ -213,7 +222,7 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
device_setpoint: device
|
||||
.filter(|item| item.power)
|
||||
.map(|item| item.target_temperature),
|
||||
desired_power: automatic_active,
|
||||
desired_power: automatic_active && !simple_day_off,
|
||||
desired_mode: effective_mode.to_string(),
|
||||
actual_power: device.map(|item| item.power),
|
||||
actual_mode: device.map(|item| {
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/// Two-block Flow heating: one always-true condition and one Day/Night thermostat action.
|
||||
/// Decision-making lives here rather than in dozens of visual Flow branches.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
struct SimpleHeatDecision {
|
||||
power: bool,
|
||||
device_target: f64,
|
||||
demand: bool,
|
||||
coasting: bool,
|
||||
}
|
||||
|
||||
fn simple_heat_step(
|
||||
profile: &SimpleHeatSettings,
|
||||
runtime: &mut SimpleHeatRuntime,
|
||||
room: f64,
|
||||
night: bool,
|
||||
now: DateTime<Utc>,
|
||||
) -> SimpleHeatDecision {
|
||||
if runtime.night != Some(night) {
|
||||
runtime.night = Some(night);
|
||||
runtime.day_heating = false;
|
||||
runtime.day_stable_since = None;
|
||||
runtime.gentle_since = None;
|
||||
runtime.gentle_start_temp = None;
|
||||
}
|
||||
let target = if night { profile.night_target } else { profile.day_target };
|
||||
let off = SimpleHeatDecision {
|
||||
power: false,
|
||||
device_target: 0.0,
|
||||
demand: false,
|
||||
coasting: false,
|
||||
};
|
||||
|
||||
// Day: stay physically OFF until the permitted minimum is breached. After a
|
||||
// daytime rescue, require 30 consecutive minutes at target before switching OFF.
|
||||
if !night {
|
||||
if !runtime.day_heating {
|
||||
if room >= profile.day_min {
|
||||
return off;
|
||||
}
|
||||
runtime.day_heating = true;
|
||||
}
|
||||
if room >= target {
|
||||
let since = runtime.day_stable_since.get_or_insert(now);
|
||||
if now.signed_duration_since(*since).num_seconds()
|
||||
>= (profile.day_off_after_minutes as i64) * 60
|
||||
{
|
||||
runtime.day_heating = false;
|
||||
runtime.day_stable_since = None;
|
||||
runtime.gentle_since = None;
|
||||
runtime.gentle_start_temp = None;
|
||||
return off;
|
||||
}
|
||||
} else {
|
||||
runtime.day_stable_since = None;
|
||||
}
|
||||
}
|
||||
|
||||
// Full comfort: 19 C at 20.5/20.6 C room target. Early braking begins
|
||||
// 0.1 C before the room target; no repeated GREE power cycling at night.
|
||||
if room >= target {
|
||||
runtime.gentle_since = None;
|
||||
runtime.gentle_start_temp = None;
|
||||
return SimpleHeatDecision {
|
||||
power: true,
|
||||
device_target: (target - 1.0).floor().clamp(16.0, 30.0),
|
||||
demand: false,
|
||||
coasting: false,
|
||||
};
|
||||
}
|
||||
if room + 0.00001 >= target - 0.1 {
|
||||
runtime.gentle_since = None;
|
||||
runtime.gentle_start_temp = None;
|
||||
return SimpleHeatDecision {
|
||||
power: true,
|
||||
device_target: (target - 0.5).floor().clamp(16.0, 30.0),
|
||||
demand: false,
|
||||
coasting: true,
|
||||
};
|
||||
}
|
||||
|
||||
// At the hard lower point (target - 0.8 C), boost immediately by 2 C.
|
||||
// Above that, start mildly at floor(target)+1, typically 21 C; escalate to
|
||||
// ceil(target+2), typically 23 C, if the room does not rise by 0.1 C
|
||||
// during the configured trial period.
|
||||
let severe = room <= target - 0.8;
|
||||
let boost = if severe {
|
||||
runtime.gentle_since = None;
|
||||
runtime.gentle_start_temp = None;
|
||||
true
|
||||
} else {
|
||||
if runtime.gentle_start_temp.is_none()
|
||||
|| runtime
|
||||
.gentle_start_temp
|
||||
.is_some_and(|previous| room >= previous + 0.1 - 0.00001)
|
||||
{
|
||||
runtime.gentle_since = Some(now);
|
||||
runtime.gentle_start_temp = Some(room);
|
||||
}
|
||||
runtime.gentle_since.is_some_and(|since| {
|
||||
now.signed_duration_since(since).num_seconds()
|
||||
>= (profile.boost_after_minutes as i64) * 60
|
||||
})
|
||||
};
|
||||
SimpleHeatDecision {
|
||||
power: true,
|
||||
device_target: (if boost { (target + 2.0).ceil() } else { target.floor() + 1.0 })
|
||||
.clamp(16.0, 30.0),
|
||||
demand: true,
|
||||
coasting: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod simple_heat_tests {
|
||||
use super::*;
|
||||
fn profile() -> SimpleHeatSettings {
|
||||
SimpleHeatSettings {
|
||||
flow_id: "flow".into(), flow_node_id: "action".into(),
|
||||
day_target: 20.0, night_target: 20.6, day_min: 19.7,
|
||||
boost_after_minutes: 10, day_off_after_minutes: 30,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn night_soft_brake_and_sleep() {
|
||||
let mut runtime = SimpleHeatRuntime::default();
|
||||
let now = Utc::now();
|
||||
let p = profile();
|
||||
assert_eq!(simple_heat_step(&p, &mut runtime, 19.7, true, now).device_target, 23.0);
|
||||
assert_eq!(simple_heat_step(&p, &mut runtime, 20.2, true, now).device_target, 21.0);
|
||||
assert_eq!(simple_heat_step(&p, &mut runtime, 20.5, true, now).device_target, 20.0);
|
||||
let standby = simple_heat_step(&p, &mut runtime, 20.6, true, now);
|
||||
assert_eq!(standby.device_target, 19.0);
|
||||
assert!(standby.power);
|
||||
assert!(!standby.demand);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boost_after_10_minutes_without_room_rise() {
|
||||
let mut runtime = SimpleHeatRuntime::default();
|
||||
let now = Utc::now();
|
||||
let p = profile();
|
||||
assert_eq!(simple_heat_step(&p, &mut runtime, 20.2, true, now).device_target, 21.0);
|
||||
assert_eq!(simple_heat_step(&p, &mut runtime, 20.2, true, now + chrono::Duration::minutes(9)).device_target, 21.0);
|
||||
assert_eq!(simple_heat_step(&p, &mut runtime, 20.2, true, now + chrono::Duration::minutes(10)).device_target, 23.0);
|
||||
assert_eq!(simple_heat_step(&p, &mut runtime, 20.3, true, now + chrono::Duration::minutes(11)).device_target, 21.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn day_off_until_limit_and_after_30_stable_minutes() {
|
||||
let mut runtime = SimpleHeatRuntime::default();
|
||||
let now = Utc::now();
|
||||
let p = profile();
|
||||
assert!(!simple_heat_step(&p, &mut runtime, 19.8, false, now).power);
|
||||
assert!(simple_heat_step(&p, &mut runtime, 19.6, false, now).power);
|
||||
assert!(simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(2)).power);
|
||||
assert!(simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(31)).power);
|
||||
assert!(!simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(32)).power);
|
||||
assert!(!simple_heat_step(&p, &mut runtime, 19.8, false, now + chrono::Duration::minutes(33)).power);
|
||||
assert!(simple_heat_step(&p, &mut runtime, 19.6, false, now + chrono::Duration::minutes(34)).power);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falling_below_target_resets_day_stability() {
|
||||
let mut runtime = SimpleHeatRuntime::default();
|
||||
let now = Utc::now();
|
||||
let p = profile();
|
||||
simple_heat_step(&p, &mut runtime, 19.6, false, now);
|
||||
simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(1));
|
||||
simple_heat_step(&p, &mut runtime, 19.9, false, now + chrono::Duration::minutes(29));
|
||||
assert!(simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(30)).power);
|
||||
assert!(simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(59)).power);
|
||||
assert!(!simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(60)).power);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn day_target_20_starts_at_21_then_boosts_to_22() {
|
||||
let mut runtime = SimpleHeatRuntime::default();
|
||||
let now = Utc::now();
|
||||
let p = profile();
|
||||
assert_eq!(simple_heat_step(&p, &mut runtime, 19.6, false, now).device_target, 21.0);
|
||||
assert_eq!(simple_heat_step(&p, &mut runtime, 19.6, false, now + chrono::Duration::minutes(10)).device_target, 22.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switching_night_to_day_resets_day_heat() {
|
||||
let mut runtime = SimpleHeatRuntime::default();
|
||||
let now = Utc::now();
|
||||
let p = profile();
|
||||
assert!(simple_heat_step(&p, &mut runtime, 20.6, true, now).power);
|
||||
assert!(!simple_heat_step(&p, &mut runtime, 20.0, false, now).power);
|
||||
assert!(simple_heat_step(&p, &mut runtime, 19.6, false, now).power);
|
||||
assert!(simple_heat_step(&p, &mut runtime, 19.6, true, now).power);
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ fn zone_has_thermostat_intent_source(
|
||||
) -> bool {
|
||||
zone.local_thermostat_power == Some(true)
|
||||
|| temporary_quick_thermostat_is_active(zone, now)
|
||||
|| (zone.simple_heat.is_some() && zone.control_source == "automation.flow")
|
||||
|| zone.manual_preset.is_some()
|
||||
|| zone.manual_setpoint.is_some()
|
||||
|| zone.control_source.starts_with("group:")
|
||||
@@ -109,6 +110,23 @@ pub fn refresh_zone_runtime_target(zone: &mut Zone, schedules: &[Schedule], hous
|
||||
return;
|
||||
}
|
||||
|
||||
// The simplified Flow profile is an automatic thermostat target, never a
|
||||
// manual/custom preset. Refresh it immediately for the UI, even before
|
||||
// the next polling cycle; night/day is re-evaluated by the zone controller.
|
||||
if let Some(profile) = zone.simple_heat.as_ref().filter(|_| {
|
||||
zone.control_source == "automation.flow"
|
||||
&& zone.manual_preset.is_none() && zone.manual_setpoint.is_none()
|
||||
}) {
|
||||
zone.effective_mode = configured_mode;
|
||||
zone.active_preset = "auto".into();
|
||||
zone.effective_setpoint = Some(if zone.simple_heat_runtime.night.unwrap_or(false) {
|
||||
profile.night_target
|
||||
} else {
|
||||
profile.day_target
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the target while `target_mode` may still borrow `configured_mode`.
|
||||
// After this call the borrow ends, so `configured_mode` can be moved into the runtime state.
|
||||
let (preset, target) = resolve_zone_target(zone, schedule, target_mode);
|
||||
|
||||
@@ -566,6 +566,7 @@ mod tests {
|
||||
last_trigger_trace: vec![],
|
||||
action_zone_id: None,
|
||||
action_zone_preset: None,
|
||||
action_zone_simple_heat: None,
|
||||
action_zone_heat_anticipation_c: None,
|
||||
action_zone_standby_offset_c: None,
|
||||
action_ha_domain: None,
|
||||
@@ -610,6 +611,8 @@ mod tests {
|
||||
min_on_seconds: 180,
|
||||
min_off_seconds: 180,
|
||||
min_adjust_seconds: 120,
|
||||
simple_heat: None,
|
||||
simple_heat_runtime: SimpleHeatRuntime::default(),
|
||||
heat_anticipation_c: 0.0,
|
||||
heat_coasting: false,
|
||||
standby_offset_c: 2.0,
|
||||
|
||||
+79
-19
@@ -541,6 +541,17 @@ 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();
|
||||
// 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?;
|
||||
@@ -576,6 +587,16 @@ 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();
|
||||
}
|
||||
}
|
||||
let outdoor_temperature = zone_outdoor_sensor_results
|
||||
.remove(&zone.id)
|
||||
.flatten()
|
||||
@@ -784,7 +805,15 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (preset, target) = resolve_zone_target(&zone, active_schedule, effective_mode);
|
||||
let simple_profile = zone.simple_heat.clone().filter(|_| {
|
||||
zone.control_owner == "automation" && zone.control_source == "automation.flow"
|
||||
&& zone.manual_preset.is_none() && zone.manual_setpoint.is_none()
|
||||
});
|
||||
let (preset, target) = if let Some(profile) = simple_profile.as_ref() {
|
||||
("auto".into(), if night_active { profile.night_target } else { profile.day_target })
|
||||
} else {
|
||||
resolve_zone_target(&zone, active_schedule, effective_mode)
|
||||
};
|
||||
zone.active_preset = preset;
|
||||
zone.effective_setpoint = Some(target);
|
||||
|
||||
@@ -802,17 +831,53 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
continue;
|
||||
};
|
||||
|
||||
let simple_decision = simple_profile.as_ref().map(|profile| {
|
||||
simple_heat_step(profile, &mut zone.simple_heat_runtime, temp, night_active, Utc::now())
|
||||
});
|
||||
if simple_decision.is_some_and(|decision| !decision.power) {
|
||||
zone.heat_coasting = false;
|
||||
zone.demand = false;
|
||||
zone.demand_since = None;
|
||||
zone.effective_mode = "off".into();
|
||||
zone.device_setpoint = if device.power { Some(device.target_temperature) } else { None };
|
||||
clear_compressor_pending(&mut zone, true);
|
||||
if device.power && device.online && device.communication_failures == 0 {
|
||||
let protection = chrono::Duration::seconds(zone.min_on_seconds.max(settings.compressor_protection_seconds).max(30) as i64);
|
||||
let safe_to_switch_off = !settings.compressor_protection_enabled
|
||||
|| zone.last_power_change_at.is_none_or(|at| Utc::now().signed_duration_since(at) >= protection);
|
||||
if safe_to_switch_off {
|
||||
match send_zone_command_if_owned(state, &zone.id, &zone.device_id, DeviceCommand {
|
||||
power: Some(false), mode: Some("heat".into()), ..Default::default()
|
||||
}).await {
|
||||
Ok(Some(updated_device)) => {
|
||||
if !updated_device.power {
|
||||
zone.last_power_change_at = Some(Utc::now());
|
||||
zone.last_action_at = Some(Utc::now());
|
||||
zone.device_setpoint = None;
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) => state.log("warn", "zone.simple_heat_day_off_error", &err.to_string(),
|
||||
json!({"zone_id": zone.id})),
|
||||
}
|
||||
}
|
||||
}
|
||||
record_zone_history(state, &zone, outdoor_temperature, settings.poll_interval_seconds);
|
||||
publish_persisted_zone_cycle(state, persist_zone_cycle(state, &zone, cycle_started_at)?)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let half = zone.hysteresis_for_mode(effective_mode) / 2.0;
|
||||
let previous_demand = zone.demand;
|
||||
let previous_heat_coasting = zone.heat_coasting;
|
||||
zone.heat_coasting = heat_coasting_next_state(
|
||||
zone.heat_coasting,
|
||||
effective_mode,
|
||||
temp,
|
||||
target,
|
||||
zone.heat_anticipation_c,
|
||||
);
|
||||
zone.demand = match effective_mode {
|
||||
zone.heat_coasting = if let Some(decision) = simple_decision {
|
||||
decision.coasting
|
||||
} else {
|
||||
heat_coasting_next_state(zone.heat_coasting, effective_mode, temp, target, zone.heat_anticipation_c)
|
||||
};
|
||||
zone.demand = if let Some(decision) = simple_decision {
|
||||
decision.demand
|
||||
} else { match effective_mode {
|
||||
"heat" => {
|
||||
if temp <= target - half {
|
||||
true
|
||||
@@ -831,7 +896,7 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
zone.demand
|
||||
}
|
||||
}
|
||||
};
|
||||
}};
|
||||
if zone.demand && !previous_demand {
|
||||
zone.demand_since = Some(Utc::now());
|
||||
zone.target_alerted_at = None;
|
||||
@@ -874,15 +939,10 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
"heat" => target - zone.standby_offset_c.max(0.5),
|
||||
_ => target + zone.standby_offset_c.max(0.5),
|
||||
};
|
||||
let desired_device_target = round_device_setpoint(
|
||||
effective_mode,
|
||||
delivering_heat,
|
||||
if delivering_heat {
|
||||
active_target
|
||||
} else {
|
||||
standby_target
|
||||
},
|
||||
);
|
||||
let desired_device_target = simple_decision.map(|decision| decision.device_target).unwrap_or_else(|| {
|
||||
round_device_setpoint(effective_mode, delivering_heat,
|
||||
if delivering_heat { active_target } else { standby_target })
|
||||
});
|
||||
// Report only the last confirmed device state here. The desired target belongs to
|
||||
// effective_setpoint/command planning until a device command succeeds.
|
||||
zone.device_setpoint = if device.power {
|
||||
|
||||
@@ -83,6 +83,9 @@ pub struct Automation {
|
||||
pub action_zone_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub action_zone_preset: Option<String>,
|
||||
/// Optional single-block Flow heating policy.
|
||||
#[serde(default)]
|
||||
pub action_zone_simple_heat: Option<SimpleHeatSettings>,
|
||||
/// Optional soft-coast parameters set by a Flow thermostat action.
|
||||
#[serde(default)]
|
||||
pub action_zone_heat_anticipation_c: Option<f64>,
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
/// One simple Flow action owns a room's daytime/night-time heating policy.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SimpleHeatSettings {
|
||||
pub flow_id: String,
|
||||
pub flow_node_id: String,
|
||||
pub day_target: f64,
|
||||
pub night_target: f64,
|
||||
pub day_min: f64,
|
||||
pub boost_after_minutes: u64,
|
||||
pub day_off_after_minutes: u64,
|
||||
}
|
||||
|
||||
/// Persisted hysteresis/timers survive application restarts and sensor polling gaps.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub struct SimpleHeatRuntime {
|
||||
pub night: Option<bool>,
|
||||
pub day_heating: bool,
|
||||
pub day_stable_since: Option<DateTime<Utc>>,
|
||||
pub gentle_since: Option<DateTime<Utc>>,
|
||||
pub gentle_start_temp: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Zone {
|
||||
pub id: String,
|
||||
@@ -44,6 +66,11 @@ pub struct Zone {
|
||||
/// Minimum interval between automatic setpoint/fan adjustments.
|
||||
#[serde(default = "default_min_adjust")]
|
||||
pub min_adjust_seconds: u64,
|
||||
/// Active simple Day/Night heating Flow (not a manual custom preset).
|
||||
#[serde(default)]
|
||||
pub simple_heat: Option<SimpleHeatSettings>,
|
||||
#[serde(default)]
|
||||
pub simple_heat_runtime: SimpleHeatRuntime,
|
||||
/// Degrees before the comfort target at which GREE enters low-heat coasting.
|
||||
/// Zero disables predictive soft standby (the pre-upgrade behavior).
|
||||
#[serde(default)]
|
||||
|
||||
+69
-2
@@ -6608,6 +6608,59 @@ textarea[aria-invalid="true"] {
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
|
||||
.flow-inspector-description {
|
||||
margin: -7px 0 15px;
|
||||
color: var(--muted);
|
||||
font-size: .82rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.flow-block-help {
|
||||
margin: 16px 0 18px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
font-size: .81rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.flow-block-help summary {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.flow-block-help[open] summary {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.flow-block-help p {
|
||||
margin: 8px 0 10px;
|
||||
}
|
||||
|
||||
.flow-block-help strong {
|
||||
display: block;
|
||||
margin: 12px 0 5px;
|
||||
}
|
||||
|
||||
.flow-block-help ul {
|
||||
margin: 0 0 10px;
|
||||
padding-left: 19px;
|
||||
}
|
||||
|
||||
.flow-block-help li {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.flow-block-library-description {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.flow-inspector label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@@ -10632,6 +10685,7 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr
|
||||
min-height: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keep Flow interpretation compact on every viewport. Details are opt-in. */
|
||||
.flow-natural-preview {
|
||||
display: block;
|
||||
@@ -10765,14 +10819,17 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr
|
||||
transition: opacity .14s ease, transform .14s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.flow-editor-notice.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.flow-editor-notice.is-error {
|
||||
color: var(--danger);
|
||||
background: var(--danger-soft);
|
||||
}
|
||||
|
||||
.flow-context-menu {
|
||||
position: fixed;
|
||||
z-index: 1200;
|
||||
@@ -10784,7 +10841,11 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr
|
||||
background: var(--surface);
|
||||
box-shadow: 0 16px 38px rgba(0, 0, 0, .28);
|
||||
}
|
||||
.flow-context-menu[hidden] { display: none; }
|
||||
|
||||
.flow-context-menu[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flow-context-menu button {
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
@@ -10797,22 +10858,28 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr
|
||||
font: inherit;
|
||||
font-size: .82rem;
|
||||
}
|
||||
|
||||
.flow-context-menu button:hover:not(:disabled),
|
||||
.flow-context-menu button:focus-visible:not(:disabled) {
|
||||
background: var(--accent-soft);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.flow-context-menu button:disabled {
|
||||
opacity: .45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.flow-context-separator {
|
||||
height: 1px;
|
||||
margin: 5px 4px;
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.flow-editor-notice { max-width: 42vw; }
|
||||
.flow-editor-notice {
|
||||
max-width: 42vw;
|
||||
}
|
||||
}
|
||||
|
||||
/* Flow quick actions: description editing and simple simulator */
|
||||
|
||||
+111
-47
@@ -86,7 +86,8 @@
|
||||
data-action="house-power" data-value="true" data-i18n="house.powerOnAll">Turn all on</button><button
|
||||
id="housePowerOff" type="button" class="secondary" data-action="house-power" data-value="false"
|
||||
data-i18n="house.powerOffAll">Turn all off</button><button id="houseEmergencyStop" type="button"
|
||||
class="secondary house-emergency-stop" data-action="house-emergency-stop" data-value="true" hidden>Emergency stop</button></div>
|
||||
class="secondary house-emergency-stop" data-action="house-emergency-stop" data-value="true"
|
||||
hidden>Emergency stop</button></div>
|
||||
</div>
|
||||
<div class="dashboard-global-divider"></div>
|
||||
<div class="house-climate dashboard-house-climate" id="houseClimate"></div>
|
||||
@@ -960,7 +961,8 @@
|
||||
</div>
|
||||
<div class="flow-palette-group block-category-sensor"><span data-i18n="flow.sensors">Sensory / wartości</span>
|
||||
<button type="button" data-flow-add="heating_curve" data-i18n="flow.node.heatingCurve">Krzywa
|
||||
grzewcza</button><button type="button" data-flow-add="outdoor_temperature" data-i18n="flow.node.outdoorTemperature">Temperatura
|
||||
grzewcza</button><button type="button" data-flow-add="outdoor_temperature"
|
||||
data-i18n="flow.node.outdoorTemperature">Temperatura
|
||||
zewn.</button><button type="button" data-flow-add="device_temperature"
|
||||
data-i18n="flow.node.deviceTemperature">Temperatura urządzenia</button><button type="button"
|
||||
data-flow-add="zone_temperature" data-i18n="flow.node.zoneTemperature">Temperatura strefy</button><button
|
||||
@@ -972,21 +974,24 @@
|
||||
data-flow-add="ha_numeric" data-i18n="flow.node.haNumeric">HA: wartość</button><button type="button"
|
||||
data-flow-add="ha_attribute" data-i18n="flow.node.haAttribute">HA: atrybut</button><button type="button"
|
||||
data-flow-add="ha_available" data-i18n="flow.node.haAvailable">HA: dostępność</button><button type="button"
|
||||
data-flow-add="constant" data-i18n="flow.node.constant">Stała testowa</button><button type="button"
|
||||
data-flow-add="shared_input" data-i18n="flow.node.sharedInput">Wspólne wejście</button><button type="button"
|
||||
data-flow-add="rolling_stat" data-i18n="flow.node.rollingStat">Średnia / mediana</button><button
|
||||
type="button" data-flow-add="oscillates" data-i18n="flow.node.oscillates">Wartość oscyluje</button>
|
||||
data-flow-add="constant" data-i18n="flow.node.constant" data-i18n-title="flow.constantDescription">Stała
|
||||
testowa</button><button type="button" data-flow-add="shared_input" data-i18n="flow.node.sharedInput">Wspólne
|
||||
wejście</button><button type="button" data-flow-add="rolling_stat" data-i18n="flow.node.rollingStat">Średnia
|
||||
/ mediana</button><button type="button" data-flow-add="oscillates" data-i18n="flow.node.oscillates">Wartość
|
||||
oscyluje</button>
|
||||
</div>
|
||||
<div class="flow-palette-group block-category-logic"><span data-i18n="flow.logic">Logika</span><button
|
||||
type="button" data-flow-add="logic_and" data-i18n="flow.node.and">AND</button><button type="button"
|
||||
data-flow-add="logic_or" data-i18n="flow.node.or">OR</button><button type="button" data-flow-add="logic_not"
|
||||
data-i18n="flow.node.not">NOT</button></div>
|
||||
<div class="flow-palette-group block-category-action"><span data-i18n="flow.actions">Akcje</span>
|
||||
<button type="button" data-flow-add="zone_heat_cycle" data-i18n="flow.node.simpleHeat"
|
||||
data-i18n-title="flow.simpleHeatDescription">Proste grzanie dzień/noc</button>
|
||||
<button type="button" data-flow-add="zone_thermostat"
|
||||
data-i18n="flow.node.thermostat">Termostat</button><button type="button" data-flow-add="device_action"
|
||||
data-i18n="flow.node.greeDevice">Urządzenie GREE</button><button type="button" data-flow-add="device_feature_action"
|
||||
data-i18n="flow.node.greeFeature">Pojedyncza funkcja GREE</button><button type="button" data-flow-add="group_action"
|
||||
data-i18n="flow.node.group">Grupa</button>
|
||||
data-i18n="flow.node.greeDevice">Urządzenie GREE</button><button type="button"
|
||||
data-flow-add="device_feature_action" data-i18n="flow.node.greeFeature">Pojedyncza funkcja
|
||||
GREE</button><button type="button" data-flow-add="group_action" data-i18n="flow.node.group">Grupa</button>
|
||||
</div>
|
||||
<div class="flow-palette-group block-category-haaction"><span data-i18n="flow.haActions">Home Assistant</span>
|
||||
<button type="button" data-flow-add="ha_service_action" data-i18n="flow.node.haServiceAction">Home Assistant:
|
||||
@@ -1005,8 +1010,8 @@
|
||||
class="secondary flow-selection-action" data-action="flow-clear-selection"
|
||||
data-i18n="flow.clearSelection">Wyczyść zaznaczenie</button><button type="button"
|
||||
class="secondary flow-selection-action" data-action="flow-shortcuts"
|
||||
data-i18n="flow.shortcuts">Skróty</button><span id="flowSelectionCount"
|
||||
class="badge"></span><span id="flowEditorNotice" class="flow-editor-notice" role="status" aria-live="polite"></span></div>
|
||||
data-i18n="flow.shortcuts">Skróty</button><span id="flowSelectionCount" class="badge"></span><span
|
||||
id="flowEditorNotice" class="flow-editor-notice" role="status" aria-live="polite"></span></div>
|
||||
<div class="flow-zoom-controls" role="group" data-i18n-aria="flow.zoomControls" aria-label="Canvas zoom">
|
||||
<button type="button" class="secondary icon-button" data-action="flow-zoom-out"
|
||||
data-i18n-title="flow.zoomOut" title="Zoom out" data-ui-icon="minus"></button><span id="flowZoomLabel"
|
||||
@@ -1054,25 +1059,43 @@
|
||||
<h2 data-i18n="flow.shortcuts">Skróty klawiaturowe</h2>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="flow.shortcutsHint">Skróty działają, gdy kursor nie znajduje się w polu formularza.</p>
|
||||
<p class="field-note" data-i18n="flow.shortcutsHint">Skróty działają, gdy kursor nie znajduje się w polu formularza.
|
||||
</p>
|
||||
<div class="flow-shortcut-grid">
|
||||
<div><span class="flow-shortcut-keys"><kbd>A</kbd><kbd>Insert</kbd><kbd>Ctrl/Cmd+K</kbd></span><span data-i18n="flow.shortcutAdd">Dodaj / wyszukaj blok</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Enter</kbd></span><span data-i18n="flow.shortcutAddFirst">Dodaj pierwszy wynik wyszukiwania</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+S</kbd></span><span data-i18n="flow.shortcutSave">Zapisz Flow</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+Z</kbd></span><span data-i18n="flow.shortcutUndo">Cofnij ostatnią zmianę</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+A</kbd></span><span data-i18n="flow.shortcutSelectAll">Zaznacz wszystkie bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+C</kbd></span><span data-i18n="flow.shortcutCopy">Kopiuj zaznaczone bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+X</kbd></span><span data-i18n="flow.shortcutCut">Wytnij zaznaczone bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+V</kbd></span><span data-i18n="flow.shortcutPaste">Wklej bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+D</kbd></span><span data-i18n="flow.shortcutDuplicate">Duplikuj zaznaczenie</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Delete</kbd><kbd>Backspace</kbd></span><span data-i18n="flow.shortcutDelete">Usuń zaznaczone bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>← ↑ ↓ →</kbd></span><span data-i18n="flow.shortcutMove">Przesuń zaznaczenie o 10 px</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Shift+← ↑ ↓ →</kbd></span><span data-i18n="flow.shortcutMoveFine">Przesuń precyzyjnie o 1 px</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Esc</kbd></span><span data-i18n="flow.shortcutClear">Wyczyść zaznaczenie / anuluj łączenie</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+0</kbd></span><span data-i18n="flow.shortcutFit">Dopasuj cały Flow do widoku</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd++ / -</kbd></span><span data-i18n="flow.shortcutZoom">Powiększ / pomniejsz</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>?</kbd></span><span data-i18n="flow.shortcutHelp">Pokaż tę listę skrótów</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd data-i18n="flow.rightClickKey">Prawy przycisk</kbd></span><span data-i18n="flow.shortcutContextMenu">Menu kontekstowe Flow</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>A</kbd><kbd>Insert</kbd><kbd>Ctrl/Cmd+K</kbd></span><span
|
||||
data-i18n="flow.shortcutAdd">Dodaj / wyszukaj blok</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Enter</kbd></span><span data-i18n="flow.shortcutAddFirst">Dodaj
|
||||
pierwszy wynik wyszukiwania</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+S</kbd></span><span data-i18n="flow.shortcutSave">Zapisz
|
||||
Flow</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+Z</kbd></span><span data-i18n="flow.shortcutUndo">Cofnij
|
||||
ostatnią zmianę</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+A</kbd></span><span data-i18n="flow.shortcutSelectAll">Zaznacz
|
||||
wszystkie bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+C</kbd></span><span data-i18n="flow.shortcutCopy">Kopiuj
|
||||
zaznaczone bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+X</kbd></span><span data-i18n="flow.shortcutCut">Wytnij
|
||||
zaznaczone bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+V</kbd></span><span data-i18n="flow.shortcutPaste">Wklej
|
||||
bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+D</kbd></span><span
|
||||
data-i18n="flow.shortcutDuplicate">Duplikuj zaznaczenie</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Delete</kbd><kbd>Backspace</kbd></span><span
|
||||
data-i18n="flow.shortcutDelete">Usuń zaznaczone bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>← ↑ ↓ →</kbd></span><span data-i18n="flow.shortcutMove">Przesuń
|
||||
zaznaczenie o 10 px</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Shift+← ↑ ↓ →</kbd></span><span
|
||||
data-i18n="flow.shortcutMoveFine">Przesuń precyzyjnie o 1 px</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Esc</kbd></span><span data-i18n="flow.shortcutClear">Wyczyść
|
||||
zaznaczenie / anuluj łączenie</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+0</kbd></span><span data-i18n="flow.shortcutFit">Dopasuj cały
|
||||
Flow do widoku</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd++ / -</kbd></span><span data-i18n="flow.shortcutZoom">Powiększ
|
||||
/ pomniejsz</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>?</kbd></span><span data-i18n="flow.shortcutHelp">Pokaż tę listę
|
||||
skrótów</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd data-i18n="flow.rightClickKey">Prawy przycisk</kbd></span><span
|
||||
data-i18n="flow.shortcutContextMenu">Menu kontekstowe Flow</span></div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -1173,8 +1196,10 @@
|
||||
</div>
|
||||
<p class="field-note"><strong id="flowDescriptionName"></strong></p>
|
||||
<label><span data-i18n="flow.description">Description</span><textarea name="description" rows="6" maxlength="1200"
|
||||
data-i18n-placeholder="flow.descriptionPlaceholder" placeholder="Describe what this Flow does…"></textarea></label>
|
||||
<p class="field-note" data-i18n="flow.descriptionQuickHint">Only the description is changed. You do not need to open the Flow editor.</p>
|
||||
data-i18n-placeholder="flow.descriptionPlaceholder"
|
||||
placeholder="Describe what this Flow does…"></textarea></label>
|
||||
<p class="field-note" data-i18n="flow.descriptionQuickHint">Only the description is changed. You do not need to
|
||||
open the Flow editor.</p>
|
||||
<div class="form-actions"><button type="button" class="secondary" data-close
|
||||
data-i18n="actions.cancel">Cancel</button><button type="submit" class="primary"
|
||||
data-i18n="actions.save">Save</button></div>
|
||||
@@ -1189,12 +1214,17 @@
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="flow-simple-intro">
|
||||
<div><strong id="flowSimpleSimulationName"></strong><span data-i18n="flow.simpleSimulatorHint">Set a situation and see whether each Flow action would run.</span></div>
|
||||
<div><strong id="flowSimpleSimulationName"></strong><span data-i18n="flow.simpleSimulatorHint">Set a situation
|
||||
and see whether each Flow action would run.</span></div>
|
||||
<span class="badge" data-i18n="flow.simpleSimulatorSafe">Preview only — nothing is changed</span>
|
||||
</div>
|
||||
<section class="flow-simple-scenario">
|
||||
<div class="flow-simple-section-head"><div><strong data-i18n="flow.simpleScenario">Situation to check</strong><p class="field-note"
|
||||
data-i18n="flow.simpleScenarioHint">Leave values unchanged to use the current application and Home Assistant state.</p></div></div>
|
||||
<div class="flow-simple-section-head">
|
||||
<div><strong data-i18n="flow.simpleScenario">Situation to check</strong>
|
||||
<p class="field-note" data-i18n="flow.simpleScenarioHint">Leave values unchanged to use the current
|
||||
application and Home Assistant state.</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="flow-simple-time"><span data-i18n="flow.simulationTime">Simulation date and time</span><input
|
||||
id="flowSimpleSimulationAt" type="datetime-local"></label>
|
||||
<div id="flowSimpleSimulationOverrides" class="flow-simple-simulation-overrides"></div>
|
||||
@@ -1345,7 +1375,10 @@
|
||||
<h2 data-i18n="discovery.title">Discover GREE devices</h2><button type="button" data-close
|
||||
data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="discovery.help">Auto accepts both AES-ECB and AES-GCM devices, uses discovery metadata only as a protocol hint and verifies the actual protocol during binding. Selecting V1 or V2 keeps discovery and binding locked to that protocol. Multiple passes improve discovery when several Wi-Fi modules answer the same broadcast.</p>
|
||||
<p class="field-note" data-i18n="discovery.help">Auto accepts both AES-ECB and AES-GCM devices, uses discovery
|
||||
metadata only as a protocol hint and verifies the actual protocol during binding. Selecting V1 or V2 keeps
|
||||
discovery and binding locked to that protocol. Multiple passes improve discovery when several Wi-Fi modules
|
||||
answer the same broadcast.</p>
|
||||
<label><span data-i18n="discovery.protocol">Discovery protocol</span><select name="protocol_version">
|
||||
<option value="0" data-i18n="devices.protocolAuto">Auto (V1 + V2)</option>
|
||||
<option value="1">V1 AES-ECB</option>
|
||||
@@ -1366,7 +1399,8 @@
|
||||
<h2 data-i18n="discovery.nameDevices">Discovered local devices</h2><button type="button" data-close
|
||||
data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="discovery.nameDevicesHelp">Select the units you want to add. MAC address, IP address and the discovery protocol hint are shown for every result; Auto is resolved during binding.</p>
|
||||
<p class="field-note" data-i18n="discovery.nameDevicesHelp">Select the units you want to add. MAC address, IP
|
||||
address and the discovery protocol hint are shown for every result; Auto is resolved during binding.</p>
|
||||
<div id="discoveryNamesList" class="discovery-name-list"></div>
|
||||
<div class="form-actions"><button type="button" class="secondary" data-close
|
||||
data-i18n="actions.cancel">Cancel</button><button class="primary" type="submit"
|
||||
@@ -1598,9 +1632,10 @@
|
||||
type="number" min="15" name="min_adjust_seconds" value="120"></label><label><span
|
||||
data-i18n="zones.standbyOffset">Standby setpoint offset °C</span><input type="text" inputmode="decimal"
|
||||
step="0.5" min="0.5" max="8" name="standby_offset_c" value="2"></label></div>
|
||||
<label><span data-i18n="zones.heatAnticipation">Early heat coasting (°C before target)</span><input
|
||||
type="text" inputmode="decimal" step="0.1" min="0" max="2.5" name="heat_anticipation_c" value="0"></label>
|
||||
<p class="field-note" data-i18n="zones.heatAnticipationHint">A positive value lowers the GREE setpoint early without powering the indoor unit off. 0 disables early coasting.</p>
|
||||
<label><span data-i18n="zones.heatAnticipation">Early heat coasting (°C before target)</span><input type="text"
|
||||
inputmode="decimal" step="0.1" min="0" max="2.5" name="heat_anticipation_c" value="0"></label>
|
||||
<p class="field-note" data-i18n="zones.heatAnticipationHint">A positive value lowers the GREE setpoint early
|
||||
without powering the indoor unit off. 0 disables early coasting.</p>
|
||||
<label class="check"><input type="checkbox" name="smart_fan" checked> <span data-i18n="zones.smartFan">Smart fan
|
||||
assist</span></label>
|
||||
<input type="hidden" name="min_on_seconds" value="180"><input type="hidden" name="min_off_seconds"
|
||||
@@ -1882,15 +1917,44 @@
|
||||
<option value="4" data-i18n="fan.mediumHigh">Medium-high</option>
|
||||
<option value="5" data-i18n="fan.high">High</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="devices.quiet">Quiet</span><select name="action_quiet"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
|
||||
<label><span data-i18n="devices.turbo">Turbo</span><select name="action_turbo"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
|
||||
<label><span data-i18n="devices.light">Panel light</span><select name="action_light"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
|
||||
<label><span data-i18n="devices.air">Air</span><select name="action_air"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
|
||||
<label><span data-i18n="devices.xfan">X-Fan</span><select name="action_xfan"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
|
||||
<label><span data-i18n="devices.health">Health</span><select name="action_health"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
|
||||
<label><span data-i18n="devices.sleep">Sleep</span><select name="action_sleep"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
|
||||
<label><span data-i18n="devices.quiet">Quiet</span><select name="action_quiet">
|
||||
<option value="" data-i18n="actions.noChange">No change</option>
|
||||
<option value="true" data-i18n="common.on">On</option>
|
||||
<option value="false" data-i18n="common.off">Off</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="devices.turbo">Turbo</span><select name="action_turbo">
|
||||
<option value="" data-i18n="actions.noChange">No change</option>
|
||||
<option value="true" data-i18n="common.on">On</option>
|
||||
<option value="false" data-i18n="common.off">Off</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="devices.light">Panel light</span><select name="action_light">
|
||||
<option value="" data-i18n="actions.noChange">No change</option>
|
||||
<option value="true" data-i18n="common.on">On</option>
|
||||
<option value="false" data-i18n="common.off">Off</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="devices.air">Air</span><select name="action_air">
|
||||
<option value="" data-i18n="actions.noChange">No change</option>
|
||||
<option value="true" data-i18n="common.on">On</option>
|
||||
<option value="false" data-i18n="common.off">Off</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="devices.xfan">X-Fan</span><select name="action_xfan">
|
||||
<option value="" data-i18n="actions.noChange">No change</option>
|
||||
<option value="true" data-i18n="common.on">On</option>
|
||||
<option value="false" data-i18n="common.off">Off</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="devices.health">Health</span><select name="action_health">
|
||||
<option value="" data-i18n="actions.noChange">No change</option>
|
||||
<option value="true" data-i18n="common.on">On</option>
|
||||
<option value="false" data-i18n="common.off">Off</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="devices.sleep">Sleep</span><select name="action_sleep">
|
||||
<option value="" data-i18n="actions.noChange">No change</option>
|
||||
<option value="true" data-i18n="common.on">On</option>
|
||||
<option value="false" data-i18n="common.off">Off</option>
|
||||
</select></label>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="automations.singleDeviceFunctionsHint">Leave every other field as No change to modify only the selected unit function.</p>
|
||||
<p class="field-note" data-i18n="automations.singleDeviceFunctionsHint">Leave every other field as No change to
|
||||
modify only the selected unit function.</p>
|
||||
</details>
|
||||
<p class="field-note" id="automationGroupHint" hidden data-i18n="automations.groupHint">For a group, Auto mode
|
||||
means follow the whole-house heating/cooling mode. Group presets are applied to all member zones.</p>
|
||||
|
||||
@@ -471,7 +471,10 @@ function zoneRuntimeStatusLabel(zone, effectiveMode, device = null) {
|
||||
if (zone.device_manual_override === true) return tr('zones.manualDeviceControl');
|
||||
if (!zone.enabled) return tr('common.disabled');
|
||||
if (zone.local_thermostat_power === false) return tr('zones.localThermostatOff');
|
||||
if ((effectiveMode || 'off') === 'off') return tr('zones.waiting');
|
||||
if ((effectiveMode || 'off') === 'off') {
|
||||
if (zone.control_source === 'automation.flow' && zone.simple_heat && zone.control_owner === 'automation') return tr(device?.power ? 'zones.flowDayPendingOff' : 'zones.flowDayOff');
|
||||
return tr('zones.waiting');
|
||||
}
|
||||
if (device?.enabled === false) return tr('zones.waitingDeviceDisabled');
|
||||
if (device && (!device.online || Number(device.communication_failures || 0) > 0)) return tr('zones.waitingOffline');
|
||||
if (zone.current_temperature == null) return tr('zones.noMeasurement');
|
||||
@@ -480,7 +483,7 @@ function zoneRuntimeStatusLabel(zone, effectiveMode, device = null) {
|
||||
if (zone.demand && device && !device.power) return tr('zones.waitingStart');
|
||||
if (zone.demand && device?.power && ['heat', 'cool'].includes(effectiveMode) && device.mode !== effectiveMode) return tr('zones.waitingMode');
|
||||
if (zone.control_owner === 'automation' && zone.control_source === 'automation.flow') {
|
||||
if (zone.demand && zone.heat_coasting && device?.power) return tr('zones.flowCoasting');
|
||||
if (zone.heat_coasting && device?.power) return tr('zones.flowCoasting');
|
||||
return zone.demand ? tr('zones.flowHeating') : tr('zones.flowStandby');
|
||||
}
|
||||
return zone.demand ? tr('zones.runningDemand') : tr('zones.satisfied');
|
||||
@@ -501,7 +504,8 @@ function zoneCard(zone, detailed = true) {
|
||||
const displayPreset = idleWithoutTarget ? 'auto' : (zone.manual_preset || zone.active_preset || 'comfort');
|
||||
const globalModeAvailable = ['heat', 'cool'].includes(app.settings?.house_mode);
|
||||
const globalModeDisabled = globalModeAvailable ? '' : ` disabled aria-disabled="true" title="${esc(tr('zones.globalModeUnavailable'))}"`;
|
||||
const hasManualOverride = zone.manual_preset != null || zone.manual_setpoint != null;
|
||||
const flowOwned = zone.control_owner === 'automation' && zone.control_source === 'automation.flow';
|
||||
const hasManualOverride = !flowOwned && (zone.manual_preset != null || zone.manual_setpoint != null);
|
||||
const deviceManualOverride = zone.device_manual_override === true;
|
||||
const controlGroup = groupControlForZone(zone);
|
||||
const deviceUnavailable = !!device && (device.enabled === false || !device.online || Number(device.communication_failures || 0) > 0);
|
||||
@@ -527,9 +531,9 @@ function zoneCard(zone, detailed = true) {
|
||||
? tr('zones.temporaryOverride', { time: temporaryStatus?.countdown || '—' })
|
||||
: localThermostatOverride
|
||||
? tr(localThermostatPower ? 'zones.localThermostatOn' : 'zones.localThermostatOff')
|
||||
: (zone.manual_override_until
|
||||
: (flowOwned ? tr('zones.ownerFlow') : (zone.manual_override_until
|
||||
? `${tr('zones.overrideUntil')} ${new Date(zone.manual_override_until).toLocaleTimeString(locale(), { hour: '2-digit', minute: '2-digit' })}`
|
||||
: (hasManualOverride ? tr('zones.manualNoBoundary') : tr('zones.scheduleControl')));
|
||||
: (hasManualOverride ? tr('zones.manualNoBoundary') : tr('zones.scheduleControl'))));
|
||||
const manualTakeover = deviceManualOverride
|
||||
? `<div class="manual-override-panel"><div><strong>${esc(tr('zones.manualDeviceControl'))}</strong><p>${esc(tr('zones.manualDeviceDescription'))}</p></div><button type="button" data-action="zone-resume-automation" data-resume="device" data-id="${esc(zone.id)}">${esc(tr('zones.resumeAutomation'))}</button></div>`
|
||||
: localThermostatOverride && !temporarySession
|
||||
|
||||
+75
-6
@@ -23,11 +23,12 @@ const FLOW_NODE_META = Object.freeze({
|
||||
zone_state: { titleKey: 'flow.node.zoneState', category: 'sensor' },
|
||||
group_state: { titleKey: 'flow.node.groupState', category: 'sensor' },
|
||||
night_mode: { titleKey: 'flow.node.nightMode', category: 'time' },
|
||||
constant: { titleKey: 'flow.node.constant', category: 'sensor' },
|
||||
constant: { titleKey: 'flow.node.constant', category: 'sensor', descriptionKey: 'flow.constantDescription' },
|
||||
shared_input: { titleKey: 'flow.node.sharedInput', category: 'sensor' },
|
||||
logic_and: { titleKey: 'flow.node.and', category: 'logic' },
|
||||
logic_or: { titleKey: 'flow.node.or', category: 'logic' },
|
||||
logic_not: { titleKey: 'flow.node.not', category: 'logic' },
|
||||
zone_heat_cycle: { titleKey: 'flow.node.simpleHeat', category: 'action', descriptionKey: 'flow.simpleHeatDescription' },
|
||||
zone_thermostat: { titleKey: 'flow.node.thermostat', category: 'action' },
|
||||
device_action: { titleKey: 'flow.node.greeDevice', category: 'action' },
|
||||
device_feature_action: { titleKey: 'flow.node.greeFeature', category: 'action' },
|
||||
@@ -98,6 +99,7 @@ function flowDefaultConfig(kind) {
|
||||
if (kind === 'night_mode') return {};
|
||||
if (kind === 'constant') return { value: true };
|
||||
if (kind === 'shared_input') return sharedFlowReferenceDefaultConfig(app.flowSharedInputs?.[0]);
|
||||
if (kind === 'zone_heat_cycle') return { zone_id: app.zones[0]?.id || '', day_target: 20.0, night_target: 20.6, day_min: 19.7, boost_after_minutes: 10, day_off_after_minutes: 30, cooldown_seconds: 60 };
|
||||
if (kind === 'zone_thermostat') return { zone_id: app.zones[0]?.id || '', preset: 'comfort', setpoint: 21, mode: 'auto', power: null, heat_anticipation_c: null, standby_offset_c: null, swing_vertical: null, swing_horizontal: null, cooldown_seconds: 60 };
|
||||
if (kind === 'device_action') return { device_id: app.devices[0]?.id || '', power: true, mode: '', target_temperature: null, fan_speed: null, swing_vertical: null, swing_horizontal: null, quiet: null, turbo: null, light: null, air: null, xfan: null, health: null, sleep: null, cooldown_seconds: 60 };
|
||||
if (kind === 'device_feature_action') return { device_id: app.devices[0]?.id || '', feature: 'light', value: true, cooldown_seconds: 60 };
|
||||
@@ -424,6 +426,10 @@ function flowNodeSummary(node) {
|
||||
if (node.kind === 'logic_and') return tr('flow.allConditions');
|
||||
if (node.kind === 'logic_or') return tr('flow.anyCondition');
|
||||
if (node.kind === 'logic_not') return tr('flow.invertCondition');
|
||||
if (node.kind === 'zone_heat_cycle') {
|
||||
const zone = app.zones.find(z => z.id === c.zone_id);
|
||||
return `${zone?.name || tr('common.noZone')} · ${tr('flow.simpleHeatDay')} ${fmtTemp(c.day_target)} · ${tr('flow.simpleHeatNight')} ${fmtTemp(c.night_target)}`;
|
||||
}
|
||||
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'));
|
||||
@@ -627,10 +633,18 @@ function renderFlowBlockLibrary(filter = '') {
|
||||
const groups = $$('.flow-palette .flow-palette-group');
|
||||
host.innerHTML = groups.map(group => {
|
||||
const title = group.querySelector(':scope > span')?.textContent?.trim() || '';
|
||||
const buttons = $$('[data-flow-add]', group).filter(button => !query || `${title} ${button.textContent}`.toLocaleLowerCase(locale()).includes(query));
|
||||
const buttons = $$('[data-flow-add]', group).filter(button => {
|
||||
const meta = FLOW_NODE_META[button.dataset.flowAdd];
|
||||
const description = meta?.descriptionKey ? tr(meta.descriptionKey) : '';
|
||||
return !query || `${title} ${button.textContent} ${description}`.toLocaleLowerCase(locale()).includes(query);
|
||||
});
|
||||
if (!buttons.length) return '';
|
||||
const categoryClass = [...group.classList].find(name => name.startsWith('block-category-')) || '';
|
||||
return `<section class="flow-block-library-group ${esc(categoryClass)}"><h3>${esc(title)}</h3><div>${buttons.map(button => `<button type="button" class="secondary" data-flow-add="${esc(button.dataset.flowAdd)}">${esc(button.textContent.trim())}</button>`).join('')}</div></section>`;
|
||||
return `<section class="flow-block-library-group ${esc(categoryClass)}"><h3>${esc(title)}</h3><div>${buttons.map(button => {
|
||||
const meta = FLOW_NODE_META[button.dataset.flowAdd];
|
||||
const description = meta?.descriptionKey ? tr(meta.descriptionKey) : '';
|
||||
return `<button type="button" class="secondary" data-flow-add="${esc(button.dataset.flowAdd)}" ${description ? `title="${esc(description)}"` : ''}><span>${esc(button.textContent.trim())}</span>${description ? `<small class="flow-block-library-description">${esc(description)}</small>` : ''}</button>`;
|
||||
}).join('')}</div></section>`;
|
||||
}).join('') || `<div class="empty compact"><strong>${esc(tr('flow.noBlocksFound'))}</strong><span>${esc(tr('flow.noBlocksFoundHint'))}</span></div>`;
|
||||
}
|
||||
|
||||
@@ -647,7 +661,7 @@ function renderFlowEditor() {
|
||||
const nodesHost = $('#flowNodes');
|
||||
nodesHost.innerHTML = draft.nodes.map(node => {
|
||||
const meta = FLOW_NODE_META[node.kind] || { title: node.kind, category: 'logic' };
|
||||
return `<article class="diagram-node flow-node-${esc(meta.category)} ${(app.flowSelectedNodeIds || []).includes(node.id) ? 'selected' : ''}" data-flow-node="${esc(node.id)}" style="left:${Number(node.x || 0)}px;top:${Number(node.y || 0)}px">
|
||||
return `<article class="diagram-node flow-node-${esc(meta.category)} ${(app.flowSelectedNodeIds || []).includes(node.id) ? 'selected' : ''}" data-flow-node="${esc(node.id)}" ${meta.descriptionKey ? `title="${esc(tr(meta.descriptionKey))}"` : ''} style="left:${Number(node.x || 0)}px;top:${Number(node.y || 0)}px">
|
||||
<button class="diagram-port flow-port-in" type="button" data-flow-input="${esc(node.id)}" title="${esc(tr('flow.connectHere'))}"></button>
|
||||
<div class="flow-node-head"><span>${esc(flowNodeTitle(meta))}</span><button type="button" data-flow-remove="${esc(node.id)}" aria-label="${esc(tr('actions.delete'))}">${uiIcon('close')}</button></div>
|
||||
<div class="flow-node-body">${esc(flowNodeSummary(node))}${node.kind === 'shared_input' && node.config?.input_id ? `<div class="flow-node-current" data-flow-shared-current="${esc(node.config.input_id)}"><span>${esc(tr('flow.sharedInputTestCurrent'))}</span><strong>—</strong></div>` : ''}</div>
|
||||
@@ -773,11 +787,50 @@ function flowIncomingHeatingCurve(nodeId) {
|
||||
return edge ? flowNodeById(edge.from) : null;
|
||||
}
|
||||
|
||||
// The inspector derives all examples from this block's room targets; GREE
|
||||
// accepts whole-degree setpoints but the room targets may have tenths.
|
||||
function flowSimpleHeatHelp(config, nodeId = '', expanded = false) {
|
||||
const day = Number.isFinite(Number(config.day_target)) ? Number(config.day_target) : 20;
|
||||
const night = Number.isFinite(Number(config.night_target)) ? Number(config.night_target) : 20.6;
|
||||
const minimum = Number.isFinite(Number(config.day_min)) ? Number(config.day_min) : 19.7;
|
||||
const boostMinutes = Number.isFinite(Number(config.boost_after_minutes)) ? Number(config.boost_after_minutes) : 10;
|
||||
const offMinutes = Number.isFinite(Number(config.day_off_after_minutes)) ? Number(config.day_off_after_minutes) : 30;
|
||||
const nightGentle = Math.min(30, Math.max(16, Math.floor(night) + 1));
|
||||
const nightBoost = Math.min(30, Math.max(16, Math.ceil(night + 2)));
|
||||
const nightBrake = Math.min(30, Math.max(16, Math.floor(night - 0.5)));
|
||||
const nightStandby = Math.min(30, Math.max(16, Math.floor(night - 1)));
|
||||
const dayGentle = Math.min(30, Math.max(16, Math.floor(day) + 1));
|
||||
const dayBoost = Math.min(30, Math.max(16, Math.ceil(day + 2)));
|
||||
const line = (key, params) => `<li>${esc(tr(key, params))}</li>`;
|
||||
return `<details class="flow-block-help" data-flow-help-node="${esc(nodeId)}" ${expanded ? 'open' : ''}>
|
||||
<summary>${esc(tr('flow.simpleHeatHowItWorks'))}</summary>
|
||||
<p>${esc(tr('flow.simpleHeatHelpIntro'))}</p>
|
||||
<strong>${esc(tr('flow.simpleHeatHelpNightTitle', { target: fmtTemp(night) }))}</strong>
|
||||
<ul>
|
||||
${line('flow.simpleHeatHelpNightSleep', { room: fmtTemp(night), gree: fmtTemp(nightStandby) })}
|
||||
${line('flow.simpleHeatHelpNightBrake', { room: fmtTemp(night - 0.1), gree: fmtTemp(nightBrake) })}
|
||||
${line('flow.simpleHeatHelpNightGentle', { room: fmtTemp(night - 0.1), gree: fmtTemp(nightGentle) })}
|
||||
${line('flow.simpleHeatHelpNightBoost', { room: fmtTemp(night - 0.8), minutes: boostMinutes, gree: fmtTemp(nightBoost) })}
|
||||
</ul>
|
||||
<strong>${esc(tr('flow.simpleHeatHelpDayTitle', { target: fmtTemp(day), minimum: fmtTemp(minimum) }))}</strong>
|
||||
<ul>
|
||||
${line('flow.simpleHeatHelpDayOff', { minimum: fmtTemp(minimum) })}
|
||||
${line('flow.simpleHeatHelpDayHeat', { gentle: fmtTemp(dayGentle), boost: fmtTemp(dayBoost), room: fmtTemp(day - 0.8), minutes: boostMinutes })}
|
||||
${line('flow.simpleHeatHelpDayStop', { target: fmtTemp(day), minutes: offMinutes })}
|
||||
</ul>
|
||||
<p>${esc(tr('flow.simpleHeatHelpSensors'))}</p>
|
||||
<p>${esc(tr('flow.simpleHeatHelpSchedule'))}</p>
|
||||
<p>${esc(tr('flow.simpleHeatHelpControl'))}</p>
|
||||
</details>`;
|
||||
}
|
||||
|
||||
function renderFlowInspector() {
|
||||
const host = $('#flowInspector'), node = flowNodeById(app.flowSelectedNodeId); if (!host) return;
|
||||
host.classList.toggle('has-selection', Boolean(node));
|
||||
if (!node) { host.classList.remove('is-expanded'); host.innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.selectBlock'))}</strong><span>${esc(tr('flow.selectBlockHint'))}</span></div>`; return; }
|
||||
const c = node.config || {}, meta = FLOW_NODE_META[node.kind] || { title: node.kind };
|
||||
const currentHelp = host.querySelector('.flow-block-help');
|
||||
const helpOpen = Boolean(currentHelp?.open && currentHelp.dataset.flowHelpNode === node.id);
|
||||
let fields = '';
|
||||
if (node.kind === 'weekday') fields = `<div class="flow-day-grid">${[1, 2, 3, 4, 5, 6, 7].map(day => `<label><input type="checkbox" data-flow-config="days" value="${day}" ${(c.days || []).includes(day) ? 'checked' : ''}><span>${esc(tr(`day.${day}`))}</span></label>`).join('')}</div>`;
|
||||
else if (node.kind === 'time_range') fields = `<div class="two"><label><span>${esc(tr('common.from'))}</span><input type="time" data-flow-config="start" value="${esc(c.start || '06:00')}"></label><label><span>${esc(tr('common.to'))}</span><input type="time" data-flow-config="end" value="${esc(c.end || '08:00')}"></label></div>`;
|
||||
@@ -803,7 +856,7 @@ function renderFlowInspector() {
|
||||
else if (node.kind === 'zone_state') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-flow-config="field">${['enabled', 'mode', 'active_preset', 'demand', 'control_owner', 'device_manual_override', 'local_thermostat_power'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(v)}</option>`).join('')}</select></label>${flowTextComparisonFields(c)}`;
|
||||
else if (node.kind === 'group_state') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-flow-config="group_id">${flowSelectOptions(app.groups, c.group_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-flow-config="field"><option value="power_enabled" selected>power_enabled</option></select></label>${flowTextComparisonFields(c)}`;
|
||||
else if (node.kind === 'night_mode') fields = `<p class="field-note">${esc(tr('flow.nightModeHint'))}</p>`;
|
||||
else if (node.kind === 'constant') fields = `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value"><option value="true" ${c.value !== false ? 'selected' : ''}>${esc(tr('common.yes'))}</option><option value="false" ${c.value === false ? 'selected' : ''}>${esc(tr('common.no'))}</option></select></label>`;
|
||||
else if (node.kind === 'constant') fields = `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value"><option value="true" ${c.value !== false ? 'selected' : ''}>${esc(tr('common.yes'))}</option><option value="false" ${c.value === false ? 'selected' : ''}>${esc(tr('common.no'))}</option></select></label><p class="field-note">${esc(tr('flow.constantHelp'))}</p>`;
|
||||
else if (node.kind === 'shared_input') {
|
||||
const item = (app.flowSharedInputs || []).find(value => value.id === c.input_id) || app.flowSharedInputs?.[0];
|
||||
fields = (app.flowSharedInputs || []).length
|
||||
@@ -813,6 +866,19 @@ function renderFlowInspector() {
|
||||
else if (node.kind === 'logic_and') fields = `<p class="field-note">${esc(tr('flow.andHint'))}</p>`;
|
||||
else if (node.kind === 'logic_or') fields = `<p class="field-note">${esc(tr('flow.orHint'))}</p>`;
|
||||
else if (node.kind === 'logic_not') fields = `<p class="field-note">${esc(tr('flow.notHint'))}</p>`;
|
||||
else if (node.kind === 'zone_heat_cycle') fields = `
|
||||
<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label>
|
||||
<div class="two">
|
||||
<label title="${esc(tr('flow.simpleHeatDayTip'))}"><span>${esc(tr('flow.simpleHeatDay'))} °C</span><input type="number" min="17" max="28" step="0.1" data-flow-config="day_target" value="${Number(c.day_target ?? 20)}"></label>
|
||||
<label title="${esc(tr('flow.simpleHeatNightTip'))}"><span>${esc(tr('flow.simpleHeatNight'))} °C</span><input type="number" min="17" max="28" step="0.1" data-flow-config="night_target" value="${Number(c.night_target ?? 20.6)}"></label>
|
||||
</div>
|
||||
<label title="${esc(tr('flow.simpleHeatMinimumTip'))}"><span>${esc(tr('flow.simpleHeatMinimum'))} °C</span><input type="number" min="17" max="28" step="0.1" data-flow-config="day_min" value="${Number(c.day_min ?? 19.7)}"></label>
|
||||
<div class="two">
|
||||
<label title="${esc(tr('flow.simpleHeatBoostTip'))}"><span>${esc(tr('flow.simpleHeatBoostMinutes'))}</span><input type="number" min="3" max="30" step="1" data-flow-config="boost_after_minutes" value="${Number(c.boost_after_minutes ?? 10)}"></label>
|
||||
<label title="${esc(tr('flow.simpleHeatOffTip'))}"><span>${esc(tr('flow.simpleHeatOffMinutes'))}</span><input type="number" min="10" max="120" step="1" data-flow-config="day_off_after_minutes" value="${Number(c.day_off_after_minutes ?? 30)}"></label>
|
||||
</div>
|
||||
<p class="field-note">${esc(tr('flow.simpleHeatHint'))}</p>
|
||||
${flowSimpleHeatHelp(c, node.id, helpOpen)}`;
|
||||
else if (node.kind === 'zone_thermostat') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label><div class="two"><label><span>${esc(tr('schedules.profile'))}</span><select data-flow-config="preset">${['auto', 'comfort', 'sleep', 'away', 'custom'].map(v => `<option value="${v}" ${c.preset === v ? 'selected' : ''}>${esc(zonePresetLabel(v))}</option>`).join('')}</select></label><label><span>${esc(tr('common.targetC'))}</span><input type="number" min="8" max="30" step="0.1" data-flow-config="setpoint" value="${Number(c.setpoint ?? 21)}"></label></div><div class="two"><label><span>${esc(tr('common.mode'))}</span><select data-flow-config="mode"><option value="auto" ${c.mode === 'auto' ? 'selected' : ''}>Auto</option><option value="heat" ${c.mode === 'heat' ? 'selected' : ''}>${esc(tr('mode.heat'))}</option><option value="cool" ${c.mode === 'cool' ? 'selected' : ''}>${esc(tr('mode.cool'))}</option></select></label><label><span>${esc(tr('common.power'))}</span><select data-flow-config="power"><option value="" ${c.power == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c.power === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c.power === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label></div><div class="two"><label><span>${esc(tr('zones.heatAnticipation'))}</span><input type="number" min="0" max="2.5" step="0.1" data-flow-config="heat_anticipation_c" value="${c.heat_anticipation_c == null ? '' : Number(c.heat_anticipation_c)}" placeholder="—"></label><label><span>${esc(tr('zones.standbyOffset'))}</span><input type="number" min="0.5" max="8" step="0.1" data-flow-config="standby_offset_c" value="${c.standby_offset_c == null ? '' : Number(c.standby_offset_c)}" placeholder="—"></label></div><details class="flow-device-options"><summary>${esc(tr('flow.thermostatDeviceOptions'))}</summary><div class="two">${flowOptionalLouverField(c, 'swing_vertical', 'vertical')}${flowOptionalLouverField(c, 'swing_horizontal', 'horizontal')}</div><p class="field-note">${esc(tr('flow.thermostatDeviceOptionsHint'))}</p></details><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
|
||||
else if (node.kind === 'device_action') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>${flowActionFields(c, false)}`;
|
||||
else if (node.kind === 'device_feature_action') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label><label><span>${esc(tr('flow.deviceFeature'))}</span><select data-flow-config="feature">${flowDeviceFeatureOptions(c.feature)}</select></label>${flowDeviceFeatureValueField(c)}<p class="field-note">${esc(tr('flow.deviceFeatureHint'))}</p><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
|
||||
@@ -826,7 +892,8 @@ function renderFlowInspector() {
|
||||
} else if (incomingCurve && node.kind === 'ha_service_action') {
|
||||
fields += `<label><span>${esc(tr('flow.heatingCurveHaValueKey'))}</span><input data-flow-config="value_key" value="${esc(c.value_key || 'temperature')}" placeholder="temperature"></label><p class="field-note flow-value-binding-note">${esc(tr('flow.heatingCurveHaBindingHint'))}</p>`;
|
||||
}
|
||||
host.innerHTML = `<div class="flow-inspector-head"><div><span class="eyebrow">${esc(tr('flow.blockSettings'))}</span><h3>${esc(flowNodeTitle(meta))}</h3></div><div class="flow-inspector-head-actions"><button type="button" class="secondary flow-inspector-expand" data-action="flow-toggle-inspector" aria-label="${esc(tr('flow.expandSettings'))}">${uiIcon('expand-vertical')}</button><button type="button" class="secondary flow-inspector-close" data-action="flow-clear-selection" aria-label="${esc(tr('flow.clearSelection'))}">${uiIcon('close')}</button><button type="button" class="danger flow-inspector-delete" data-flow-remove="${esc(node.id)}">${esc(tr('actions.delete'))}</button></div></div>${fields}<div class="flow-inspector-meta"><small>ID</small><code>${esc(node.id)}</code></div>`;
|
||||
const description = meta.descriptionKey ? `<p class="flow-inspector-description">${esc(tr(meta.descriptionKey))}</p>` : '';
|
||||
host.innerHTML = `<div class="flow-inspector-head"><div><span class="eyebrow">${esc(tr('flow.blockSettings'))}</span><h3>${esc(flowNodeTitle(meta))}</h3></div><div class="flow-inspector-head-actions"><button type="button" class="secondary flow-inspector-expand" data-action="flow-toggle-inspector" aria-label="${esc(tr('flow.expandSettings'))}">${uiIcon('expand-vertical')}</button><button type="button" class="secondary flow-inspector-close" data-action="flow-clear-selection" aria-label="${esc(tr('flow.clearSelection'))}">${uiIcon('close')}</button><button type="button" class="danger flow-inspector-delete" data-flow-remove="${esc(node.id)}">${esc(tr('actions.delete'))}</button></div></div>${description}${fields}<div class="flow-inspector-meta"><small>ID</small><code>${esc(node.id)}</code></div>`;
|
||||
}
|
||||
|
||||
function flowTextComparisonFields(c, numeric = false) { const options = numeric ? flowOperatorOptions(c.operator || 'eq') : [['eq', '='], ['neq', '≠']].map(([v, l]) => `<option value="${v}" ${v === (c.operator || 'eq') ? 'selected' : ''}>${l}</option>`).join(''); return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${options}</select></label><label><span>${esc(tr('flow.value'))}</span><input data-flow-config="value" value="${esc(c.value ?? '')}"></label></div>`; }
|
||||
@@ -1736,6 +1803,8 @@ function updateFlowConfig(input) {
|
||||
else if (wasLouver) node.config.value = '';
|
||||
}
|
||||
else if (node.kind === 'device_state' && key === 'value' && ['swing_vertical', 'swing_horizontal'].includes(node.config.field)) node.config.value = Number(input.value);
|
||||
else if (node.kind === 'zone_heat_cycle' && ['day_target', 'night_target', 'day_min', 'boost_after_minutes', 'day_off_after_minutes'].includes(key))
|
||||
node.config[key] = Number(input.value);
|
||||
else if (node.kind === 'zone_thermostat' && ['heat_anticipation_c', 'standby_offset_c'].includes(key))
|
||||
node.config[key] = input.value === '' ? null : Number(input.value);
|
||||
else if (['swing_vertical', 'swing_horizontal'].includes(key)) node.config[key] = input.value === '' ? null : Number(input.value);
|
||||
|
||||
Reference in new issue
Block a user