This commit is contained in:
Mateusz Gruszczyński
2026-09-17 11:21:49 +02:00
parent 32f8a32bd2
commit df10ead47e
38 changed files with 655 additions and 210 deletions
Generated
+1 -1
View File
@@ -700,7 +700,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.14.24"
version = "0.15.0"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
version = "0.14.24"
version = "0.15.0"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+37 -37
View File
@@ -1,28 +1,28 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
f71644d81f7f91aee248bf6f61d711a8429694dae6a785706679468cde082e28 ./Cargo.lock
ed4fcc9fcf0d11995f086047c5876787e035a539d0aa588397f90862686adab1 ./Cargo.toml
c3c16d0c76fbb80ce5752b28a1736177940670b10f44235eac5997c4e195c634 ./Cargo.lock
ad513e247ab5dbed3c91a6463cf410cf1a4ac73c0b30422bad40afb93cee0159 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
9b0edc59898f7fdfa169cacc17b9c3307fd555cd9c4ca4079b534d0ae2437d23 ./README.md
67fc31faccb818e097c4ee5fd54c5929c6a1d63cf12cc36c652d6562a3bc82cd ./SOURCE_MANIFEST.sha256
65a1319962dc7a1e7ac0db1a00219b86fae87492a30d38f4df2c896e213b7c83 ./README.md
8505dda3503e63f0958898ccb2ea9321f9e5d0ac64b2d25782d952dc2105b3e4 ./SOURCE_MANIFEST.sha256
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
dfb2736a61aa52634a0ac23fe379baf5f6c03e04837356a1c0ff93e25dc41832 ./docs/API.md
8b548d02dbdf0563f2be617a1d677459bd1959bed93a1799a01bf61bb5aed405 ./docs/FLOW.md
d9bf3a1644ed3628bd574109602857d7e425a272b9d168416911a8d93bf6f8c5 ./docs/openapi.json
8a96630c36cfac5c392837c8872b20d39d72fe3ec99c7c9e09ec78f2228fd262 ./docs/API.md
58b79297607303151c1de2da74344cd4f6277bc53d80ebe04ed4567d906c963b ./docs/FLOW.md
525b03af649d0215e3b81e52ce63ebf8651f47b49503c30d0177b3e0b2714ac4 ./docs/openapi.json
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile
adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md
dd9291b12afade6fc20d66e80a5513251270e18e4c265260b2bbbe393a5a94c4 ./ha-addon/build.sh
082dfeb6326f3e1b9841ccf7f2954117ecdf0aa1a90b0aef1e6a1960dacdecbf ./ha-addon/home-assistant/README.md
10b6140e75f2641e6d196db16e2612c691666db64e3ca1cd89bce8eabc991296 ./ha-addon/home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./ha-addon/home-assistant/custom_components/gree_controller/__init__.py
3f6ef15ef58456376ac53fde7cace1ef359d6a6f7a64c5b575ad77ea6e55ccc4 ./ha-addon/home-assistant/custom_components/gree_controller/api.py
0c008329aad15f5af6328380c1afe1f49db186b37b3d1cafec4b6f4088386e8e ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
3072d7caea53f46bf68314a59e2a99779675869d2ffcd472b2bc2e188140ec0f ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
5e4aef2143e81bedb5a15dd4be5c71b64a3ab448ec6d33a20851edd098e5f529 ./ha-addon/home-assistant/custom_components/gree_controller/config_flow.py
b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/home-assistant/custom_components/gree_controller/const.py
41a8958a6fe6d10e5679c95ac34832583a2a15ecfd67d09b4e094a83d06f5c47 ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
37ce45f7e9a9fdf258ee1e97ca47599a86b33017e0c52227cdfc900f637b7f80 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
d2a18bc3de3207b9f9025507981ad3742ddc3f3728fa223baf84c7ecbb05973e ./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
@@ -30,10 +30,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
a1cfb2ec0dfc786e8d2c9adad5ad7b62abdf917c0090f06c817e78cd26115683 ./ha-addon/repository/gree-controller/CHANGELOG.md
4d0b518f7219de51cc71bfa2079d82b26d662d81e75a9596d2a0cc92e92a29e7 ./ha-addon/repository/gree-controller/CHANGELOG.md
1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.md
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
e87466f09667d3b7ba35a09e1879cd616ce62c28d99c60363ec835a3bb07ffa6 ./ha-addon/repository/gree-controller/config.yaml
f7e5325e34b2785d0d0bea657c882202d7557d313cbdc464dcdb91f702d36391 ./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
@@ -43,8 +43,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
3f7ffc2b9492593297668cf52307ad12a4a763dde3d0f8341fdb4b7298b40748 ./lang/en.json
23bfb017e2530ab8fa6c62908211016e802e990a75944a9914681ded6e2079d6 ./lang/pl.json
1bb256c32c07b8366751bbc79e8ceb284a267f3b389115263b2c67cfd73223e3 ./lang/en.json
826362354203c7f66af8acdfc367d2b10868add09b59dc5d3c756f6102692e77 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -102,13 +102,13 @@ bd7e0455c6a1501f761f0d4c00ff74833eecbd75078af9168235f90a2d9485b3 ./src/api.rs
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
b24092f34a46626d64628b510eba82193f0b203fd097f6a621b91af5d91eb571 ./src/api/auth.rs
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
a4222a29cdc90e75dffd5fb5e35c11e3556924a6489b1b31272cd81d0b141821 ./src/api/configuration.rs
27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
2d487e274c777cbde081c5803311bec449097f45bd2d021117cfe10677b39efb ./src/api/devices.rs
5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
d59b080b392b574ec94276c7b485b455d0d1abacc7de2ef29dcbfcf7d86dbdb0 ./src/api/flows.rs
a17ab0b6d683d285441be15f2af33172d7caa9a57ec8b32e59107379631a377e ./src/api/gree_cloud.rs
b9a9a68e47fdaa577c28063fa68bf95075bc355bff81addca4ee448ef70dd818 ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
3cc64b9c992805f3fbf01c0785cf29ce3121750bf30bd6420ed6e5cf2f8df422 ./src/api/history.rs
e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs
@@ -135,8 +135,8 @@ bb5048ee60053f39ce3c1203dc1f24c7047310538d2357f105a052c016d21ec4 ./src/db/flows
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
75f4f139cb268b2dfda73966b52624735380724e73b99930bffe36328620f111 ./src/engine.rs
ed5c9a0c61c6e597acb33f58593ebe02b562ddc68fa61271e1c1cd436c1003d3 ./src/engine/automations.rs
0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs
76dbf72600a48a6ba5b2273777bd8f2c8a6bd212aa236d419cc59eedcb8ab22b ./src/engine/automations.rs
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
ad53cd7be24deb759e03cbc6472522b69ebd36d8fac35398a9b19c899d3b17f2 ./src/engine/control_plan.rs
@@ -146,13 +146,13 @@ e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/g
4ec11426ef860811348f2bf1b458bc5713fe8a7794fb4c8045dd637652e8e696 ./src/engine/history.rs
41b7045877771b7c352328998cb45ce2433e7f9631fad3eb1a882ab4897dc31f ./src/engine/local_thermostat.rs
f684e6755d023478882d1689833f307f3b4cd2c66abf45f8af8315cee94d36ef ./src/engine/ownership.rs
56d64614d8ca85cec7098b1f5d2963ecdcbfb7d0a3d4f22e4424a653895b5a16 ./src/engine/polling.rs
76c1594f31af64c9a34e2555ac015e8719d42273c687b4c85b9c8f306b3c69a2 ./src/engine/polling.rs
c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/runtime.rs
48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
2e7349862beb5f844aea0f1c4cd96ab6e53d3500559d7b2562bd93da595d7c41 ./src/engine/tests.rs
af4edaf12faf294f44a8992d120a99b09edd1cbdd66981390e027d13fd6b6790 ./src/engine/tests.rs
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
fb169bcc20f95abe9892367df6dd3a683584bdbc721792d6ffb9311b81ca30d9 ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
@@ -166,7 +166,7 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb
920e028d96a14cca9f74a964bb296e68a109cfead314d3164c180201e606d151 ./src/models/automation.rs
fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/control_plan.rs
06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs
f3439a154fad8a0111db26918160901dd04fe47dd56d529490e75deaf0a78b43 ./src/models/device.rs
698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs
1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs
@@ -176,20 +176,20 @@ c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/t
bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
cca742bb2b500eb9269f69385fcf88053cd08aabf6a98b7669b8a7a2fd67df13 ./src/protocol/crypto.rs
cd0612021f8ed6c9e0823d63883c0790db09aa485583f3f3fc9f2579f2c28ea3 ./src/protocol/gree.rs
daca14e9a4dd65394add7c7753c8ae5e7e43e991323a5ca23d8788aae4428925 ./src/protocol/gree.rs
7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs
fcf5e67539c419f3e7e4533d49f41f963fa7779f099bd33c1b210fac32a78949 ./src/protocol/gree/commands.rs
91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs
4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs
05fa2e4ec133a61c23a3f4ab2ce792bbfab93feaea9f0ce0d4ccc67fc479833d ./src/protocol/gree/discovery.rs
668e3bfcc4be84d1a86909bfc18e0d3816e0343cdc0d9b43189f7b0c6e0666d4 ./src/protocol/gree/discovery.rs
0ab094f609edb99715402d8eeaa7e38cbfd2d271036aec787dcbb3be3e005720 ./src/protocol/gree/merge.rs
ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs
0a901ff258a74072af3812c5417196da1a9b8daa8a2eff30c96d038558601739 ./src/protocol/gree/polling.rs
1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.rs
e0555fac67d1dd0129ecfea6d1fd2fbeb1a73ac89ad95d1b74a7094e50e29ae4 ./src/protocol/gree/tests.rs
3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
297983059c915057869fbb4f6bcfe752f3d5bd1e93c3a2651a1bf883c739ed29 ./src/protocol/gree_cloud.rs
b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs
6258ee3888a5f9a4f8bb6cfbbac97f2621de07f9da6bcb2fa596d452f47f2e49 ./src/protocol/mod.rs
a78d73571fbd983660449fcbd7857064a1325ca12adc07e61c2f50a3208adf37 ./src/provider.rs
2a08099b456847f24a828d711c4a0aba3fc372936d9a53f0be931f6706226cc5 ./src/provider.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
580917b91441fe9b3276cf0cfc31536ba7954b6a86f4080347b93f9367a5b1ac ./src/queries/entities.rs
@@ -200,18 +200,18 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
6531bd30b81e8fe22ce973c2be3308e8ef7e3aa42285d86175b8329f2a670b61 ./web/css/styles.css
c184023f9f8a766529e047fb2798296472456a5859828e527cc3a5f180ed2b1e ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
784e2c28effb9872b582298292d3e2a6b8dd6140207342452193894d210e2f45 ./web/index.html
263d5554e62d2dd26b251f2c43a7e4ec14c8d8d790fa67e55c9bebf16f9d55c0 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
27eff2821c40395369be15e197fa830535d2327fcb27c93a0e8e65a1c62de3ad ./web/js-dynamic/bootstrap.js
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
7d81a798fb9a91125642d3bb8c938d4b1dcaef3aa580203bc48bf73308229bd7 ./web/js-dynamic/core.js
aa97e4c67a6f8fe8f473e95e678d7cb79b5756fd915270ceaae59827e6bf7ae1 ./web/js-dynamic/core.js
752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js
0f975542ae1d9bd7216123dd3718be6e9a25dbe643eb899c67024d293940fae5 ./web/js-dynamic/entities.js
7a4d02bb1ebfa3a5962c7a0b567338d460b99f30ec582fda3a7fec9f6fe54840 ./web/js-dynamic/events.js
1865ef1ed93f04fe323338ac44ef06c9b19ad6ec222d47085ce51cb4c03fe13d ./web/js-dynamic/flows.js
0e1992a7b93767f1fe4634121ae5aa43900d99f8c2614c2a4cbd5770a49b87c1 ./web/js-dynamic/entities.js
eeeef31f767f0aa3efb78bc44a2fea2b9eb86f4d4c50cf1ccae69b5ca30cdfda ./web/js-dynamic/events.js
22da18288de3282245f88f9f525d9ad29ec027571e7ea893720a9470170eccd2 ./web/js-dynamic/flows.js
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
@@ -219,7 +219,7 @@ e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.
799613c6d465cb2f92ecb57cb082d0c33729a03d88ab4ca7fa12a3cbd7e38e75 ./web/js-dynamic/realtime.js
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
f7245f0e6cb5011196704f201675ebc6c19d902b8253ffcdbf9ead46cb1c6f86 ./web/js-dynamic/settings-ui.js
bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js
672e404470bc6331aa6d658523b221659ec64222af9cc9c17ced978f725790da ./web/js-dynamic/settings.js
489ab0208c86f7a2bb0d721106f01fb5d4f39f41e94749b440a6c58e13c4de07 ./web/js/custom-chart.js
6ae536b18db756bf5eda9adb1b8a550d5a02bfc80f0b7da27fa146039f22e884 ./web/js/lang-init.js
+2 -2
View File
@@ -2,9 +2,9 @@
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
**Version: 0.14.24**
**Version: 0.15.0**
Version 0.14.24 improves public Custom Charts with single-pass language loading, English 404 fallback, pinch-to-zoom/pan and clickable metric legends, and includes the related public-chart, favicon and access-token fixes.
Version 0.15.0 adds full vertical/horizontal louver positioning across direct control, thermostat views, automations, Visual Flow and Home Assistant, while keeping legacy boolean swing payloads compatible. Direct add-on access also shows an invalid-token error only after an explicit token submission.
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
+36 -36
View File
@@ -1,27 +1,27 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
f71644d81f7f91aee248bf6f61d711a8429694dae6a785706679468cde082e28 ./Cargo.lock
ed4fcc9fcf0d11995f086047c5876787e035a539d0aa588397f90862686adab1 ./Cargo.toml
c3c16d0c76fbb80ce5752b28a1736177940670b10f44235eac5997c4e195c634 ./Cargo.lock
ad513e247ab5dbed3c91a6463cf410cf1a4ac73c0b30422bad40afb93cee0159 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
9b0edc59898f7fdfa169cacc17b9c3307fd555cd9c4ca4079b534d0ae2437d23 ./README.md
65a1319962dc7a1e7ac0db1a00219b86fae87492a30d38f4df2c896e213b7c83 ./README.md
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
dfb2736a61aa52634a0ac23fe379baf5f6c03e04837356a1c0ff93e25dc41832 ./docs/API.md
8b548d02dbdf0563f2be617a1d677459bd1959bed93a1799a01bf61bb5aed405 ./docs/FLOW.md
d9bf3a1644ed3628bd574109602857d7e425a272b9d168416911a8d93bf6f8c5 ./docs/openapi.json
8a96630c36cfac5c392837c8872b20d39d72fe3ec99c7c9e09ec78f2228fd262 ./docs/API.md
58b79297607303151c1de2da74344cd4f6277bc53d80ebe04ed4567d906c963b ./docs/FLOW.md
525b03af649d0215e3b81e52ce63ebf8651f47b49503c30d0177b3e0b2714ac4 ./docs/openapi.json
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile
adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md
dd9291b12afade6fc20d66e80a5513251270e18e4c265260b2bbbe393a5a94c4 ./ha-addon/build.sh
082dfeb6326f3e1b9841ccf7f2954117ecdf0aa1a90b0aef1e6a1960dacdecbf ./ha-addon/home-assistant/README.md
10b6140e75f2641e6d196db16e2612c691666db64e3ca1cd89bce8eabc991296 ./ha-addon/home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./ha-addon/home-assistant/custom_components/gree_controller/__init__.py
3f6ef15ef58456376ac53fde7cace1ef359d6a6f7a64c5b575ad77ea6e55ccc4 ./ha-addon/home-assistant/custom_components/gree_controller/api.py
0c008329aad15f5af6328380c1afe1f49db186b37b3d1cafec4b6f4088386e8e ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
3072d7caea53f46bf68314a59e2a99779675869d2ffcd472b2bc2e188140ec0f ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
5e4aef2143e81bedb5a15dd4be5c71b64a3ab448ec6d33a20851edd098e5f529 ./ha-addon/home-assistant/custom_components/gree_controller/config_flow.py
b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/home-assistant/custom_components/gree_controller/const.py
41a8958a6fe6d10e5679c95ac34832583a2a15ecfd67d09b4e094a83d06f5c47 ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
37ce45f7e9a9fdf258ee1e97ca47599a86b33017e0c52227cdfc900f637b7f80 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
d2a18bc3de3207b9f9025507981ad3742ddc3f3728fa223baf84c7ecbb05973e ./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
@@ -29,10 +29,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
a1cfb2ec0dfc786e8d2c9adad5ad7b62abdf917c0090f06c817e78cd26115683 ./ha-addon/repository/gree-controller/CHANGELOG.md
4d0b518f7219de51cc71bfa2079d82b26d662d81e75a9596d2a0cc92e92a29e7 ./ha-addon/repository/gree-controller/CHANGELOG.md
1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.md
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
e87466f09667d3b7ba35a09e1879cd616ce62c28d99c60363ec835a3bb07ffa6 ./ha-addon/repository/gree-controller/config.yaml
f7e5325e34b2785d0d0bea657c882202d7557d313cbdc464dcdb91f702d36391 ./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
@@ -42,8 +42,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
3f7ffc2b9492593297668cf52307ad12a4a763dde3d0f8341fdb4b7298b40748 ./lang/en.json
23bfb017e2530ab8fa6c62908211016e802e990a75944a9914681ded6e2079d6 ./lang/pl.json
1bb256c32c07b8366751bbc79e8ceb284a267f3b389115263b2c67cfd73223e3 ./lang/en.json
826362354203c7f66af8acdfc367d2b10868add09b59dc5d3c756f6102692e77 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -101,13 +101,13 @@ bd7e0455c6a1501f761f0d4c00ff74833eecbd75078af9168235f90a2d9485b3 ./src/api.rs
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
b24092f34a46626d64628b510eba82193f0b203fd097f6a621b91af5d91eb571 ./src/api/auth.rs
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
a4222a29cdc90e75dffd5fb5e35c11e3556924a6489b1b31272cd81d0b141821 ./src/api/configuration.rs
27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
2d487e274c777cbde081c5803311bec449097f45bd2d021117cfe10677b39efb ./src/api/devices.rs
5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
d59b080b392b574ec94276c7b485b455d0d1abacc7de2ef29dcbfcf7d86dbdb0 ./src/api/flows.rs
a17ab0b6d683d285441be15f2af33172d7caa9a57ec8b32e59107379631a377e ./src/api/gree_cloud.rs
b9a9a68e47fdaa577c28063fa68bf95075bc355bff81addca4ee448ef70dd818 ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
3cc64b9c992805f3fbf01c0785cf29ce3121750bf30bd6420ed6e5cf2f8df422 ./src/api/history.rs
e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs
@@ -134,8 +134,8 @@ bb5048ee60053f39ce3c1203dc1f24c7047310538d2357f105a052c016d21ec4 ./src/db/flows
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
75f4f139cb268b2dfda73966b52624735380724e73b99930bffe36328620f111 ./src/engine.rs
ed5c9a0c61c6e597acb33f58593ebe02b562ddc68fa61271e1c1cd436c1003d3 ./src/engine/automations.rs
0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs
76dbf72600a48a6ba5b2273777bd8f2c8a6bd212aa236d419cc59eedcb8ab22b ./src/engine/automations.rs
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
ad53cd7be24deb759e03cbc6472522b69ebd36d8fac35398a9b19c899d3b17f2 ./src/engine/control_plan.rs
@@ -145,13 +145,13 @@ e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/g
4ec11426ef860811348f2bf1b458bc5713fe8a7794fb4c8045dd637652e8e696 ./src/engine/history.rs
41b7045877771b7c352328998cb45ce2433e7f9631fad3eb1a882ab4897dc31f ./src/engine/local_thermostat.rs
f684e6755d023478882d1689833f307f3b4cd2c66abf45f8af8315cee94d36ef ./src/engine/ownership.rs
56d64614d8ca85cec7098b1f5d2963ecdcbfb7d0a3d4f22e4424a653895b5a16 ./src/engine/polling.rs
76c1594f31af64c9a34e2555ac015e8719d42273c687b4c85b9c8f306b3c69a2 ./src/engine/polling.rs
c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/runtime.rs
48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
2e7349862beb5f844aea0f1c4cd96ab6e53d3500559d7b2562bd93da595d7c41 ./src/engine/tests.rs
af4edaf12faf294f44a8992d120a99b09edd1cbdd66981390e027d13fd6b6790 ./src/engine/tests.rs
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
fb169bcc20f95abe9892367df6dd3a683584bdbc721792d6ffb9311b81ca30d9 ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
@@ -165,7 +165,7 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb
920e028d96a14cca9f74a964bb296e68a109cfead314d3164c180201e606d151 ./src/models/automation.rs
fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/control_plan.rs
06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs
f3439a154fad8a0111db26918160901dd04fe47dd56d529490e75deaf0a78b43 ./src/models/device.rs
698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs
1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs
@@ -175,20 +175,20 @@ c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/t
bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
cca742bb2b500eb9269f69385fcf88053cd08aabf6a98b7669b8a7a2fd67df13 ./src/protocol/crypto.rs
cd0612021f8ed6c9e0823d63883c0790db09aa485583f3f3fc9f2579f2c28ea3 ./src/protocol/gree.rs
daca14e9a4dd65394add7c7753c8ae5e7e43e991323a5ca23d8788aae4428925 ./src/protocol/gree.rs
7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs
fcf5e67539c419f3e7e4533d49f41f963fa7779f099bd33c1b210fac32a78949 ./src/protocol/gree/commands.rs
91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs
4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs
05fa2e4ec133a61c23a3f4ab2ce792bbfab93feaea9f0ce0d4ccc67fc479833d ./src/protocol/gree/discovery.rs
668e3bfcc4be84d1a86909bfc18e0d3816e0343cdc0d9b43189f7b0c6e0666d4 ./src/protocol/gree/discovery.rs
0ab094f609edb99715402d8eeaa7e38cbfd2d271036aec787dcbb3be3e005720 ./src/protocol/gree/merge.rs
ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs
0a901ff258a74072af3812c5417196da1a9b8daa8a2eff30c96d038558601739 ./src/protocol/gree/polling.rs
1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.rs
e0555fac67d1dd0129ecfea6d1fd2fbeb1a73ac89ad95d1b74a7094e50e29ae4 ./src/protocol/gree/tests.rs
3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
297983059c915057869fbb4f6bcfe752f3d5bd1e93c3a2651a1bf883c739ed29 ./src/protocol/gree_cloud.rs
b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs
6258ee3888a5f9a4f8bb6cfbbac97f2621de07f9da6bcb2fa596d452f47f2e49 ./src/protocol/mod.rs
a78d73571fbd983660449fcbd7857064a1325ca12adc07e61c2f50a3208adf37 ./src/provider.rs
2a08099b456847f24a828d711c4a0aba3fc372936d9a53f0be931f6706226cc5 ./src/provider.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
580917b91441fe9b3276cf0cfc31536ba7954b6a86f4080347b93f9367a5b1ac ./src/queries/entities.rs
@@ -199,18 +199,18 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
6531bd30b81e8fe22ce973c2be3308e8ef7e3aa42285d86175b8329f2a670b61 ./web/css/styles.css
c184023f9f8a766529e047fb2798296472456a5859828e527cc3a5f180ed2b1e ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
784e2c28effb9872b582298292d3e2a6b8dd6140207342452193894d210e2f45 ./web/index.html
263d5554e62d2dd26b251f2c43a7e4ec14c8d8d790fa67e55c9bebf16f9d55c0 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
27eff2821c40395369be15e197fa830535d2327fcb27c93a0e8e65a1c62de3ad ./web/js-dynamic/bootstrap.js
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
7d81a798fb9a91125642d3bb8c938d4b1dcaef3aa580203bc48bf73308229bd7 ./web/js-dynamic/core.js
aa97e4c67a6f8fe8f473e95e678d7cb79b5756fd915270ceaae59827e6bf7ae1 ./web/js-dynamic/core.js
752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js
0f975542ae1d9bd7216123dd3718be6e9a25dbe643eb899c67024d293940fae5 ./web/js-dynamic/entities.js
7a4d02bb1ebfa3a5962c7a0b567338d460b99f30ec582fda3a7fec9f6fe54840 ./web/js-dynamic/events.js
1865ef1ed93f04fe323338ac44ef06c9b19ad6ec222d47085ce51cb4c03fe13d ./web/js-dynamic/flows.js
0e1992a7b93767f1fe4634121ae5aa43900d99f8c2614c2a4cbd5770a49b87c1 ./web/js-dynamic/entities.js
eeeef31f767f0aa3efb78bc44a2fea2b9eb86f4d4c50cf1ccae69b5ca30cdfda ./web/js-dynamic/events.js
22da18288de3282245f88f9f525d9ad29ec027571e7ea893720a9470170eccd2 ./web/js-dynamic/flows.js
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
@@ -218,7 +218,7 @@ e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.
799613c6d465cb2f92ecb57cb082d0c33729a03d88ab4ca7fa12a3cbd7e38e75 ./web/js-dynamic/realtime.js
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
f7245f0e6cb5011196704f201675ebc6c19d902b8253ffcdbf9ead46cb1c6f86 ./web/js-dynamic/settings-ui.js
bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js
672e404470bc6331aa6d658523b221659ec64222af9cc9c17ced978f725790da ./web/js-dynamic/settings.js
489ab0208c86f7a2bb0d721106f01fb5d4f39f41e94749b440a6c58e13c4de07 ./web/js/custom-chart.js
6ae536b18db756bf5eda9adb1b8a550d5a02bfc80f0b7da27fa146039f22e884 ./web/js/lang-init.js
+18 -9
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.14.24**.
HTTP and WebSocket API for GREE Controller **0.15.0**.
[← Main documentation](../README.md)
@@ -240,7 +240,7 @@ Response:
{
"status": "ok",
"name": "gree-controller",
"version": "0.14.24",
"version": "0.15.0",
"uptime_seconds": 1234,
"control_ready": true,
"time": "2026-08-30T06:54:00Z"
@@ -303,7 +303,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.14.24",
"version": "0.15.0",
"uptime_seconds": 1234,
"auth_required": false,
"control_ready": true,
@@ -360,7 +360,8 @@ A device response contains:
| `mode` | string | `auto`, `cool`, `dry`, `fan`, `heat`. |
| `target_temperature` | number | Last known unit setpoint. |
| `fan_speed` | integer | `0..5`; `0` is Auto. |
| `swing_vertical`, `swing_horizontal` | boolean | Swing state. |
| `swing_vertical` | integer | Vertical louver (`SwUpDn`): `0` off/default, `1` full range, `2..6` fixed positions, `7..11` partial swing ranges. |
| `swing_horizontal` | integer | Horizontal louver (`SwingLfRig`): `0` off/default, `1` full range, `2..6` fixed positions. |
| `quiet`, `turbo`, `light`, `air`, `xfan`, `health`, `sleep` | boolean | Optional GREE features. |
| `supports_*` | boolean/null | Capability learned from device status. |
| `current_temperature` | number/null | GREE indoor temperature. |
@@ -484,8 +485,8 @@ All fields optional; at least one meaningful field should be sent:
"mode": "cool",
"target_temperature": 22,
"fan_speed": 3,
"swing_vertical": true,
"swing_horizontal": false,
"swing_vertical": 2,
"swing_horizontal": 1,
"quiet": false,
"turbo": false,
"light": true,
@@ -496,6 +497,8 @@ All fields optional; at least one meaningful field should be sent:
}
```
Louver command values follow the raw GREE positions. For backward compatibility, legacy booleans are still accepted (`false` = `0`, `true` = `1`).
Rules:
- modes: `auto`, `cool`, `dry`, `fan`, `heat`,
@@ -1691,11 +1694,17 @@ Additional Flow condition blocks are `house_mode`, `device_state`, `zone_state`,
## Granular louver positions — 0.15.0
Vertical (`swing_vertical`) and horizontal (`swing_horizontal`) controls now carry the raw GREE louver position instead of only on/off. Vertical supports `0..11`; horizontal supports `0..6`. The first values map to Off, Full range (Auto) and the fixed positions shown by GREE remotes/apps; vertical `7..11` additionally represent partial swing ranges.
Manual unit control and thermostat cards use position dropdowns. Legacy direct-device automations, `device_action` / `zone_thermostat` Flow actions and `device_state` conditions expose the same positions. The bundled Home Assistant direct and zone climate entities expose granular swing modes while retaining `off` and `on` as the first two modes for compatibility. Louver changes remain auxiliary one-shot commands and do not take thermostat ownership.
The command API continues accepting legacy boolean swing payloads (`false` = `0`, `true` = `1`) so existing clients can migrate without an immediate break.
## Swing control across manual, thermostat and automation surfaces — 0.14.2
Local device control exposes vertical (`swing_vertical`) and horizontal (`swing_horizontal`) swing independently. Thermostat zone UI and the bundled Home Assistant zone climate entity expose the same auxiliary controls when the assigned device reports support; changing swing does not hand thermostat ownership to manual control.
Legacy direct-device automations accept optional `swing_vertical` and `swing_horizontal` action fields. Visual Flow exposes both axes in `device_action` and `zone_thermostat` actions and in device-state conditions. A `zone_thermostat` Flow using swing compiles to an automation rather than a native schedule so the auxiliary command is preserved. Swing remains outside the thermostat's continuously regulated fan/temperature outputs.
Version 0.14.2 introduced independent vertical (`swing_vertical`) and horizontal (`swing_horizontal`) boolean swing controls across direct control, thermostat zones, legacy automations, Visual Flow and Home Assistant zone climates. Version 0.15.0 extends those fields to granular louver positions while preserving boolean command compatibility.
## Split/multisplit installations and energy comparison — 0.14.1
+4 -4
View File
@@ -4,7 +4,7 @@ Flow is an authoring/orchestration layer. It does not replace the thermostat, sc
## Compilation
- `weekday + time_range -> zone_thermostat` with schedule-compatible settings compiles to a native `Schedule`; selecting vertical or horizontal swing makes the action compile as an `Automation` so the swing command is not lost.
- `weekday + time_range -> zone_thermostat` with schedule-compatible settings compiles to a native `Schedule`; selecting a vertical or horizontal louver position makes the action compile as an `Automation` so the louver command is not lost.
- Richer DAGs compile to native `Automation` rows with `trigger_kind = "flow"`.
- Generated rows keep `flow_id` and `flow_node_id`, use stable `flow-<stable-unique-id>` names, and are read-only in the legacy editors.
- The Flow source graph plus all of its generated schedule/automation rows are replaced in one SQLite transaction.
@@ -14,11 +14,11 @@ Flow is an authoring/orchestration layer. It does not replace the thermostat, sc
## Existing control domains remain authoritative
- `zone_thermostat` writes the existing persistent Zone intent and wakes the normal thermostat cycle. It can also issue one-shot vertical/horizontal swing commands to the assigned unit; these auxiliary fields do not take over thermostat ownership.
- `zone_thermostat` writes the existing persistent Zone intent and wakes the normal thermostat cycle. It can also issue one-shot granular vertical/horizontal louver commands to the assigned unit; these auxiliary fields do not take over thermostat ownership.
- `group_action` calls the existing `control_group` path, including custom group targets.
- `device_action` uses the existing `DeviceCommand` / automatic-device path.
- Thermostat hysteresis, compressor protection, schedule hand-back, manual/local ownership and Temporary Quick Thermostat keep their existing priority.
- GREE fan/quiet/sleep and dry/fan HVAC modes are suppressed on thermostat-assigned devices when they would fight the regulator. Device-only features such as light/turbo/swing/air/xfan/health remain available through the normal command path.
- GREE fan/quiet/sleep and dry/fan HVAC modes are suppressed on thermostat-assigned devices when they would fight the regulator. Device-only features such as light/turbo/louver/air/xfan/health remain available through the normal command path.
## Concurrency invariants
@@ -61,7 +61,7 @@ Dry-run never mutates thermostats, devices, groups, schedules or automations. Fo
Condition/source blocks include weekday, time range, date range, optional 5-field CRON triggers, application Night mode, outdoor/device/zone temperature, house mode, device state, thermostat/zone state, group state, Home Assistant state/numeric/attribute/availability, rolling mean/median windows, numeric oscillation detection, a diagnostic constant, and `shared_input`. Stateful gates include `stable_for`, bounded `state_duration`, `on_change` (condition-result or raw-value edge), `rate_limit` (maximum successful executions in a rolling period), and `delay`. Their runtime state, together with rolling statistics and oscillation samples, is persisted in the generated automation payload so restarts do not silently reset timing/history. Editing a stateful block resets only that block's incompatible runtime state. Flow state continues to be observed while the action itself is in cooldown, preventing stale continuity/change state. A `rate_limit` block must be placed directly before an action and consumes quota only after a successful execution; suppressed or failed actions do not consume it. Shared inputs are configured centrally in Home Assistant / Sensors and referenced by ID. Shared inputs store reusable value sources only; operators and comparison values are configured exclusively in each Flow reference. Logic blocks are AND, OR and NOT.
Actions include thermostat zone, GREE device, climate group, and generic Home Assistant service calls (`domain`, `service`, optional `entity_id`, JSON service data). Direct GREE actions expose power, HVAC mode, target temperature, fan speed, vertical/horizontal swing, quiet, turbo, light, air, xfan, health and sleep.
Actions include thermostat zone, GREE device, climate group, and generic Home Assistant service calls (`domain`, `service`, optional `entity_id`, JSON service data). Direct GREE actions expose power, HVAC mode, target temperature, fan speed, vertical/horizontal louver position, quiet, turbo, light, air, xfan, health and sleep.
## Built-in presets
+71 -21
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -8,7 +8,7 @@ ha-addon/home-assistant/custom_components/gree_controller/
It creates HA entities for physical units, thermostat zones, whole-house controls and climate groups, while sending every command to the standalone Rust controller. Home Assistant therefore remains a client and UDP/AES GREE communication stays outside HA.
The climate proxy supports power/turn on/off, HVAC modes, target temperature, fan mode, vertical swing and horizontal swing.
The climate proxy supports power/turn on/off, HVAC modes, target temperature, fan mode, granular vertical louver positions and granular horizontal louver positions. `off`/`on` remain the first two swing modes for compatibility, followed by fixed positions and the supported vertical partial-swing ranges.
Automation plan:
@@ -39,6 +39,40 @@ HA_TO_MODE = {value: key for key, value in MODE_TO_HA.items()}
FAN_TO_NAME = {0: "auto", 1: "low", 2: "medium_low", 3: "medium", 4: "medium_high", 5: "high"}
NAME_TO_FAN = {value: key for key, value in FAN_TO_NAME.items()}
VERTICAL_SWING_TO_VALUE = {
SWING_OFF: 0,
SWING_ON: 1,
"fixed_upper": 2,
"fixed_upper_middle": 3,
"fixed_middle": 4,
"fixed_lower_middle": 5,
"fixed_lower": 6,
"swing_upper": 7,
"swing_upper_middle": 8,
"swing_middle": 9,
"swing_lower_middle": 10,
"swing_lower": 11,
}
HORIZONTAL_SWING_TO_VALUE = {
SWING_OFF: 0,
SWING_ON: 1,
"fixed_left": 2,
"fixed_left_middle": 3,
"fixed_middle": 4,
"fixed_right_middle": 5,
"fixed_right": 6,
}
VALUE_TO_VERTICAL_SWING = {value: mode for mode, value in VERTICAL_SWING_TO_VALUE.items()}
VALUE_TO_HORIZONTAL_SWING = {value: mode for mode, value in HORIZONTAL_SWING_TO_VALUE.items()}
def _louver_mode(raw: Any, modes: dict[int, str]) -> str:
try:
value = int(raw)
except (TypeError, ValueError):
value = 0
return modes.get(value, SWING_OFF)
async def async_setup_entry(
hass: HomeAssistant,
@@ -132,8 +166,8 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat
_attr_target_temperature_step = 1.0
_attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT, HVACMode.DRY, HVACMode.FAN_ONLY]
_attr_fan_modes = list(NAME_TO_FAN)
_attr_swing_modes = [SWING_OFF, SWING_ON]
_attr_swing_horizontal_modes = [SWING_OFF, SWING_ON]
_attr_swing_modes = list(VERTICAL_SWING_TO_VALUE)
_attr_swing_horizontal_modes = list(HORIZONTAL_SWING_TO_VALUE)
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
@@ -200,11 +234,11 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat
@property
def swing_mode(self) -> str:
return SWING_ON if self._device.get("swing_vertical", False) else SWING_OFF
return _louver_mode(self._device.get("swing_vertical", 0), VALUE_TO_VERTICAL_SWING)
@property
def swing_horizontal_mode(self) -> str:
return SWING_ON if self._device.get("swing_horizontal", False) else SWING_OFF
return _louver_mode(self._device.get("swing_horizontal", 0), VALUE_TO_HORIZONTAL_SWING)
@property
def extra_state_attributes(self) -> dict[str, Any]:
@@ -219,6 +253,8 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat
"air": bool(device.get("air", False)),
"health": bool(device.get("health", False)),
"sleep": bool(device.get("sleep", False)),
"vertical_louver_position": int(device.get("swing_vertical", 0) or 0),
"horizontal_louver_position": int(device.get("swing_horizontal", 0) or 0),
"last_seen": device.get("last_seen"),
}
@@ -250,10 +286,14 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat
await self._command({"fan_speed": NAME_TO_FAN[fan_mode]})
async def async_set_swing_mode(self, swing_mode: str) -> None:
await self._command({"swing_vertical": swing_mode == SWING_ON})
value = VERTICAL_SWING_TO_VALUE.get(swing_mode)
if value is not None:
await self._command({"swing_vertical": value})
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
await self._command({"swing_horizontal": swing_horizontal_mode == SWING_ON})
value = HORIZONTAL_SWING_TO_VALUE.get(swing_horizontal_mode)
if value is not None:
await self._command({"swing_horizontal": value})
class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], ClimateEntity):
"""Full climate entity for a controller thermostat zone."""
@@ -266,8 +306,8 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
_attr_target_temperature_step = 0.5
_attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT]
_attr_preset_modes = ["auto", "comfort", "sleep", "away"]
_attr_swing_modes = [SWING_OFF, SWING_ON]
_attr_swing_horizontal_modes = [SWING_OFF, SWING_ON]
_attr_swing_modes = list(VERTICAL_SWING_TO_VALUE)
_attr_swing_horizontal_modes = list(HORIZONTAL_SWING_TO_VALUE)
def __init__(self, coordinator: GreeControllerCoordinator, zone_id: str) -> None:
super().__init__(coordinator)
@@ -343,11 +383,11 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
@property
def swing_mode(self) -> str:
return SWING_ON if self._device.get("swing_vertical", False) else SWING_OFF
return _louver_mode(self._device.get("swing_vertical", 0), VALUE_TO_VERTICAL_SWING)
@property
def swing_horizontal_mode(self) -> str:
return SWING_ON if self._device.get("swing_horizontal", False) else SWING_OFF
return _louver_mode(self._device.get("swing_horizontal", 0), VALUE_TO_HORIZONTAL_SWING)
@property
def extra_state_attributes(self) -> dict[str, Any]:
@@ -378,6 +418,8 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
"actual_mode": zone.get("actual_mode"),
"actual_setpoint": zone.get("actual_setpoint"),
"current_schedule": zone.get("current_schedule_name"),
"vertical_louver_position": int(self._device.get("swing_vertical", 0) or 0),
"horizontal_louver_position": int(self._device.get("swing_horizontal", 0) or 0),
}
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
@@ -406,17 +448,15 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
async def async_set_swing_mode(self, swing_mode: str) -> None:
device_id = str(self._zone.get("device_id") or "").strip()
if device_id:
await self.coordinator.async_device_command(
device_id, {"swing_vertical": swing_mode == SWING_ON}
)
value = VERTICAL_SWING_TO_VALUE.get(swing_mode)
if device_id and value is not None:
await self.coordinator.async_device_command(device_id, {"swing_vertical": value})
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
device_id = str(self._zone.get("device_id") or "").strip()
if device_id:
await self.coordinator.async_device_command(
device_id, {"swing_horizontal": swing_horizontal_mode == SWING_ON}
)
value = HORIZONTAL_SWING_TO_VALUE.get(swing_horizontal_mode)
if device_id and value is not None:
await self.coordinator.async_device_command(device_id, {"swing_horizontal": value})
async def async_turn_on(self) -> None:
await self.coordinator.async_zone_control(self._zone_id, {"power": True})
@@ -67,6 +67,10 @@ class GreeControllerCoordinator(DataUpdateCoordinator[dict[str, dict]]):
expected["target_temperature"] = float(int(value + 0.5))
if "fan_speed" in expected:
expected["fan_speed"] = min(5, max(0, int(expected["fan_speed"])))
if "swing_vertical" in expected:
expected["swing_vertical"] = min(11, max(0, int(expected["swing_vertical"])))
if "swing_horizontal" in expected:
expected["swing_horizontal"] = min(6, max(0, int(expected["swing_horizontal"])))
return expected
def _overlay_pending_device_commands(self, devices: dict[str, dict[str, Any]]) -> None:
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.14.24",
"version": "0.15.0",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
@@ -1,5 +1,11 @@
# Changelog
## 0.15.0
- Adds full vertical and horizontal louver positions to manual unit control and thermostat cards, with compact dropdowns that preserve the existing view layout.
- Extends legacy automations, Visual Flow and the bundled Home Assistant climate entities with granular louver positions; legacy boolean swing API payloads remain accepted.
- Direct add-on access no longer shows `Invalid access token` on initial page load; that validation message appears only after a token is submitted.
## 0.14.24
- Public Custom Charts fetch chart metadata before language assets, so a chart loads only its selected language pack; missing chart IDs fall back to English.
@@ -1,5 +1,5 @@
name: "GREE Controller"
version: "0.14.24"
version: "0.15.0"
slug: "gree_controller"
description: "Local GREE HVAC controller with Web UI and Home Assistant integration"
url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/"
+23 -5
View File
@@ -1000,7 +1000,7 @@
"flow.available": "available",
"flow.haAvailableHint": "This condition is true only when the Home Assistant entity returns a state other than unknown/unavailable. Connection errors fail closed as false. Useful as a guard before NOT and safety logic.",
"flow.deviceOptions": "Advanced GREE options",
"flow.deviceOptionsHint": "These fields use the existing DeviceCommand. Fan/quiet/sleep and dry/fan HVAC modes are blocked on a thermostat-assigned device when they would fight the regulator; use the Thermostat action there instead.",
"flow.deviceOptionsHint": "These fields use the existing DeviceCommand. Louver positions are one-shot auxiliary settings; fan/quiet/sleep and dry/fan HVAC modes are blocked on a thermostat-assigned device when they would fight the regulator; use the Thermostat action there instead.",
"settings.haConnection": "Home Assistant connection",
"settings.haConnectionHint": "Server address and credentials used by all Home Assistant features.",
"settings.haThermostatSources": "Thermostat temperature sources",
@@ -1327,10 +1327,28 @@
"energy.maxTargets": "You can select up to 8 devices or installations.",
"energy.period": "Data period",
"energy.periodHint": "Time range used for energy bars and totals.",
"devices.swingVertical": "Vertical swing",
"devices.swingHorizontal": "Horizontal swing",
"flow.thermostatDeviceOptions": "Unit swing control",
"flow.thermostatDeviceOptionsHint": "Vertical and horizontal swing are sent to the unit as independent settings and do not change thermostat logic.",
"devices.swingVertical": "Vertical louver",
"devices.swingHorizontal": "Horizontal louver",
"louver.off": "Off",
"louver.fullRangeAuto": "Full range (Auto)",
"louver.advancedSwingRanges": "Additional swing ranges",
"louver.vertical.fixedTop": "Fixed: Top",
"louver.vertical.fixedUpperMiddle": "Fixed: Upper-middle",
"louver.vertical.fixedMiddle": "Fixed: Middle",
"louver.vertical.fixedLowerMiddle": "Fixed: Lower-middle",
"louver.vertical.fixedBottom": "Fixed: Bottom",
"louver.vertical.swingTop": "Swing: Top",
"louver.vertical.swingUpperMiddle": "Swing: Upper-middle",
"louver.vertical.swingMiddle": "Swing: Middle",
"louver.vertical.swingLowerMiddle": "Swing: Lower-middle",
"louver.vertical.swingBottom": "Swing: Bottom",
"louver.horizontal.fixedLeft": "Fixed: Left",
"louver.horizontal.fixedLeftMiddle": "Fixed: Left-middle",
"louver.horizontal.fixedMiddle": "Fixed: Middle",
"louver.horizontal.fixedRightMiddle": "Fixed: Right-middle",
"louver.horizontal.fixedRight": "Fixed: Right",
"flow.thermostatDeviceOptions": "Unit louver control",
"flow.thermostatDeviceOptionsHint": "Vertical and horizontal louver positions are sent to the unit independently and do not change thermostat logic.",
"history.tabNetwork": "Pings",
"history.networkTarget": "Endpoint",
"history.allNetworkTargets": "All endpoints",
+22 -4
View File
@@ -1000,7 +1000,7 @@
"flow.available": "dostępna",
"flow.haAvailableHint": "Warunek jest prawdziwy tylko wtedy, gdy encja Home Assistant odpowiada stanem innym niż unknown/unavailable. Błąd połączenia jest traktowany bezpiecznie jako false. Przydatne jako bramka przed NOT i logiką bezpieczeństwa.",
"flow.deviceOptions": "Zaawansowane opcje GREE",
"flow.deviceOptionsHint": "Te pola używają istniejącego DeviceCommand. Fan/quiet/sleep oraz tryby dry/fan są blokowane dla urządzenia przypisanego do termostatu, gdy mogłyby walczyć z regulatorem; użyj wtedy akcji Termostat.",
"flow.deviceOptionsHint": "Te pola używają istniejącego DeviceCommand. Pozycje żaluzji są jednorazowymi ustawieniami pomocniczymi; fan/quiet/sleep oraz tryby dry/fan są blokowane dla urządzenia przypisanego do termostatu, gdy mogłyby walczyć z regulatorem; użyj wtedy akcji Termostat.",
"settings.haConnection": "Połączenie z Home Assistant",
"settings.haConnectionHint": "Adres serwera i dane dostępu używane przez wszystkie funkcje Home Assistant.",
"settings.haThermostatSources": "Źródła temperatury sterowania",
@@ -1327,10 +1327,28 @@
"energy.maxTargets": "Możesz wybrać maksymalnie 8 urządzeń lub instalacji.",
"energy.period": "Okres danych",
"energy.periodHint": "Zakres czasu używany do słupków i sum energii.",
"devices.swingVertical": "Swing pionowy",
"devices.swingHorizontal": "Swing poziomy",
"devices.swingVertical": "Żaluzja pionowa",
"devices.swingHorizontal": "Żaluzja pozioma",
"louver.off": "Wyłączony",
"louver.fullRangeAuto": "Pełny zakres (Auto)",
"louver.advancedSwingRanges": "Dodatkowe zakresy ruchu",
"louver.vertical.fixedTop": "Stała: Góra",
"louver.vertical.fixedUpperMiddle": "Stała: Środek-góra",
"louver.vertical.fixedMiddle": "Stała: Środek",
"louver.vertical.fixedLowerMiddle": "Stała: Środek-dół",
"louver.vertical.fixedBottom": "Stała: Dół",
"louver.vertical.swingTop": "Ruch: Góra",
"louver.vertical.swingUpperMiddle": "Ruch: Środek-góra",
"louver.vertical.swingMiddle": "Ruch: Środek",
"louver.vertical.swingLowerMiddle": "Ruch: Środek-dół",
"louver.vertical.swingBottom": "Ruch: Dół",
"louver.horizontal.fixedLeft": "Stała: Lewo",
"louver.horizontal.fixedLeftMiddle": "Stała: Lewo-środek",
"louver.horizontal.fixedMiddle": "Stała: Środek",
"louver.horizontal.fixedRightMiddle": "Stała: Środek-prawo",
"louver.horizontal.fixedRight": "Stała: Prawo",
"flow.thermostatDeviceOptions": "Sterowanie żaluzjami jednostki",
"flow.thermostatDeviceOptionsHint": "Swing pionowy i poziomy są wysyłane do jednostki jako niezależne ustawienia i nie zmieniają logiki termostatu.",
"flow.thermostatDeviceOptionsHint": "Pozycje żaluzji pionowej i poziomej są wysyłane do jednostki niezależnie i nie zmieniają logiki termostatu.",
"history.tabNetwork": "Pingi",
"history.networkTarget": "Punkt pomiaru",
"history.allNetworkTargets": "Wszystkie punkty",
+2 -2
View File
@@ -594,8 +594,8 @@ fn sanitize_imported_device(device: &mut Device, now: chrono::DateTime<Utc>) {
device.mode = "cool".into();
device.target_temperature = 23.0;
device.fan_speed = 0;
device.swing_vertical = false;
device.swing_horizontal = false;
device.swing_vertical = 0;
device.swing_horizontal = 0;
device.quiet = false;
device.turbo = false;
device.light = false;
+2 -2
View File
@@ -116,8 +116,8 @@ async fn add_device(
mode: "cool".into(),
target_temperature: 24.0,
fan_speed: 0,
swing_vertical: false,
swing_horizontal: false,
swing_vertical: 0,
swing_horizontal: 0,
quiet: false,
quiet_wire_value: None,
turbo: false,
+22 -8
View File
@@ -53,6 +53,15 @@ fn flow_u8(config: &Value, key: &str) -> Option<u8> {
fn flow_bool(config: &Value, key: &str) -> Option<bool> {
config.get(key).and_then(Value::as_bool)
}
fn flow_louver_position(config: &Value, key: &str) -> Option<u8> {
match config.get(key) {
Some(Value::Bool(value)) => Some(u8::from(*value)),
Some(value) => value
.as_u64()
.and_then(|raw| u8::try_from(raw).ok()),
None => None,
}
}
fn generated_flow_name(flow_id: &str, action_node_id: &str) -> String {
let digest = Sha256::digest(format!("{flow_id}:{action_node_id}").as_bytes());
@@ -1108,8 +1117,8 @@ fn compile_flow(
&& preset_for_schedule != "auto"
&& mode_for_schedule == "auto"
&& flow_bool(&action_node.config, "power").is_none()
&& flow_bool(&action_node.config, "swing_vertical").is_none()
&& flow_bool(&action_node.config, "swing_horizontal").is_none();
&& flow_louver_position(&action_node.config, "swing_vertical").is_none()
&& flow_louver_position(&action_node.config, "swing_horizontal").is_none();
if schedule_only {
let zone_id = flow_string(&action_node.config, "zone_id")
.ok_or_else(|| AppError::BadRequest("thermostat block needs a zone".into()))?;
@@ -1287,8 +1296,11 @@ fn compile_flow(
}
item.action.mode = Some(mode);
}
item.action.swing_vertical = flow_bool(&action_node.config, "swing_vertical");
item.action.swing_horizontal = flow_bool(&action_node.config, "swing_horizontal");
item.action.swing_vertical =
flow_louver_position(&action_node.config, "swing_vertical");
item.action.swing_horizontal =
flow_louver_position(&action_node.config, "swing_horizontal");
engine::validate_command(&item.action)?;
}
"device_action" => {
let id = flow_string(&action_node.config, "device_id")
@@ -1304,8 +1316,10 @@ fn compile_flow(
item.action.target_temperature =
flow_f64(&action_node.config, "target_temperature");
item.action.fan_speed = flow_u8(&action_node.config, "fan_speed");
item.action.swing_vertical = flow_bool(&action_node.config, "swing_vertical");
item.action.swing_horizontal = flow_bool(&action_node.config, "swing_horizontal");
item.action.swing_vertical =
flow_louver_position(&action_node.config, "swing_vertical");
item.action.swing_horizontal =
flow_louver_position(&action_node.config, "swing_horizontal");
item.action.quiet = flow_bool(&action_node.config, "quiet");
item.action.turbo = flow_bool(&action_node.config, "turbo");
item.action.light = flow_bool(&action_node.config, "light");
@@ -1733,8 +1747,8 @@ fn dry_run_block_reason(
command.mode = flow_string(&action.config, "mode");
command.target_temperature = flow_f64(&action.config, "target_temperature");
command.fan_speed = flow_u8(&action.config, "fan_speed");
command.swing_vertical = flow_bool(&action.config, "swing_vertical");
command.swing_horizontal = flow_bool(&action.config, "swing_horizontal");
command.swing_vertical = flow_louver_position(&action.config, "swing_vertical");
command.swing_horizontal = flow_louver_position(&action.config, "swing_horizontal");
command.quiet = flow_bool(&action.config, "quiet");
command.turbo = flow_bool(&action.config, "turbo");
command.light = flow_bool(&action.config, "light");
+2 -2
View File
@@ -300,8 +300,8 @@ async fn add_gree_cloud_device(
mode: "cool".into(),
target_temperature: 24.0,
fan_speed: 0,
swing_vertical: false,
swing_horizontal: false,
swing_vertical: 0,
swing_horizontal: 0,
quiet: false,
quiet_wire_value: None,
turbo: false,
+1
View File
@@ -4,6 +4,7 @@ use crate::{
models::{
Automation, AutomationPlanRule, ClimateGroup, ConnectionStatus, ConnectionType,
ControlPlan, ControlPlanEvent, Device, DeviceCommand, DeviceGroup, EnergyReading,
HORIZONTAL_SWING_MAX, VERTICAL_SWING_MAX,
EnergySourcePreference, GroupControlPatch, HaReading, NetworkReading, NightModeSettings,
Reading, RuntimeSettings, Schedule, TemporaryQuickThermostat, Zone, ZoneControlPlan,
ZoneReading,
+20 -2
View File
@@ -264,6 +264,18 @@ fn flow_compare_value(actual: &Value, operator: &str, expected: &Value) -> bool
if operator == "neq" { !equal } else { equal }
}
fn flow_louver_compat_value(field: &str, value: Value) -> Value {
if !matches!(field, "swing_vertical" | "swing_horizontal") {
return value;
}
match value {
Value::Bool(value) => json!(u8::from(value)),
Value::String(value) if value.eq_ignore_ascii_case("true") => json!(1),
Value::String(value) if value.eq_ignore_ascii_case("false") => json!(0),
value => value,
}
}
fn flow_device_state_value(device: &Device, field: &str) -> Option<Value> {
Some(match field {
"enabled" => json!(device.enabled),
@@ -603,8 +615,14 @@ async fn flow_leaf_observation(
"device_state" => {
let id = c.get("device_id").and_then(Value::as_str).unwrap_or("");
let field = c.get("field").and_then(Value::as_str).unwrap_or("");
let actual = override_value.unwrap_or_else(|| devices.iter().find(|device| device.id == id).and_then(|device| flow_device_state_value(device, field)).unwrap_or(Value::Null));
let expected = c.get("value").cloned().unwrap_or(Value::Null);
let actual = flow_louver_compat_value(
field,
override_value.unwrap_or_else(|| devices.iter().find(|device| device.id == id).and_then(|device| flow_device_state_value(device, field)).unwrap_or(Value::Null)),
);
let expected = flow_louver_compat_value(
field,
c.get("value").cloned().unwrap_or(Value::Null),
);
(flow_compare_value(&actual, c.get("operator").and_then(Value::as_str).unwrap_or("eq"), &expected), actual)
}
"zone_state" => {
+14
View File
@@ -392,6 +392,20 @@ pub(crate) fn validate_command(command: &DeviceCommand) -> Result<(), AppError>
));
}
}
if let Some(value) = command.swing_vertical {
if value > VERTICAL_SWING_MAX {
return Err(AppError::BadRequest(format!(
"vertical louver position must be between 0 and {VERTICAL_SWING_MAX}"
)));
}
}
if let Some(value) = command.swing_horizontal {
if value > HORIZONTAL_SWING_MAX {
return Err(AppError::BadRequest(format!(
"horizontal louver position must be between 0 and {HORIZONTAL_SWING_MAX}"
)));
}
}
if let Some(value) = &command.mode {
if !matches!(value.as_str(), "auto" | "cool" | "dry" | "fan" | "heat") {
return Err(AppError::BadRequest("unsupported HVAC mode".into()));
+2 -2
View File
@@ -708,8 +708,8 @@ mod tests {
));
assert!(!automation_action_conflicts_with_thermostat(
&DeviceCommand {
swing_vertical: Some(true),
swing_horizontal: Some(false),
swing_vertical: Some(1),
swing_horizontal: Some(0),
..Default::default()
}
));
+74 -10
View File
@@ -89,6 +89,41 @@ fn default_temperature_step() -> f64 {
1.0
}
pub const VERTICAL_SWING_MAX: u8 = 11;
pub const HORIZONTAL_SWING_MAX: u8 = 6;
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(untagged)]
enum LouverPositionCompat {
Bool(bool),
Number(u8),
}
fn louver_position_compat(value: LouverPositionCompat) -> u8 {
match value {
LouverPositionCompat::Bool(value) => u8::from(value),
LouverPositionCompat::Number(value) => value,
}
}
fn deserialize_louver_position<'de, D>(deserializer: D) -> Result<u8, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(louver_position_compat(LouverPositionCompat::deserialize(
deserializer,
)?))
}
fn deserialize_optional_louver_position<'de, D>(
deserializer: D,
) -> Result<Option<u8>, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Option::<LouverPositionCompat>::deserialize(deserializer)?.map(louver_position_compat))
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum EnergySourcePreference {
@@ -176,10 +211,12 @@ pub struct Device {
pub target_temperature: f64,
#[serde(default = "default_fan")]
pub fan_speed: u8,
#[serde(default)]
pub swing_vertical: bool,
#[serde(default)]
pub swing_horizontal: bool,
/// Raw GREE `SwUpDn` louver position. 0=off/default, 1=full swing, 2..6=fixed positions, 7..11=partial swing ranges.
#[serde(default, deserialize_with = "deserialize_louver_position")]
pub swing_vertical: u8,
/// Raw GREE `SwingLfRig` louver position. 0=off/default, 1=full swing, 2..6=fixed positions.
#[serde(default, deserialize_with = "deserialize_louver_position")]
pub swing_horizontal: u8,
#[serde(default)]
pub quiet: bool,
/// Raw non-zero value last reported by the unit for the GREE `Quiet` property.
@@ -290,8 +327,8 @@ impl Device {
mode: "cool".into(),
target_temperature: 23.0,
fan_speed: 0,
swing_vertical: false,
swing_horizontal: false,
swing_vertical: 0,
swing_horizontal: 0,
quiet: false,
quiet_wire_value: None,
turbo: false,
@@ -389,8 +426,12 @@ pub struct DeviceCommand {
pub mode: Option<String>,
pub target_temperature: Option<f64>,
pub fan_speed: Option<u8>,
pub swing_vertical: Option<bool>,
pub swing_horizontal: Option<bool>,
/// Raw GREE `SwUpDn` value. Legacy boolean payloads remain accepted (`false`=0, `true`=1).
#[serde(default, deserialize_with = "deserialize_optional_louver_position")]
pub swing_vertical: Option<u8>,
/// Raw GREE `SwingLfRig` value. Legacy boolean payloads remain accepted (`false`=0, `true`=1).
#[serde(default, deserialize_with = "deserialize_optional_louver_position")]
pub swing_horizontal: Option<u8>,
pub quiet: Option<bool>,
pub turbo: Option<bool>,
pub light: Option<bool>,
@@ -476,10 +517,10 @@ impl DeviceCommand {
device.fan_speed = v.min(5);
}
if let Some(v) = self.swing_vertical {
device.swing_vertical = v;
device.swing_vertical = v.min(VERTICAL_SWING_MAX);
}
if let Some(v) = self.swing_horizontal {
device.swing_horizontal = v;
device.swing_horizontal = v.min(HORIZONTAL_SWING_MAX);
}
if let Some(v) = self.quiet {
device.quiet = v;
@@ -544,4 +585,27 @@ mod connection_type_tests {
let migrated: Device = serde_json::from_value(value).expect("deserialize legacy device");
assert_eq!(migrated.connection_type, ConnectionType::Local);
}
#[test]
fn legacy_boolean_swing_values_migrate_to_positions() {
let device = Device::simulated_default();
let mut value = serde_json::to_value(device).expect("serialize device");
let object = value.as_object_mut().expect("device object");
object.insert("swing_vertical".into(), serde_json::Value::Bool(true));
object.insert("swing_horizontal".into(), serde_json::Value::Bool(false));
let migrated: Device = serde_json::from_value(value).expect("deserialize legacy device");
assert_eq!(migrated.swing_vertical, 1);
assert_eq!(migrated.swing_horizontal, 0);
}
#[test]
fn legacy_boolean_swing_command_is_accepted() {
let command: DeviceCommand = serde_json::from_value(serde_json::json!({
"swing_vertical": true,
"swing_horizontal": false
}))
.expect("deserialize legacy command");
assert_eq!(command.swing_vertical, Some(1));
assert_eq!(command.swing_horizontal, Some(0));
}
}
+4 -1
View File
@@ -1,7 +1,10 @@
use super::crypto::{
decrypt_v1, decrypt_v2, encrypt_v1, encrypt_v2, GENERIC_GREE_V1_KEY, GENERIC_GREE_V2_KEY,
};
use crate::models::{ApiEvent, ConnectionStatus, ConnectionType, Device, DeviceCommand};
use crate::models::{
ApiEvent, ConnectionStatus, ConnectionType, Device, DeviceCommand, HORIZONTAL_SWING_MAX,
VERTICAL_SWING_MAX,
};
use anyhow::{anyhow, bail, Context, Result};
use chrono::Utc;
use serde_json::{json, Value};
+2 -2
View File
@@ -191,11 +191,11 @@ impl GreeClient {
}
if let Some(v) = command.swing_vertical {
opt.push("SwUpDn");
values.push(json!(if v { 1 } else { 0 }));
values.push(json!(v.min(VERTICAL_SWING_MAX)));
}
if let Some(v) = command.swing_horizontal {
opt.push("SwingLfRig");
values.push(json!(if v { 1 } else { 0 }));
values.push(json!(v.min(HORIZONTAL_SWING_MAX)));
}
if let Some(v) = command.quiet {
opt.push("Quiet");
+2 -2
View File
@@ -201,8 +201,8 @@ impl GreeClient {
mode: "cool".into(),
target_temperature: 24.0,
fan_speed: 0,
swing_vertical: false,
swing_horizontal: false,
swing_vertical: 0,
swing_horizontal: 0,
quiet: false,
quiet_wire_value: None,
turbo: false,
+14 -2
View File
@@ -193,8 +193,20 @@ impl GreeClient {
}
next.fan_speed = raw as u8;
}
"SwUpDn" => next.swing_vertical = status_i64(name, value)? != 0,
"SwingLfRig" => next.swing_horizontal = status_i64(name, value)? != 0,
"SwUpDn" => {
let raw = status_i64(name, value)?;
if !(0..=i64::from(VERTICAL_SWING_MAX)).contains(&raw) {
bail!("invalid GREE vertical louver value for {name}: {raw}")
}
next.swing_vertical = raw as u8;
}
"SwingLfRig" => {
let raw = status_i64(name, value)?;
if !(0..=i64::from(HORIZONTAL_SWING_MAX)).contains(&raw) {
bail!("invalid GREE horizontal louver value for {name}: {raw}")
}
next.swing_horizontal = raw as u8;
}
"Quiet" => {
let raw = status_i64(name, value)?;
if raw < 0 {
+11 -6
View File
@@ -1,6 +1,7 @@
use crate::{
models::{
ApiEvent, ConnectionStatus, ConnectionType, Device, DeviceCommand, GreeCloudSettings,
HORIZONTAL_SWING_MAX, VERTICAL_SWING_MAX,
},
protocol::{
crypto::{decrypt_v1, decrypt_v2, encrypt_v1, encrypt_v2},
@@ -603,9 +604,9 @@ impl GreeCloudProvider {
if let Some(value) = command.swing_vertical {
simple.push((
"SwUpDn",
json!(if value { 1 } else { 0 }),
json!(value.min(VERTICAL_SWING_MAX)),
DeviceCommand {
swing_vertical: Some(value),
swing_vertical: Some(value.min(VERTICAL_SWING_MAX)),
..Default::default()
},
));
@@ -613,9 +614,9 @@ impl GreeCloudProvider {
if let Some(value) = command.swing_horizontal {
simple.push((
"SwingLfRig",
json!(if value { 1 } else { 0 }),
json!(value.min(HORIZONTAL_SWING_MAX)),
DeviceCommand {
swing_horizontal: Some(value),
swing_horizontal: Some(value.min(HORIZONTAL_SWING_MAX)),
..Default::default()
},
));
@@ -1675,10 +1676,14 @@ pub fn apply_cloud_properties(device: &mut Device, props: &BTreeMap<String, Valu
device.fan_speed = value.clamp(0, 5) as u8;
}
if let Some(value) = props.get("SwUpDn").and_then(value_i64) {
device.swing_vertical = value != 0;
if (0..=i64::from(VERTICAL_SWING_MAX)).contains(&value) {
device.swing_vertical = value as u8;
}
}
if let Some(value) = props.get("SwingLfRig").and_then(value_i64) {
device.swing_horizontal = value != 0;
if (0..=i64::from(HORIZONTAL_SWING_MAX)).contains(&value) {
device.swing_horizontal = value as u8;
}
}
if let Some(value) = props.get("Quiet").and_then(value_i64) {
device.quiet = value != 0;
+42
View File
@@ -621,6 +621,33 @@ h3 {
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
}
.louver-controls {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 7px;
margin-top: 4px;
}
.louver-control {
display: grid;
grid-template-columns: minmax(96px, .72fr) minmax(0, 1.28fr);
align-items: center;
gap: 8px;
min-width: 0;
}
.louver-control > span {
color: var(--muted);
font-size: 11px;
line-height: 1.15;
}
.louver-control select,
.louver-control .custom-select {
width: 100%;
min-width: 0;
}
.card-footer {
display: flex;
justify-content: space-between;
@@ -4027,6 +4054,10 @@ body.simulation-standalone [data-view="simulation"] {
padding: 2px 0 5px;
}
.quick-control-card .quick-control-row-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.quick-control-card .quick-control-row-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@@ -4097,6 +4128,17 @@ body.simulation-standalone [data-view="simulation"] {
margin-top: 1px;
}
.quick-control-card .louver-controls {
margin: 1px 0 4px;
}
.quick-control-card .louver-control .custom-select-trigger,
.quick-control-card .louver-control select {
min-height: 34px;
padding-block: 5px;
font-size: 10px;
}
.quick-thermostat-control .zone-quick-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
+21 -6
View File
@@ -1760,15 +1760,30 @@
name="action_target_temperature"></label><label><span data-i18n="common.cooldownSeconds">Cooldown
(s)</span><input type="number" min="30" name="cooldown_seconds" value="300"></label></div>
<div class="two" id="automationDeviceSwingOptions">
<label><span data-i18n="devices.swingVertical">Vertical swing</span><select name="action_swing_vertical">
<label><span data-i18n="devices.swingVertical">Vertical louver</span><select name="action_swing_vertical">
<option value="" data-i18n="actions.noChange">No change</option>
<option value="true" data-i18n="actions.turnOn">Turn on</option>
<option value="false" data-i18n="actions.turnOff">Turn off</option>
<option value="0" data-i18n="louver.off">Off</option>
<option value="1" data-i18n="louver.fullRangeAuto">Full range (Auto)</option>
<option value="2" data-i18n="louver.vertical.fixedTop">Fixed: Top</option>
<option value="3" data-i18n="louver.vertical.fixedUpperMiddle">Fixed: Upper-middle</option>
<option value="4" data-i18n="louver.vertical.fixedMiddle">Fixed: Middle</option>
<option value="5" data-i18n="louver.vertical.fixedLowerMiddle">Fixed: Lower-middle</option>
<option value="6" data-i18n="louver.vertical.fixedBottom">Fixed: Bottom</option>
<option value="7" data-i18n="louver.vertical.swingTop">Swing: Top</option>
<option value="8" data-i18n="louver.vertical.swingUpperMiddle">Swing: Upper-middle</option>
<option value="9" data-i18n="louver.vertical.swingMiddle">Swing: Middle</option>
<option value="10" data-i18n="louver.vertical.swingLowerMiddle">Swing: Lower-middle</option>
<option value="11" data-i18n="louver.vertical.swingBottom">Swing: Bottom</option>
</select></label>
<label><span data-i18n="devices.swingHorizontal">Horizontal swing</span><select name="action_swing_horizontal">
<label><span data-i18n="devices.swingHorizontal">Horizontal louver</span><select name="action_swing_horizontal">
<option value="" data-i18n="actions.noChange">No change</option>
<option value="true" data-i18n="actions.turnOn">Turn on</option>
<option value="false" data-i18n="actions.turnOff">Turn off</option>
<option value="0" data-i18n="louver.off">Off</option>
<option value="1" data-i18n="louver.fullRangeAuto">Full range (Auto)</option>
<option value="2" data-i18n="louver.horizontal.fixedLeft">Fixed: Left</option>
<option value="3" data-i18n="louver.horizontal.fixedLeftMiddle">Fixed: Left-middle</option>
<option value="4" data-i18n="louver.horizontal.fixedMiddle">Fixed: Middle</option>
<option value="5" data-i18n="louver.horizontal.fixedRightMiddle">Fixed: Right-middle</option>
<option value="6" data-i18n="louver.horizontal.fixedRight">Fixed: Right</option>
</select></label>
</div>
<p class="field-note" id="automationGroupHint" hidden data-i18n="automations.groupHint">For a group, Auto mode
+1
View File
@@ -66,6 +66,7 @@ async function loadBootstrap(showMessage = false) {
app.loading = true;
try {
const data = await api('/api/bootstrap');
app.authTokenSubmitted = false;
const hasControlPlan = applyBootstrapSnapshot(data);
renderAll();
if (!hasControlPlan) scheduleControlPlanLoad(0);
+47 -2
View File
@@ -26,7 +26,7 @@ const preferredTheme = ['system', 'light', 'dark'].includes(getCookie('gree_cont
const app = {
devices: [], zones: [], groups: [], deviceGroups: [], deviceGroupEnergy: {}, schedules: [], automations: [], flows: [], accessTokens: [], settings: null, system: {}, outdoorTemperature: null,
token: localStorage.getItem('gree_controller_token') || '', ws: null, wsTimer: null,
token: localStorage.getItem('gree_controller_token') || '', authTokenSubmitted: false, ws: null, wsTimer: null,
currentView: 'dashboard', loading: false, bootstrapReloadPending: false, language: preferredLanguage, theme: preferredTheme, connectionStatus: 'connecting',
languages: [], translations: {}, locales: {}, defaultLanguage: DEFAULT_LANGUAGE,
historyTab: 'overview', historyData: { zones: [], devices: [], sensors: [] }, historyCounts: {},
@@ -128,6 +128,50 @@ const deviceCommandFieldLabel = key => {
}[key];
return translationKey ? tr(translationKey) : key;
};
const VERTICAL_LOUVER_OPTIONS = Object.freeze([
[0, 'louver.off'],
[1, 'louver.fullRangeAuto'],
[2, 'louver.vertical.fixedTop'],
[3, 'louver.vertical.fixedUpperMiddle'],
[4, 'louver.vertical.fixedMiddle'],
[5, 'louver.vertical.fixedLowerMiddle'],
[6, 'louver.vertical.fixedBottom'],
[7, 'louver.vertical.swingTop'],
[8, 'louver.vertical.swingUpperMiddle'],
[9, 'louver.vertical.swingMiddle'],
[10, 'louver.vertical.swingLowerMiddle'],
[11, 'louver.vertical.swingBottom'],
]);
const HORIZONTAL_LOUVER_OPTIONS = Object.freeze([
[0, 'louver.off'],
[1, 'louver.fullRangeAuto'],
[2, 'louver.horizontal.fixedLeft'],
[3, 'louver.horizontal.fixedLeftMiddle'],
[4, 'louver.horizontal.fixedMiddle'],
[5, 'louver.horizontal.fixedRightMiddle'],
[6, 'louver.horizontal.fixedRight'],
]);
const normalizedLouverPosition = value => {
if (value === null || value === undefined || value === '') return null;
if (value === true || String(value).toLowerCase() === 'true') return 1;
if (value === false || String(value).toLowerCase() === 'false') return 0;
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : null;
};
const louverPositionLabel = (axis, value) => {
const options = axis === 'horizontal' ? HORIZONTAL_LOUVER_OPTIONS : VERTICAL_LOUVER_OPTIONS;
const normalized = normalizedLouverPosition(value);
const item = options.find(([raw]) => raw === normalized);
return item ? tr(item[1]) : String(value ?? '');
};
const louverOptions = (axis, selected, { includeNoChange = false } = {}) => {
const options = axis === 'horizontal' ? HORIZONTAL_LOUVER_OPTIONS : VERTICAL_LOUVER_OPTIONS;
const selectedValue = normalizedLouverPosition(selected);
const render = items => items.map(([value, key]) => `<option value="${value}" ${selectedValue === value ? 'selected' : ''}>${esc(tr(key))}</option>`).join('');
const noChange = includeNoChange ? `<option value="" ${selectedValue === null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option>` : '';
if (axis !== 'vertical') return `${noChange}${render(options)}`;
return `${noChange}${render(options.slice(0, 7))}<optgroup label="${esc(tr('louver.advancedSwingRanges'))}">${render(options.slice(7))}</optgroup>`;
};
const dateTime = value => value ? new Intl.DateTimeFormat(locale(), { dateStyle: 'short', timeStyle: 'short' }).format(new Date(value)) : '—';
const localResumeSeconds = value => {
const timestamp = value ? new Date(value).getTime() : NaN;
@@ -413,9 +457,10 @@ async function api(path, options = {}) {
}
const response = await fetch(withBase(path), { ...options, headers, body });
if (response.status === 401) {
const invalidToken = Boolean(app.token);
const invalidToken = app.authTokenSubmitted && Boolean(app.token);
const message = invalidToken ? tr('auth.invalid') : tr('auth.required');
showTokenDialog(invalidToken ? message : '');
app.authTokenSubmitted = false;
const error = new Error(message);
error.status = response.status;
throw error;
+15 -7
View File
@@ -130,6 +130,16 @@ function powerIconMarkup() {
return uiIcon('power', 'power-icon');
}
function deviceLouverControls(device, { disabled = false } = {}) {
if (!device) return '';
const caps = device.capabilities || {};
const disabledAttr = disabled ? ' disabled' : '';
const controls = [];
if (caps.vertical_swing !== false) controls.push(`<label class="louver-control"><span>${esc(tr('devices.swingVertical'))}</span><select data-louver-field="swing_vertical" data-device="${esc(device.id)}"${disabledAttr}>${louverOptions('vertical', device.swing_vertical)}</select></label>`);
if (caps.horizontal_swing !== false) controls.push(`<label class="louver-control"><span>${esc(tr('devices.swingHorizontal'))}</span><select data-louver-field="swing_horizontal" data-device="${esc(device.id)}"${disabledAttr}>${louverOptions('horizontal', device.swing_horizontal)}</select></label>`);
return controls.length ? `<div class="louver-controls">${controls.join('')}</div>` : '';
}
function manualLocalDeviceCard(device) {
const caps = device.capabilities || {};
const modes = ['auto', 'cool', 'dry', 'fan', 'heat'];
@@ -153,9 +163,8 @@ function manualLocalDeviceCard(device) {
${compressorQueuePanel(managedZone)}
<div class="mode-row quick-control-row quick-control-row-5">${modes.map(mode => `<button class="${device.mode === mode ? 'active' : ''}" data-action="mode" data-value="${mode}" data-device="${esc(device.id)}">${esc(modeLabel(mode))}</button>`).join('')}</div>
<div class="fan-row quick-control-row quick-control-row-4">${fans.map(fan => `<button class="${Number(device.fan_speed) === fan ? 'active' : ''}" data-action="fan" data-value="${fan}" data-device="${esc(device.id)}">${esc(fanLabel(fan))}</button>`).join('')}</div>
<div class="device-toggles quick-control-row quick-control-row-4">
${caps.vertical_swing === false ? '' : `<button class="${device.swing_vertical ? 'active' : ''}" data-action="toggle" data-field="swing_vertical" data-device="${esc(device.id)}">${esc(tr('devices.swingVertical'))}</button>`}
${caps.horizontal_swing === false ? '' : `<button class="${device.swing_horizontal ? 'active' : ''}" data-action="toggle" data-field="swing_horizontal" data-device="${esc(device.id)}">${esc(tr('devices.swingHorizontal'))}</button>`}
${deviceLouverControls(device)}
<div class="device-toggles quick-control-row quick-control-row-2">
${device.supports_quiet === false ? '' : `<button class="${device.quiet ? 'active' : ''}" data-action="toggle" data-field="quiet" data-device="${esc(device.id)}">${esc(tr('devices.quiet'))}</button>`}
${device.supports_turbo === false ? '' : `<button class="${device.turbo ? 'active' : ''}" data-action="toggle" data-field="turbo" data-device="${esc(device.id)}">${esc(tr('devices.turbo'))}</button>`}
</div>
@@ -192,9 +201,8 @@ function manualCloudDeviceCard(device) {
${compressorQueuePanel(managedZone)}
${modes.length ? `<div class="mode-row quick-control-row quick-control-row-5">${modes.map(mode => `<button class="${device.mode === mode ? 'active' : ''}" data-action="mode" data-value="${mode}" data-device="${esc(device.id)}">${esc(modeLabel(mode))}</button>`).join('')}</div>` : ''}
${fans.length ? `<div class="fan-row quick-control-row quick-control-row-${Math.min(6, fans.length)}">${fans.map(fan => `<button class="${Number(device.fan_speed) === fan ? 'active' : ''}" data-action="fan" data-value="${fan}" data-device="${esc(device.id)}">${esc(fanLabel(fan))}</button>`).join('')}</div>` : ''}
<div class="device-toggles quick-control-row quick-control-row-4">
${caps.vertical_swing === false ? '' : `<button class="${device.swing_vertical ? 'active' : ''}" data-action="toggle" data-field="swing_vertical" data-device="${esc(device.id)}">${esc(tr('devices.swingVertical'))}</button>`}
${caps.horizontal_swing === false ? '' : `<button class="${device.swing_horizontal ? 'active' : ''}" data-action="toggle" data-field="swing_horizontal" data-device="${esc(device.id)}">${esc(tr('devices.swingHorizontal'))}</button>`}
${deviceLouverControls(device)}
<div class="device-toggles quick-control-row quick-control-row-2">
${device.supports_quiet === false ? '' : `<button class="${device.quiet ? 'active' : ''}" data-action="toggle" data-field="quiet" data-device="${esc(device.id)}">${esc(tr('devices.quiet'))}</button>`}
${device.supports_turbo === false ? '' : `<button class="${device.turbo ? 'active' : ''}" data-action="toggle" data-field="turbo" data-device="${esc(device.id)}">${esc(tr('devices.turbo'))}</button>`}
</div>
@@ -564,7 +572,7 @@ function zoneCard(zone, detailed = true) {
${['auto', 'comfort', 'sleep', 'away'].map(preset => `<button class="${manual === preset ? 'active' : ''}" data-action="zone-preset" data-id="${esc(zone.id)}" data-value="${preset}">${esc(preset === 'sleep' ? tr('zones.sleepNow') : zonePresetLabel(preset))}</button>`).join('')}
</div>
<div class="mode-row zone-mode-row quick-control-row quick-control-row-3"><button class="${mode === 'house' && globalModeAvailable ? 'active' : ''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="house"${globalModeDisabled}>${esc(tr('zones.followHouseShort'))}</button><button class="${mode === 'heat' ? 'active' : ''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="heat">${esc(modeLabel('heat'))}</button><button class="${mode === 'cool' ? 'active' : ''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="cool">${esc(modeLabel('cool'))}</button></div>
${device ? `<div class="device-toggles thermostat-swing-row quick-control-row quick-control-row-2">${device.capabilities?.vertical_swing === false ? '' : `<button class="${device.swing_vertical ? 'active' : ''}" data-action="zone-swing" data-field="swing_vertical" data-device="${esc(device.id)}"${deviceUnavailable ? ' disabled' : ''}>${esc(tr('devices.swingVertical'))}</button>`}${device.capabilities?.horizontal_swing === false ? '' : `<button class="${device.swing_horizontal ? 'active' : ''}" data-action="zone-swing" data-field="swing_horizontal" data-device="${esc(device.id)}"${deviceUnavailable ? ' disabled' : ''}>${esc(tr('devices.swingHorizontal'))}</button>`}</div>` : ''}
${device ? deviceLouverControls(device, { disabled: deviceUnavailable }) : ''}
<div class="zone-state-line"><span class="zone-runtime-status" title="${esc(runtimeStatus)}">${esc(runtimeStatus)}${controlGroup ? ` <b class="group-control-tag">${esc(controlGroup.name)}</b>` : ''}</span><span>${esc(override)}</span></div>
<div class="zone-state-line control-owner-line"><span><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}</span><span>${esc(zoneControlOwnerMeta(zone))}</span></div>
${compressorQueuePanel(zone)}
+11 -2
View File
@@ -8,6 +8,15 @@ document.addEventListener('keydown', event => {
}
});
document.addEventListener('change', async event => {
const select = event.target.closest?.('[data-louver-field][data-device]');
if (!select) return;
const value = Number(select.value);
if (!Number.isInteger(value)) return;
await sendDeviceCommand(select.dataset.device, { [select.dataset.louverField]: value });
});
document.addEventListener('click', async event => {
const inlineTarget = event.target.closest('[data-temperature-kind]');
if (inlineTarget && !event.target.closest('button')) { beginInlineTemperatureEdit(inlineTarget); return; }
@@ -194,7 +203,6 @@ document.addEventListener('click', async event => {
if (action === 'cancel-compressor-task') return cancelCompressorTask(button.dataset.id);
if (action === 'cancel-all-compressor-tasks') { if (!confirm(tr('zones.queueCancelAllConfirm', { count: app.zones.filter(zone => !!zone.compressor_pending_action).length }))) return; return cancelAllCompressorTasks(); }
if (action === 'zone-device-power') return sendZoneLocalPower(button.dataset.id, button.dataset.value === 'true');
if (action === 'zone-swing' && device) return sendDeviceCommand(device.id, current => ({ [button.dataset.field]: !current?.[button.dataset.field] }));
if (action === 'zone-open-temporary') return populateTemporaryThermostat(button.dataset.id);
if (action === 'zone-temperature') { const zone = app.zones.find(v => v.id === button.dataset.id); if (zone) { const base = Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint); queueZoneTemperature(zone, base + Number(button.dataset.delta)); } return; }
if (action === 'zone-mode') return sendZoneControl(button.dataset.id, { mode: button.dataset.value });
@@ -349,6 +357,7 @@ $('#tokenForm').addEventListener('submit', async event => {
setFormBusy(form, true, 'auth.connecting');
app.token = new FormData(form).get('token').trim();
localStorage.setItem('gree_controller_token', app.token);
app.authTokenSubmitted = true;
if (app.ws) app.ws.close();
try { await loadBootstrap(); }
finally { setFormBusy(form, false); }
@@ -634,7 +643,7 @@ $('#scheduleForm').addEventListener('submit', async event => {
$('#automationForm').addEventListener('submit', async event => {
event.preventDefault(); const form = event.currentTarget, raw = Object.fromEntries(new FormData(form)); const id = raw.id;
const groupTarget = raw.action_target_kind === 'group';
const action = {}; if (raw.action_power !== '') action.power = raw.action_power === 'true'; if (raw.action_mode) action.mode = raw.action_mode; if (!groupTarget && raw.action_target_temperature !== '') action.target_temperature = parseDecimal(raw.action_target_temperature); if (!groupTarget && raw.action_swing_vertical !== '') action.swing_vertical = raw.action_swing_vertical === 'true'; if (!groupTarget && raw.action_swing_horizontal !== '') action.swing_horizontal = raw.action_swing_horizontal === 'true';
const action = {}; if (raw.action_power !== '') action.power = raw.action_power === 'true'; if (raw.action_mode) action.mode = raw.action_mode; if (!groupTarget && raw.action_target_temperature !== '') action.target_temperature = parseDecimal(raw.action_target_temperature); if (!groupTarget && raw.action_swing_vertical !== '') action.swing_vertical = Number(raw.action_swing_vertical); if (!groupTarget && raw.action_swing_horizontal !== '') action.swing_horizontal = Number(raw.action_swing_horizontal);
const body = { name: raw.name, enabled: form.enabled.checked, trigger_kind: raw.trigger_kind, trigger_device_id: raw.trigger_device_id || null, threshold: raw.threshold === '' ? null : parseDecimal(raw.threshold), at_time: raw.at_time || null, action_device_id: groupTarget ? '' : raw.action_device_id, action_group_id: groupTarget ? (raw.action_group_id || null) : null, action_preset: groupTarget ? (raw.action_preset || null) : null, action, cooldown_seconds: Number(raw.cooldown_seconds) };
await runFormTask(form, async () => {
await api(id ? `/api/automations/${encodeURIComponent(id)}` : '/api/automations', { method: id ? 'PUT' : 'POST', body }); form.closest('dialog').close(); form.reset(); await loadBootstrap(); toast(tr('common.saved'));
+59 -10
View File
@@ -42,6 +42,13 @@ function sharedFlowInputRequiresComparison(kind) {
return ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_state', 'ha_numeric', 'ha_attribute', 'house_mode', 'device_state', 'zone_state', 'group_state'].includes(kind);
}
function sharedFlowInputLouverAxis(item) {
if (item?.kind !== 'device_state') return null;
if (item.config?.field === 'swing_vertical') return 'vertical';
if (item.config?.field === 'swing_horizontal') return 'horizontal';
return null;
}
function sharedFlowReferenceComparisonDefaults(item) {
const kind = item?.kind || '';
if (['outdoor_temperature', 'device_temperature', 'zone_temperature'].includes(kind)) return { operator: 'lt', value: 20 };
@@ -49,6 +56,7 @@ function sharedFlowReferenceComparisonDefaults(item) {
if (kind === 'ha_state') return { operator: 'eq', value: 'on' };
if (kind === 'ha_attribute') return { operator: 'eq', value: '' };
if (kind === 'house_mode') return { operator: 'eq', value: 'cool' };
if (sharedFlowInputLouverAxis(item)) return { operator: 'eq', value: 0 };
if (kind === 'device_state') return { operator: 'eq', value: 'true' };
if (kind === 'zone_state') return { operator: 'eq', value: 'true' };
if (kind === 'group_state') return { operator: 'eq', value: 'true' };
@@ -225,6 +233,8 @@ function flowSharedInputObservation(item) {
function flowSharedInputCurrentText(item, observation = flowSharedInputObservation(item)) {
if (!observation?.hasValue) return '—';
const value = observation.value;
const louverAxis = sharedFlowInputLouverAxis(item);
if (louverAxis) return louverPositionLabel(louverAxis, value);
if (['outdoor_temperature', 'device_temperature', 'zone_temperature'].includes(item.kind)) return fmtTemp(value);
if (item.kind === 'ha_numeric') {
const numeric = Number(value);
@@ -321,7 +331,11 @@ function flowNodeSummary(node) {
if (node.kind === 'ha_attribute') return `${c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id'}.${c.attribute || 'attribute'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
if (node.kind === 'ha_available') return `${c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id'} · ${tr('flow.available')}`;
if (node.kind === 'house_mode') return `${tr('flow.houseMode')} ${flowOperatorLabel(c.operator)} ${c.value || '—'}`;
if (node.kind === 'device_state') return `${app.devices.find(d => d.id === c.device_id)?.name || tr('common.noDevice')} · ${c.field || 'state'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
if (node.kind === 'device_state') {
const axis = c.field === 'swing_vertical' ? 'vertical' : c.field === 'swing_horizontal' ? 'horizontal' : null;
const value = axis ? louverPositionLabel(axis, c.value) : (c.value ?? '—');
return `${app.devices.find(d => d.id === c.device_id)?.name || tr('common.noDevice')} · ${deviceCommandFieldLabel(c.field || 'state')} ${flowOperatorLabel(c.operator)} ${value}`;
}
if (node.kind === 'zone_state') return `${app.zones.find(z => z.id === c.zone_id)?.name || tr('common.noZone')} · ${c.field || 'state'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
if (node.kind === 'group_state') return `${app.groups.find(g => g.id === c.group_id)?.name || tr('groups.group')} · ${c.field || 'power_enabled'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
if (node.kind === 'night_mode') return tr('flow.nightModeActive');
@@ -330,8 +344,10 @@ function flowNodeSummary(node) {
const item = (app.flowSharedInputs || []).find(value => value.id === c.input_id);
if (!item) return tr('flow.sharedInputMissing');
if (sharedFlowInputRequiresComparison(item.kind)) {
const axis = sharedFlowInputLouverAxis(item);
const value = axis ? louverPositionLabel(axis, c.value) : (c.value ?? '—');
return c.operator
? `${item.name} · ${sharedFlowInputSourceSummary(item)} · ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`
? `${item.name} · ${sharedFlowInputSourceSummary(item)} · ${flowOperatorLabel(c.operator)} ${value}`
: `${item.name} · ${tr('flow.selectOperator')}`;
}
return `${item.name} · ${sharedFlowInputSourceSummary(item)}`;
@@ -481,12 +497,14 @@ function sharedFlowReferenceComparisonFields(item, config) {
if (!item || !sharedFlowInputRequiresComparison(item.kind)) return '';
const defaults = sharedFlowReferenceComparisonDefaults(item);
const selected = config.operator || '';
const louverAxis = sharedFlowInputLouverAxis(item);
const numeric = ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(item.kind);
const fullOperators = numeric || item.kind === 'ha_attribute';
const operatorOptions = `${!selected ? `<option value="" selected disabled>${esc(tr('flow.selectOperator'))}</option>` : ''}${fullOperators ? flowOperatorOptions(selected) : [['eq', '='], ['neq', '≠']].map(([value, label]) => `<option value="${value}" ${value === selected ? 'selected' : ''}>${label}</option>`).join('')}`;
const value = config.value ?? defaults.value ?? '';
let valueField = `<input data-flow-config="value" value="${esc(value)}">`;
if (numeric) valueField = `<input type="number" step="0.1" data-flow-config="value" value="${Number(value ?? 0)}">`;
if (louverAxis) valueField = `<select data-flow-config="value">${louverOptions(louverAxis, value)}</select>`;
else if (numeric) valueField = `<input type="number" step="0.1" data-flow-config="value" value="${Number(value ?? 0)}">`;
else if (item.kind === 'house_mode') valueField = `<select data-flow-config="value">${['cool', 'heat', 'off'].map(v => `<option value="${v}" ${String(value) === v ? 'selected' : ''}>${esc(v === 'off' ? tr('common.off') : tr(`mode.${v}`))}</option>`).join('')}</select>`;
return `<p class="field-note">${esc(tr('flow.sharedInputFlowComparisonHint'))}</p><div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${operatorOptions}</select></label><label><span>${esc(tr('flow.value'))}</span>${valueField}</label></div>`;
}
@@ -516,7 +534,7 @@ function renderFlowInspector() {
else if (node.kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="climate.living_room"></label><label><span>${esc(tr('flow.attribute'))}</span><input data-flow-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label>${flowTextComparisonFields(c, true)}`;
else if (node.kind === 'ha_available') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label><p class="field-note">${esc(tr('flow.haAvailableHint'))}</p>`;
else if (node.kind === 'house_mode') fields = `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator"><option value="eq" ${c.operator !== 'neq' ? 'selected' : ''}>=</option><option value="neq" ${c.operator === 'neq' ? 'selected' : ''}>≠</option></select></label><label><span>${esc(tr('flow.houseMode'))}</span><select data-flow-config="value">${['cool', 'heat', 'off'].map(v => `<option value="${v}" ${c.value === v ? 'selected' : ''}>${esc(v === 'off' ? tr('common.off') : tr(`mode.${v}`))}</option>`).join('')}</select></label></div>`;
else if (node.kind === 'device_state') 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.field'))}</span><select data-flow-config="field">${['enabled', 'online', 'power', 'mode', 'fan_speed', 'swing_vertical', 'swing_horizontal', 'quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(deviceCommandFieldLabel(v))}</option>`).join('')}</select></label>${flowTextComparisonFields(c)}`;
else if (node.kind === 'device_state') 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.field'))}</span><select data-flow-config="field">${['enabled', 'online', 'power', 'mode', 'fan_speed', 'swing_vertical', 'swing_horizontal', 'quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(deviceCommandFieldLabel(v))}</option>`).join('')}</select></label>${['swing_vertical', 'swing_horizontal'].includes(c.field) ? flowLouverComparisonFields(c, c.field === 'swing_horizontal' ? 'horizontal' : 'vertical') : flowTextComparisonFields(c)}`;
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>`;
@@ -530,7 +548,7 @@ 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_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><details class="flow-device-options"><summary>${esc(tr('flow.thermostatDeviceOptions'))}</summary><div class="two">${flowOptionalBoolField(c, 'swing_vertical')}${flowOptionalBoolField(c, 'swing_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 === '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><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 === 'group_action') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-flow-config="group_id">${flowSelectOptions(app.groups, c.group_id)}</select></label>${flowActionFields(c, true)}`;
else if (node.kind === 'ha_service_action') fields = `<div class="two"><label><span>domain</span><input data-flow-config="domain" value="${esc(c.domain || 'switch')}" placeholder="switch"></label><label><span>service</span><input data-flow-config="service" value="${esc(c.service || 'turn_on')}" placeholder="turn_on"></label></div><p class="field-note">${esc(tr('flow.serviceExample'))}</p><label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="switch.gas"></label><label><span>${esc(tr('flow.serviceData'))}</span><textarea rows="5" data-flow-config="data_json">${esc(JSON.stringify(c.data || {}, null, 2))}</textarea></label><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
@@ -538,10 +556,18 @@ function renderFlowInspector() {
}
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>`; }
function flowLouverComparisonFields(c, axis) {
const options = [['eq', '='], ['neq', '≠']].map(([v, l]) => `<option value="${v}" ${v === (c.operator || 'eq') ? 'selected' : ''}>${l}</option>`).join('');
const value = Number.isInteger(Number(c.value)) ? Number(c.value) : 0;
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><select data-flow-config="value">${louverOptions(axis, value)}</select></label></div>`;
}
function flowComparisonFields(c, temperature = true) { return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${flowOperatorOptions(c.operator || 'lt')}</select></label><label><span>${esc(temperature ? tr('automations.thresholdC') : tr('flow.value'))}</span><input type="number" step="0.1" data-flow-config="value" value="${Number(c.value ?? 0)}"></label></div>`; }
function flowOptionalBoolField(c, key) {
return `<label><span>${esc(deviceCommandFieldLabel(key))}</span><select data-flow-config="${esc(key)}"><option value="" ${c[key] == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c[key] === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c[key] === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label>`;
}
function flowOptionalLouverField(c, key, axis) {
return `<label><span>${esc(deviceCommandFieldLabel(key))}</span><select data-flow-config="${esc(key)}">${louverOptions(axis, c[key], { includeNoChange: true })}</select></label>`;
}
function flowActionFields(c, group) {
const modes = group ? ['auto', 'house', 'heat', 'cool'] : ['auto', 'cool', 'dry', 'fan', 'heat'];
const modeOptions = modes.map(v => `<option value="${v}" ${c.mode === v ? 'selected' : ''}>${esc(v === 'auto' ? 'Auto' : v === 'house' ? tr('flow.houseMode') : (tr(`mode.${v}`) || v))}</option>`).join('');
@@ -549,7 +575,7 @@ function flowActionFields(c, group) {
const target = group
? `<label><span>${esc(tr('groups.profile'))}</span><select data-flow-config="preset"><option value="">${esc(tr('actions.noChange'))}</option>${['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="${c.setpoint ?? 21}"></label>`
: `<label><span>${esc(tr('common.targetC'))}</span><input type="number" min="8" max="30" step="0.5" data-flow-config="target_temperature" value="${c.target_temperature ?? ''}"></label>`;
const deviceOptions = group ? '' : `<details class="flow-device-options"><summary>${esc(tr('flow.deviceOptions'))}</summary><div class="two"><label><span>${esc(deviceCommandFieldLabel('fan_speed'))}</span><select data-flow-config="fan_speed"><option value="" ${c.fan_speed == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option>${[0, 1, 2, 3, 4, 5].map(v => `<option value="${v}" ${Number(c.fan_speed) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label>${flowOptionalBoolField(c, 'swing_vertical')}${flowOptionalBoolField(c, 'swing_horizontal')}${flowOptionalBoolField(c, 'quiet')}${flowOptionalBoolField(c, 'turbo')}${flowOptionalBoolField(c, 'light')}${flowOptionalBoolField(c, 'air')}${flowOptionalBoolField(c, 'xfan')}${flowOptionalBoolField(c, 'health')}${flowOptionalBoolField(c, 'sleep')}</div><p class="field-note">${esc(tr('flow.deviceOptionsHint'))}</p></details>`;
const deviceOptions = group ? '' : `<details class="flow-device-options"><summary>${esc(tr('flow.deviceOptions'))}</summary><div class="two"><label><span>${esc(deviceCommandFieldLabel('fan_speed'))}</span><select data-flow-config="fan_speed"><option value="" ${c.fan_speed == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option>${[0, 1, 2, 3, 4, 5].map(v => `<option value="${v}" ${Number(c.fan_speed) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label>${flowOptionalLouverField(c, 'swing_vertical', 'vertical')}${flowOptionalLouverField(c, 'swing_horizontal', 'horizontal')}${flowOptionalBoolField(c, 'quiet')}${flowOptionalBoolField(c, 'turbo')}${flowOptionalBoolField(c, 'light')}${flowOptionalBoolField(c, 'air')}${flowOptionalBoolField(c, 'xfan')}${flowOptionalBoolField(c, 'health')}${flowOptionalBoolField(c, 'sleep')}</div><p class="field-note">${esc(tr('flow.deviceOptionsHint'))}</p></details>`;
return `${base}${target}${deviceOptions}<label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
}
@@ -954,13 +980,28 @@ function localDateTimeInputValue(date = new Date()) {
function flowSimulationOverrideNodes() {
return (app.flowDraft?.nodes || []).filter(node => ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_state', 'ha_numeric', 'ha_attribute', 'ha_available', 'house_mode', 'device_state', 'zone_state', 'group_state', 'night_mode', 'shared_input'].includes(node.kind));
}
function flowSimulationLouverAxis(node) {
if (node?.kind === 'device_state') {
if (node.config?.field === 'swing_vertical') return 'vertical';
if (node.config?.field === 'swing_horizontal') return 'horizontal';
}
if (node?.kind === 'shared_input') {
const item = (app.flowSharedInputs || []).find(value => value.id === node.config?.input_id);
return sharedFlowInputLouverAxis(item);
}
return null;
}
function renderFlowSimulationOverrides() {
const host = $('#flowSimulationOverrides'); if (!host) return;
const nodes = flowSimulationOverrideNodes();
host.innerHTML = nodes.length ? nodes.map(node => {
const effectiveKind = node.kind === 'shared_input' ? (app.flowSharedInputs || []).find(item => item.id === node.config?.input_id)?.kind : node.kind;
const louverAxis = flowSimulationLouverAxis(node);
const numeric = ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(effectiveKind);
return `<label><span>${esc(flowNodeTitle(FLOW_NODE_META[node.kind]))} · ${esc(flowNodeSummary(node))}</span><input ${numeric ? 'type="number" step="0.1"' : 'type="text"'} data-flow-sim-node="${esc(node.id)}" placeholder="${esc(tr('flow.useLiveValue'))}"></label>`;
const control = louverAxis
? `<select data-flow-sim-node="${esc(node.id)}"><option value="">${esc(tr('flow.useLiveValue'))}</option>${louverOptions(louverAxis, null)}</select>`
: `<input ${numeric ? 'type="number" step="0.1"' : 'type="text"'} data-flow-sim-node="${esc(node.id)}" placeholder="${esc(tr('flow.useLiveValue'))}">`;
return `<label><span>${esc(flowNodeTitle(FLOW_NODE_META[node.kind]))} · ${esc(flowNodeSummary(node))}</span>${control}</label>`;
}).join('') : `<p class="field-note">${esc(tr('flow.noSimulationOverrides'))}</p>`;
}
function collectFlowSimulationOverrides() {
@@ -969,7 +1010,7 @@ function collectFlowSimulationOverrides() {
if (input.value.trim() === '') return;
const node = flowNodeById(input.dataset.flowSimNode); let value = input.value.trim();
const effectiveKind = node?.kind === 'shared_input' ? (app.flowSharedInputs || []).find(item => item.id === node.config?.input_id)?.kind : node?.kind;
if (node && ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(effectiveKind)) value = Number(value);
if (node && (flowSimulationLouverAxis(node) || ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(effectiveKind))) value = Number(value);
else if (/^(true|false)$/i.test(value)) value = value.toLowerCase() === 'true';
result[input.dataset.flowSimNode] = value;
});
@@ -1125,10 +1166,18 @@ function updateFlowConfig(input) {
}
else if (node.kind === 'shared_input' && key === 'value') {
const item = (app.flowSharedInputs || []).find(value => value.id === node.config.input_id);
const numeric = ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(item?.kind);
const numeric = Boolean(sharedFlowInputLouverAxis(item)) || ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(item?.kind);
node.config.value = numeric ? (input.value === '' ? null : Number(input.value)) : input.value;
}
else if (['power', 'swing_vertical', 'swing_horizontal', 'quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep'].includes(key)) node.config[key] = input.value === '' ? null : input.value === 'true';
else if (node.kind === 'device_state' && key === 'field') {
const wasLouver = ['swing_vertical', 'swing_horizontal'].includes(node.config.field);
node.config.field = input.value;
if (['swing_vertical', 'swing_horizontal'].includes(input.value)) node.config.value = 0;
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 (['swing_vertical', 'swing_horizontal'].includes(key)) node.config[key] = input.value === '' ? null : Number(input.value);
else if (['power', 'quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep'].includes(key)) node.config[key] = input.value === '' ? null : input.value === 'true';
else if (key === 'value' && node.kind === 'constant') node.config[key] = input.value === 'true';
else if (key === 'data_json' && node.kind === 'ha_service_action') { try { node.config.data = JSON.parse(input.value || '{}'); } catch { toast(tr('flow.invalidJson'), true); return; } }
else if (key === 'seconds' && ['stable_for', 'delay'].includes(node.kind)) node.config.seconds = Math.max(1, Number(input.value || 1));
+1 -1
View File
@@ -87,7 +87,7 @@ function sharedFlowInputSourceSummary(item) {
if (item.kind === 'ha_state' || item.kind === 'ha_numeric' || item.kind === 'ha_available') return c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id';
if (item.kind === 'ha_attribute') return `${c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id'}.${c.attribute || 'attribute'}`;
if (item.kind === 'house_mode') return tr('flow.houseMode');
if (item.kind === 'device_state') return `${app.devices.find(value => value.id === c.device_id)?.name || tr('common.noDevice')} · ${c.field || 'state'}`;
if (item.kind === 'device_state') return `${app.devices.find(value => value.id === c.device_id)?.name || tr('common.noDevice')} · ${deviceCommandFieldLabel(c.field || 'state')}`;
if (item.kind === 'zone_state') return `${app.zones.find(value => value.id === c.zone_id)?.name || tr('common.noZone')} · ${c.field || 'state'}`;
if (item.kind === 'group_state') return `${app.groups.find(value => value.id === c.group_id)?.name || tr('groups.group')} · ${c.field || 'power_enabled'}`;
return flowNodeSummary({ kind: item.kind, config: c });