diff --git a/Cargo.lock b/Cargo.lock index 61b6c3b..33625ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.15.0" +version = "0.14.25" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 6af8cb6..221e88d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.15.0" +version = "0.14.25" edition = "2021" authors = ["GREE Controller contributors"] description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support" diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index 205ce66..05338aa 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,39 +1,39 @@ de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -c3c16d0c76fbb80ce5752b28a1736177940670b10f44235eac5997c4e195c634 ./Cargo.lock -ad513e247ab5dbed3c91a6463cf410cf1a4ac73c0b30422bad40afb93cee0159 ./Cargo.toml +7fc984bbc6ee597fb9bb036f1c359f60aa8a3fd100daf9dfb41a43eb34f2da62 ./Cargo.lock +13d12e35a4919efad82747707c5f42c0fafe242d304e04629dde0cf159487d2f ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -db052d6c978b067a31d752f293fc8c02702d50137ba7b84bce87371089972f11 ./README.md -84bae184c72909d968eeece0a092dbb4fe7a8a071927b29cc188ac6fb6a450f0 ./SOURCE_MANIFEST.sha256 +db95805ae6dfd20d9c83d97fda5977672432ea8f2bce9ae023808b35f7047b3f ./README.md +5771a9d3361ab936c7df33999fcbd57cc0f76777ff584f714456e4afb608c979 ./SOURCE_MANIFEST.sha256 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs -8a96630c36cfac5c392837c8872b20d39d72fe3ec99c7c9e09ec78f2228fd262 ./docs/API.md -58b79297607303151c1de2da74344cd4f6277bc53d80ebe04ed4567d906c963b ./docs/FLOW.md -525b03af649d0215e3b81e52ce63ebf8651f47b49503c30d0177b3e0b2714ac4 ./docs/openapi.json +3031d65ac45a9189a7087bc80b7ae340d906a8da8c7db35ae34e50ce299b1e09 ./docs/API.md +8b548d02dbdf0563f2be617a1d677459bd1959bed93a1799a01bf61bb5aed405 ./docs/FLOW.md +26af934fe981ac1f3f42b86d1fbe8afb2883de46dfab78a49808893c96ffde4e ./docs/openapi.json c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example 0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md dd9291b12afade6fc20d66e80a5513251270e18e4c265260b2bbbe393a5a94c4 ./ha-addon/build.sh -4061846cec4d829e4101b4021df7f07dc2e0293ccfd11e91589f49733ec7c5b4 ./ha-addon/home-assistant/README.md +082dfeb6326f3e1b9841ccf7f2954117ecdf0aa1a90b0aef1e6a1960dacdecbf ./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 -da2a0a9390ab75cef150cd281bc329fa409d0d28f51382b36bd8de3a73325c16 ./ha-addon/home-assistant/custom_components/gree_controller/climate.py +0c008329aad15f5af6328380c1afe1f49db186b37b3d1cafec4b6f4088386e8e ./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 -6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py +41a8958a6fe6d10e5679c95ac34832583a2a15ecfd67d09b4e094a83d06f5c47 ./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 -d2a18bc3de3207b9f9025507981ad3742ddc3f3728fa223baf84c7ecbb05973e ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +2c933d0774037f487e7c927af9688bb79f2ec5876f0ad4f42603430f18e54354 ./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 338a42662e77f91215150851e55bbd39a5c77f5612cc77f3e037c06a6e6bdadb ./ha-addon/home-assistant/custom_components/gree_controller/switch.py -a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json -50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json +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 -b3ee553cc7dbc68c3bb7cdd6dbb9b5ca8646df113cb499ca5bd9d3d0e380db73 ./ha-addon/repository/gree-controller/CHANGELOG.md +b207451e12368ac448998cf2f6a98cdf435a391f9bc5b973d89e5ff9c24a090a ./ha-addon/repository/gree-controller/CHANGELOG.md 1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.md c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md -f7e5325e34b2785d0d0bea657c882202d7557d313cbdc464dcdb91f702d36391 ./ha-addon/repository/gree-controller/config.yaml +42ab24948d58622cd9c51f2bd766058f1b4b15dd36adce8186b6e9b4d72da87c ./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 -1bb256c32c07b8366751bbc79e8ceb284a267f3b389115263b2c67cfd73223e3 ./lang/en.json -826362354203c7f66af8acdfc367d2b10868add09b59dc5d3c756f6102692e77 ./lang/pl.json +744bfd0d8eeaacf4fa4ac748b2d71554727fd00e0887b5aa058482695d1b531d ./lang/en.json +491c9ee92c3a5518ebcab30abb5ce25ee71819fa96bb6239b894add888792312 ./lang/pl.json d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json @@ -92,7 +92,7 @@ c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/READ 3fe88e64e43d380c56955f577992ea5b0252c05280f6e9dc3cd0bbcc0bf00896 ./scripts/generate_ha_migration.py e4849261fd9ed1f01df96c0637c439c0c4eff8fa317b2918026167bba343af79 ./scripts/install-lxc.sh bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./scripts/install.sh -50cc08e2aeff052a47c68212524f4584e9201cfc535d71803f276cdc223d5216 ./scripts/live_realtime_test.js +b3c22df4f6012a1c3210bd613a263624eb92f636b303c538f473fabdd6845b2c ./scripts/live_realtime_test.js e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh 01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh @@ -102,13 +102,13 @@ bd7e0455c6a1501f761f0d4c00ff74833eecbd75078af9168235f90a2d9485b3 ./src/api.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs b24092f34a46626d64628b510eba82193f0b203fd097f6a621b91af5d91eb571 ./src/api/auth.rs 9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs -27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs +a4222a29cdc90e75dffd5fb5e35c11e3556924a6489b1b31272cd81d0b141821 ./src/api/configuration.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs -5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs +2d487e274c777cbde081c5803311bec449097f45bd2d021117cfe10677b39efb ./src/api/devices.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs -b9a9a68e47fdaa577c28063fa68bf95075bc355bff81addca4ee448ef70dd818 ./src/api/flows.rs -82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs +d59b080b392b574ec94276c7b485b455d0d1abacc7de2ef29dcbfcf7d86dbdb0 ./src/api/flows.rs +a17ab0b6d683d285441be15f2af33172d7caa9a57ec8b32e59107379631a377e ./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 -0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs -76dbf72600a48a6ba5b2273777bd8f2c8a6bd212aa236d419cc59eedcb8ab22b ./src/engine/automations.rs +75f4f139cb268b2dfda73966b52624735380724e73b99930bffe36328620f111 ./src/engine.rs +ed5c9a0c61c6e597acb33f58593ebe02b562ddc68fa61271e1c1cd436c1003d3 ./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 -76c1594f31af64c9a34e2555ac015e8719d42273c687b4c85b9c8f306b3c69a2 ./src/engine/polling.rs +56d64614d8ca85cec7098b1f5d2963ecdcbfb7d0a3d4f22e4424a653895b5a16 ./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 -af4edaf12faf294f44a8992d120a99b09edd1cbdd66981390e027d13fd6b6790 ./src/engine/tests.rs +2e7349862beb5f844aea0f1c4cd96ab6e53d3500559d7b2562bd93da595d7c41 ./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 -698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs +f3439a154fad8a0111db26918160901dd04fe47dd56d529490e75deaf0a78b43 ./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 -daca14e9a4dd65394add7c7753c8ae5e7e43e991323a5ca23d8788aae4428925 ./src/protocol/gree.rs +cd0612021f8ed6c9e0823d63883c0790db09aa485583f3f3fc9f2579f2c28ea3 ./src/protocol/gree.rs 7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs -91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs +fcf5e67539c419f3e7e4533d49f41f963fa7779f099bd33c1b210fac32a78949 ./src/protocol/gree/commands.rs 4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs -668e3bfcc4be84d1a86909bfc18e0d3816e0343cdc0d9b43189f7b0c6e0666d4 ./src/protocol/gree/discovery.rs +05fa2e4ec133a61c23a3f4ab2ce792bbfab93feaea9f0ce0d4ccc67fc479833d ./src/protocol/gree/discovery.rs 0ab094f609edb99715402d8eeaa7e38cbfd2d271036aec787dcbb3be3e005720 ./src/protocol/gree/merge.rs ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs -1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.rs +0a901ff258a74072af3812c5417196da1a9b8daa8a2eff30c96d038558601739 ./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 -2a08099b456847f24a828d711c4a0aba3fc372936d9a53f0be931f6706226cc5 ./src/provider.rs +a78d73571fbd983660449fcbd7857064a1325ca12adc07e61c2f50a3208adf37 ./src/provider.rs 830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs 7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs 580917b91441fe9b3276cf0cfc31536ba7954b6a86f4080347b93f9367a5b1ac ./src/queries/entities.rs @@ -200,26 +200,26 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/ 8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html -c184023f9f8a766529e047fb2798296472456a5859828e527cc3a5f180ed2b1e ./web/css/styles.css +5cf9a9c26eb2aceee852af05d66a7ce409e375e94d3b59299acda452c6254895 ./web/css/styles.css 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -263d5554e62d2dd26b251f2c43a7e4ec14c8d8d790fa67e55c9bebf16f9d55c0 ./web/index.html +784e2c28effb9872b582298292d3e2a6b8dd6140207342452193894d210e2f45 ./web/index.html 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md -e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js +27eff2821c40395369be15e197fa830535d2327fcb27c93a0e8e65a1c62de3ad ./web/js-dynamic/bootstrap.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js -aa97e4c67a6f8fe8f473e95e678d7cb79b5756fd915270ceaae59827e6bf7ae1 ./web/js-dynamic/core.js +6bef6daa9908520e85055e2372e04dd69b29c1e58d36f0645d258f56fb0b42b7 ./web/js-dynamic/core.js 752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js -0e1992a7b93767f1fe4634121ae5aa43900d99f8c2614c2a4cbd5770a49b87c1 ./web/js-dynamic/entities.js -eeeef31f767f0aa3efb78bc44a2fea2b9eb86f4d4c50cf1ccae69b5ca30cdfda ./web/js-dynamic/events.js -22da18288de3282245f88f9f525d9ad29ec027571e7ea893720a9470170eccd2 ./web/js-dynamic/flows.js +0f975542ae1d9bd7216123dd3718be6e9a25dbe643eb899c67024d293940fae5 ./web/js-dynamic/entities.js +7a4d02bb1ebfa3a5962c7a0b567338d460b99f30ec582fda3a7fec9f6fe54840 ./web/js-dynamic/events.js +1865ef1ed93f04fe323338ac44ef06c9b19ad6ec222d47085ce51cb4c03fe13d ./web/js-dynamic/flows.js 3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js 86031d4f7f7d1b46412c8fbb09ff1ad1b2b4cd84eb949bc749b27663f1dc27fa ./web/js-dynamic/navigation.js -799613c6d465cb2f92ecb57cb082d0c33729a03d88ab4ca7fa12a3cbd7e38e75 ./web/js-dynamic/realtime.js +2dfeb55897960c48e4fdb9f2fac7ee2956015582e1e3d25b09653ee83e9a07e6 ./web/js-dynamic/realtime.js 1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js -bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js +f7245f0e6cb5011196704f201675ebc6c19d902b8253ffcdbf9ead46cb1c6f86 ./web/js-dynamic/settings-ui.js 672e404470bc6331aa6d658523b221659ec64222af9cc9c17ced978f725790da ./web/js-dynamic/settings.js 489ab0208c86f7a2bb0d721106f01fb5d4f39f41e94749b440a6c58e13c4de07 ./web/js/custom-chart.js 6ae536b18db756bf5eda9adb1b8a550d5a02bfc80f0b7da27fa146039f22e884 ./web/js/lang-init.js diff --git a/README.md b/README.md index 692a200..7de5403 100644 --- a/README.md +++ b/README.md @@ -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.15.0** +**Version: 0.14.25** -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. The bundled Home Assistant climate cards also localize louver/fan/profile labels without changing their stable technical service values. +Version 0.14.25 improves WebSocket connection status handling: transient disconnects show a yellow reconnecting state, persistent outages become connection errors after 15 seconds, and successful reconnects restore the connected state. > [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples. diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256 index 9628d02..126a0b1 100644 --- a/SOURCE_MANIFEST.sha256 +++ b/SOURCE_MANIFEST.sha256 @@ -1,38 +1,38 @@ de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -c3c16d0c76fbb80ce5752b28a1736177940670b10f44235eac5997c4e195c634 ./Cargo.lock -ad513e247ab5dbed3c91a6463cf410cf1a4ac73c0b30422bad40afb93cee0159 ./Cargo.toml +7fc984bbc6ee597fb9bb036f1c359f60aa8a3fd100daf9dfb41a43eb34f2da62 ./Cargo.lock +13d12e35a4919efad82747707c5f42c0fafe242d304e04629dde0cf159487d2f ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -db052d6c978b067a31d752f293fc8c02702d50137ba7b84bce87371089972f11 ./README.md +db95805ae6dfd20d9c83d97fda5977672432ea8f2bce9ae023808b35f7047b3f ./README.md 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs -8a96630c36cfac5c392837c8872b20d39d72fe3ec99c7c9e09ec78f2228fd262 ./docs/API.md -58b79297607303151c1de2da74344cd4f6277bc53d80ebe04ed4567d906c963b ./docs/FLOW.md -525b03af649d0215e3b81e52ce63ebf8651f47b49503c30d0177b3e0b2714ac4 ./docs/openapi.json +3031d65ac45a9189a7087bc80b7ae340d906a8da8c7db35ae34e50ce299b1e09 ./docs/API.md +8b548d02dbdf0563f2be617a1d677459bd1959bed93a1799a01bf61bb5aed405 ./docs/FLOW.md +26af934fe981ac1f3f42b86d1fbe8afb2883de46dfab78a49808893c96ffde4e ./docs/openapi.json c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example 0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md dd9291b12afade6fc20d66e80a5513251270e18e4c265260b2bbbe393a5a94c4 ./ha-addon/build.sh -4061846cec4d829e4101b4021df7f07dc2e0293ccfd11e91589f49733ec7c5b4 ./ha-addon/home-assistant/README.md +082dfeb6326f3e1b9841ccf7f2954117ecdf0aa1a90b0aef1e6a1960dacdecbf ./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 -da2a0a9390ab75cef150cd281bc329fa409d0d28f51382b36bd8de3a73325c16 ./ha-addon/home-assistant/custom_components/gree_controller/climate.py +0c008329aad15f5af6328380c1afe1f49db186b37b3d1cafec4b6f4088386e8e ./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 -6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py +41a8958a6fe6d10e5679c95ac34832583a2a15ecfd67d09b4e094a83d06f5c47 ./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 -d2a18bc3de3207b9f9025507981ad3742ddc3f3728fa223baf84c7ecbb05973e ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +2c933d0774037f487e7c927af9688bb79f2ec5876f0ad4f42603430f18e54354 ./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 338a42662e77f91215150851e55bbd39a5c77f5612cc77f3e037c06a6e6bdadb ./ha-addon/home-assistant/custom_components/gree_controller/switch.py -a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json -50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json +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 -b3ee553cc7dbc68c3bb7cdd6dbb9b5ca8646df113cb499ca5bd9d3d0e380db73 ./ha-addon/repository/gree-controller/CHANGELOG.md +b207451e12368ac448998cf2f6a98cdf435a391f9bc5b973d89e5ff9c24a090a ./ha-addon/repository/gree-controller/CHANGELOG.md 1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.md c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md -f7e5325e34b2785d0d0bea657c882202d7557d313cbdc464dcdb91f702d36391 ./ha-addon/repository/gree-controller/config.yaml +42ab24948d58622cd9c51f2bd766058f1b4b15dd36adce8186b6e9b4d72da87c ./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 -1bb256c32c07b8366751bbc79e8ceb284a267f3b389115263b2c67cfd73223e3 ./lang/en.json -826362354203c7f66af8acdfc367d2b10868add09b59dc5d3c756f6102692e77 ./lang/pl.json +744bfd0d8eeaacf4fa4ac748b2d71554727fd00e0887b5aa058482695d1b531d ./lang/en.json +491c9ee92c3a5518ebcab30abb5ce25ee71819fa96bb6239b894add888792312 ./lang/pl.json d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json @@ -91,7 +91,7 @@ c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/READ 3fe88e64e43d380c56955f577992ea5b0252c05280f6e9dc3cd0bbcc0bf00896 ./scripts/generate_ha_migration.py e4849261fd9ed1f01df96c0637c439c0c4eff8fa317b2918026167bba343af79 ./scripts/install-lxc.sh bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./scripts/install.sh -50cc08e2aeff052a47c68212524f4584e9201cfc535d71803f276cdc223d5216 ./scripts/live_realtime_test.js +b3c22df4f6012a1c3210bd613a263624eb92f636b303c538f473fabdd6845b2c ./scripts/live_realtime_test.js e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh 01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh @@ -101,13 +101,13 @@ bd7e0455c6a1501f761f0d4c00ff74833eecbd75078af9168235f90a2d9485b3 ./src/api.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs b24092f34a46626d64628b510eba82193f0b203fd097f6a621b91af5d91eb571 ./src/api/auth.rs 9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs -27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs +a4222a29cdc90e75dffd5fb5e35c11e3556924a6489b1b31272cd81d0b141821 ./src/api/configuration.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs -5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs +2d487e274c777cbde081c5803311bec449097f45bd2d021117cfe10677b39efb ./src/api/devices.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs -b9a9a68e47fdaa577c28063fa68bf95075bc355bff81addca4ee448ef70dd818 ./src/api/flows.rs -82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs +d59b080b392b574ec94276c7b485b455d0d1abacc7de2ef29dcbfcf7d86dbdb0 ./src/api/flows.rs +a17ab0b6d683d285441be15f2af33172d7caa9a57ec8b32e59107379631a377e ./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 -0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs -76dbf72600a48a6ba5b2273777bd8f2c8a6bd212aa236d419cc59eedcb8ab22b ./src/engine/automations.rs +75f4f139cb268b2dfda73966b52624735380724e73b99930bffe36328620f111 ./src/engine.rs +ed5c9a0c61c6e597acb33f58593ebe02b562ddc68fa61271e1c1cd436c1003d3 ./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 -76c1594f31af64c9a34e2555ac015e8719d42273c687b4c85b9c8f306b3c69a2 ./src/engine/polling.rs +56d64614d8ca85cec7098b1f5d2963ecdcbfb7d0a3d4f22e4424a653895b5a16 ./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 -af4edaf12faf294f44a8992d120a99b09edd1cbdd66981390e027d13fd6b6790 ./src/engine/tests.rs +2e7349862beb5f844aea0f1c4cd96ab6e53d3500559d7b2562bd93da595d7c41 ./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 -698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs +f3439a154fad8a0111db26918160901dd04fe47dd56d529490e75deaf0a78b43 ./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 -daca14e9a4dd65394add7c7753c8ae5e7e43e991323a5ca23d8788aae4428925 ./src/protocol/gree.rs +cd0612021f8ed6c9e0823d63883c0790db09aa485583f3f3fc9f2579f2c28ea3 ./src/protocol/gree.rs 7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs -91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs +fcf5e67539c419f3e7e4533d49f41f963fa7779f099bd33c1b210fac32a78949 ./src/protocol/gree/commands.rs 4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs -668e3bfcc4be84d1a86909bfc18e0d3816e0343cdc0d9b43189f7b0c6e0666d4 ./src/protocol/gree/discovery.rs +05fa2e4ec133a61c23a3f4ab2ce792bbfab93feaea9f0ce0d4ccc67fc479833d ./src/protocol/gree/discovery.rs 0ab094f609edb99715402d8eeaa7e38cbfd2d271036aec787dcbb3be3e005720 ./src/protocol/gree/merge.rs ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs -1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.rs +0a901ff258a74072af3812c5417196da1a9b8daa8a2eff30c96d038558601739 ./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 -2a08099b456847f24a828d711c4a0aba3fc372936d9a53f0be931f6706226cc5 ./src/provider.rs +a78d73571fbd983660449fcbd7857064a1325ca12adc07e61c2f50a3208adf37 ./src/provider.rs 830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs 7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs 580917b91441fe9b3276cf0cfc31536ba7954b6a86f4080347b93f9367a5b1ac ./src/queries/entities.rs @@ -199,26 +199,26 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/ 8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html -c184023f9f8a766529e047fb2798296472456a5859828e527cc3a5f180ed2b1e ./web/css/styles.css +5cf9a9c26eb2aceee852af05d66a7ce409e375e94d3b59299acda452c6254895 ./web/css/styles.css 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -263d5554e62d2dd26b251f2c43a7e4ec14c8d8d790fa67e55c9bebf16f9d55c0 ./web/index.html +784e2c28effb9872b582298292d3e2a6b8dd6140207342452193894d210e2f45 ./web/index.html 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md -e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js +27eff2821c40395369be15e197fa830535d2327fcb27c93a0e8e65a1c62de3ad ./web/js-dynamic/bootstrap.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js -aa97e4c67a6f8fe8f473e95e678d7cb79b5756fd915270ceaae59827e6bf7ae1 ./web/js-dynamic/core.js +6bef6daa9908520e85055e2372e04dd69b29c1e58d36f0645d258f56fb0b42b7 ./web/js-dynamic/core.js 752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js -0e1992a7b93767f1fe4634121ae5aa43900d99f8c2614c2a4cbd5770a49b87c1 ./web/js-dynamic/entities.js -eeeef31f767f0aa3efb78bc44a2fea2b9eb86f4d4c50cf1ccae69b5ca30cdfda ./web/js-dynamic/events.js -22da18288de3282245f88f9f525d9ad29ec027571e7ea893720a9470170eccd2 ./web/js-dynamic/flows.js +0f975542ae1d9bd7216123dd3718be6e9a25dbe643eb899c67024d293940fae5 ./web/js-dynamic/entities.js +7a4d02bb1ebfa3a5962c7a0b567338d460b99f30ec582fda3a7fec9f6fe54840 ./web/js-dynamic/events.js +1865ef1ed93f04fe323338ac44ef06c9b19ad6ec222d47085ce51cb4c03fe13d ./web/js-dynamic/flows.js 3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js 86031d4f7f7d1b46412c8fbb09ff1ad1b2b4cd84eb949bc749b27663f1dc27fa ./web/js-dynamic/navigation.js -799613c6d465cb2f92ecb57cb082d0c33729a03d88ab4ca7fa12a3cbd7e38e75 ./web/js-dynamic/realtime.js +2dfeb55897960c48e4fdb9f2fac7ee2956015582e1e3d25b09653ee83e9a07e6 ./web/js-dynamic/realtime.js 1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js -bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js +f7245f0e6cb5011196704f201675ebc6c19d902b8253ffcdbf9ead46cb1c6f86 ./web/js-dynamic/settings-ui.js 672e404470bc6331aa6d658523b221659ec64222af9cc9c17ced978f725790da ./web/js-dynamic/settings.js 489ab0208c86f7a2bb0d721106f01fb5d4f39f41e94749b440a6c58e13c4de07 ./web/js/custom-chart.js 6ae536b18db756bf5eda9adb1b8a550d5a02bfc80f0b7da27fa146039f22e884 ./web/js/lang-init.js diff --git a/docs/API.md b/docs/API.md index 668da3c..2a03463 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # GREE Controller API reference -HTTP and WebSocket API for GREE Controller **0.15.0**. +HTTP and WebSocket API for GREE Controller **0.14.25**. [← Main documentation](../README.md) @@ -240,7 +240,7 @@ Response: { "status": "ok", "name": "gree-controller", - "version": "0.15.0", + "version": "0.14.25", "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.15.0", + "version": "0.14.25", "uptime_seconds": 1234, "auth_required": false, "control_ready": true, @@ -360,8 +360,7 @@ 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` | 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. | +| `swing_vertical`, `swing_horizontal` | boolean | Swing state. | | `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. | @@ -485,8 +484,8 @@ All fields optional; at least one meaningful field should be sent: "mode": "cool", "target_temperature": 22, "fan_speed": 3, - "swing_vertical": 2, - "swing_horizontal": 1, + "swing_vertical": true, + "swing_horizontal": false, "quiet": false, "turbo": false, "light": true, @@ -497,8 +496,6 @@ 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`, @@ -1694,17 +1691,11 @@ 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 -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. +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. ## Split/multisplit installations and energy comparison — 0.14.1 diff --git a/docs/FLOW.md b/docs/FLOW.md index 6f36cd0..48b4226 100644 --- a/docs/FLOW.md +++ b/docs/FLOW.md @@ -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 a vertical or horizontal louver position makes the action compile as an `Automation` so the louver command is not lost. +- `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. - Richer DAGs compile to native `Automation` rows with `trigger_kind = "flow"`. - Generated rows keep `flow_id` and `flow_node_id`, use stable `flow-` 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 granular vertical/horizontal louver 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 vertical/horizontal swing 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/louver/air/xfan/health remain available through the normal command path. +- GREE fan/quiet/sleep and dry/fan HVAC modes are suppressed on thermostat-assigned devices when they would fight the regulator. Device-only features such as light/turbo/swing/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 louver position, 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 swing, quiet, turbo, light, air, xfan, health and sleep. ## Built-in presets diff --git a/docs/openapi.json b/docs/openapi.json index 7df15e9..91be35a 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2,9 +2,9 @@ "openapi": "3.1.0", "info": { "title": "GREE Controller API", - "version": "0.15.0", + "version": "0.14.25", "summary": "Local HTTP/WebSocket API for GREE HVAC control", - "description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nAdministrator endpoints accept `Authorization: Bearer ` or `x-api-token: ` when configured. Restricted Home Assistant control endpoints also accept generated integration tokens.\n\n### Local and GREE Cloud\nDevices use one API model and one command surface. `connection_type=local` is dispatched only to the existing LAN/UDP provider; `connection_type=gree_cloud` is dispatched only to the GREE Cloud provider. No automatic transport fallback is performed.\n\n### GREE Cloud security\nCloud passwords, REST/MQTT tokens, Authorization values and device cipher keys are never returned in normal API responses or Cloud diagnostics. MQTT uses TLS with certificate and hostname validation.\n\n### Energy\nGREE Cloud `ElcAll` and selected Home Assistant cumulative energy sensors are normalized to kWh and converted to consumption deltas before aggregation. Raw cumulative values are retained only as meter samples/baselines.\n\n### 0.15.0\nAdds granular vertical/horizontal louver positions across direct control, thermostat views, legacy automations, Visual Flow and the bundled Home Assistant integration. Legacy boolean swing command payloads remain accepted. Direct add-on access now shows an invalid-token error only after an explicit token submission.\n\n### 0.14.24\nPublic Custom Charts now fetch chart metadata before language assets, use English for missing-chart errors, support pinch-to-zoom/panning and clickable metric legends, and include the related missing-ID, favicon and Access Token UX fixes.\n\n### 0.14.23\nCode cleanup and small UX changes.\n\n### 0.14.22\nCode cleanup and small UX changes.\n\n### 0.14.21\nCode cleanup and small UX changes.\n\n### 0.14.20\nFixes the ARM cross-build bootstrap by removing the invalid xx-cargo version probe, prepares the target Rust standard library in a cacheable pre-source layer, shares dependency fetching across target architectures, and isolates compiled target caches per architecture while preserving DISTRO-selected Alpine/musl and Debian Trixie/glibc runtimes.\n\n### 0.14.19\nHome Assistant OCI builds now compile Rust for amd64/arm64 on the native build platform through xx cross-compilation, keep Cargo registry/target caches between builds, and send a smaller Docker context while preserving the selected Alpine (musl) or Debian Trixie (glibc) runtime. The Web UI also deduplicates the initial GREE Cloud runtime-status GET across the HTTP and WebSocket bootstrap renders.\n\n### 0.14.18\nShared Web UI CSS now uses generic reusable class names for segmented tabs, configuration layouts, chart cards, enable toggles, diagram primitives and Flow block categories; behavior and visual styling are unchanged.\n\n### 0.14.17\nHistory navigation now reuses the same segmented tab component as Flow / Schedules / Automations, with redundant History-specific tab CSS removed.\n\n### 0.14.16\nCustom Charts now expose saved-chart editing in the Web UI, public chart pages can switch history range through an optional hours override, and the standalone chart layout fits the viewport without vertical page scrolling.\n\n### 0.14.15\nHome Assistant add-on Custom Chart links now resolve the primary HA host IPv4 through Supervisor, support an explicit public_chart_base_url override, and enforce the fixed 8787 ingress/application port contract at startup and build time. Standalone bind/port behavior remains configurable.\n\n### 0.14.14\nShared Flow input Home Assistant sources now expose a dedicated entity search box separate from the selected entity_id, making large live entity catalogs easy to filter by name, ID and state.\n\n### 0.14.13\nHome Assistant add-on authentication is now hidden behind an automatic Supervisor status, manual URL/token fallback is unlocked only after a failed HA test, connection tests return a live sample entity reading, and Shared Flow input HA sources include searchable live entity suggestions.\n\n### 0.14.12\nThe Home Assistant add-on now uses the Supervisor Core API proxy and runtime SUPERVISOR_TOKEN automatically; standalone Home Assistant URL/token configuration is unchanged.\n\n### 0.14.11\nWeb UI icons now use a shared inline SVG set instead of font-dependent Unicode glyphs across navigation, dialogs, Flow, charts, controls and event categories for consistent cross-platform rendering.\n\n\n### 0.14.10\nNight mode and Home Assistant / Sensors standalone settings now use the full available page width, matching the other Web UI sections.\n\n### 0.14.9\nPower controls use an embedded SVG icon instead of a font-dependent Unicode glyph. Home Assistant room-sensor stale/error events identify the affected entity and zone, and historical event rows can recover that context from metadata.\n\n### 0.14.8\nRemaining Home Assistant and notification diagnostic errors no longer use device-transport 502 responses. Missing/invalid integration configuration returns 400, configured external dependency failures return 424, and 502 is reserved for Local/LAN GREE device communication.\n\n### 0.14.7\nHome Assistant energy-sensor discovery now returns an empty optional result when HA is not configured, uses 424 for configured external-dependency failures, and custom select popovers use their rendered height when positioning above a field.\n\n\n### 0.14.6\nFixes dynamic custom-select duplication in History → Energy and hides Energy history controls when no energy source is configured.\n\n### 0.14.5\nWeb UI single-choice controls now use one consistent custom dropdown, language packs are loaded on demand, and the dashboard outdoor temperature opens a 24-hour history modal.\n\n### 0.14.4\nHome Assistant connection tests validate only the configured server/API token and require no entity. `outdoor_entity_id` is the global Home Assistant outdoor-temperature sensor; zones can override it with `ha_outdoor_entity_id`. Room-temperature Home Assistant sensors remain configured per zone with `ha_entity_id`. Global/per-zone outdoor sensors are integrated with aliases, metrics/history and Visual Flow entity suggestions.\n\n### 0.14.2\nAdds independent vertical/horizontal swing controls across manual device control, thermostat zones, legacy automations, Visual Flow and Home Assistant zone climates. Swing is treated as an auxiliary one-shot unit setting and does not replace thermostat ownership.\n\n### 0.14.1\nAdds split/multisplit installation groups with shared energy and outdoor-temperature sources, multi-target energy charts with period comparison, cleaner Cloud status/alerts and safe MQTT cleanup after the last Cloud device is removed.\n\n### 0.14.0\nAdds first-class GREE Cloud MQTT status/control, reconnect/resubscribe lifecycle, Cloud diagnostics, per-device capabilities, Cloud/HA energy sources and energy-history aggregation while preserving the LAN transport contract.", + "description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nAdministrator endpoints accept `Authorization: Bearer ` or `x-api-token: ` when configured. Restricted Home Assistant control endpoints also accept generated integration tokens.\n\n### Local and GREE Cloud\nDevices use one API model and one command surface. `connection_type=local` is dispatched only to the existing LAN/UDP provider; `connection_type=gree_cloud` is dispatched only to the GREE Cloud provider. No automatic transport fallback is performed.\n\n### GREE Cloud security\nCloud passwords, REST/MQTT tokens, Authorization values and device cipher keys are never returned in normal API responses or Cloud diagnostics. MQTT uses TLS with certificate and hostname validation.\n\n### Energy\nGREE Cloud `ElcAll` and selected Home Assistant cumulative energy sensors are normalized to kWh and converted to consumption deltas before aggregation. Raw cumulative values are retained only as meter samples/baselines.\n\n### 0.14.25\nWebSocket disconnects now enter a reconnecting state first, become a connection error only after a persistent 15-second outage, and recover cleanly after reconnect.\n\n### 0.14.24\nPublic Custom Charts now fetch chart metadata before language assets, use English for missing-chart errors, support pinch-to-zoom/panning and clickable metric legends, and include the related missing-ID, favicon and Access Token UX fixes.\n\n### 0.14.23\nCode cleanup and small UX changes.\n\n### 0.14.22\nCode cleanup and small UX changes.\n\n### 0.14.21\nCode cleanup and small UX changes.\n\n### 0.14.20\nFixes the ARM cross-build bootstrap by removing the invalid xx-cargo version probe, prepares the target Rust standard library in a cacheable pre-source layer, shares dependency fetching across target architectures, and isolates compiled target caches per architecture while preserving DISTRO-selected Alpine/musl and Debian Trixie/glibc runtimes.\n\n### 0.14.19\nHome Assistant OCI builds now compile Rust for amd64/arm64 on the native build platform through xx cross-compilation, keep Cargo registry/target caches between builds, and send a smaller Docker context while preserving the selected Alpine (musl) or Debian Trixie (glibc) runtime. The Web UI also deduplicates the initial GREE Cloud runtime-status GET across the HTTP and WebSocket bootstrap renders.\n\n### 0.14.18\nShared Web UI CSS now uses generic reusable class names for segmented tabs, configuration layouts, chart cards, enable toggles, diagram primitives and Flow block categories; behavior and visual styling are unchanged.\n\n### 0.14.17\nHistory navigation now reuses the same segmented tab component as Flow / Schedules / Automations, with redundant History-specific tab CSS removed.\n\n### 0.14.16\nCustom Charts now expose saved-chart editing in the Web UI, public chart pages can switch history range through an optional hours override, and the standalone chart layout fits the viewport without vertical page scrolling.\n\n### 0.14.15\nHome Assistant add-on Custom Chart links now resolve the primary HA host IPv4 through Supervisor, support an explicit public_chart_base_url override, and enforce the fixed 8787 ingress/application port contract at startup and build time. Standalone bind/port behavior remains configurable.\n\n### 0.14.14\nShared Flow input Home Assistant sources now expose a dedicated entity search box separate from the selected entity_id, making large live entity catalogs easy to filter by name, ID and state.\n\n### 0.14.13\nHome Assistant add-on authentication is now hidden behind an automatic Supervisor status, manual URL/token fallback is unlocked only after a failed HA test, connection tests return a live sample entity reading, and Shared Flow input HA sources include searchable live entity suggestions.\n\n### 0.14.12\nThe Home Assistant add-on now uses the Supervisor Core API proxy and runtime SUPERVISOR_TOKEN automatically; standalone Home Assistant URL/token configuration is unchanged.\n\n### 0.14.11\nWeb UI icons now use a shared inline SVG set instead of font-dependent Unicode glyphs across navigation, dialogs, Flow, charts, controls and event categories for consistent cross-platform rendering.\n\n\n### 0.14.10\nNight mode and Home Assistant / Sensors standalone settings now use the full available page width, matching the other Web UI sections.\n\n### 0.14.9\nPower controls use an embedded SVG icon instead of a font-dependent Unicode glyph. Home Assistant room-sensor stale/error events identify the affected entity and zone, and historical event rows can recover that context from metadata.\n\n### 0.14.8\nRemaining Home Assistant and notification diagnostic errors no longer use device-transport 502 responses. Missing/invalid integration configuration returns 400, configured external dependency failures return 424, and 502 is reserved for Local/LAN GREE device communication.\n\n### 0.14.7\nHome Assistant energy-sensor discovery now returns an empty optional result when HA is not configured, uses 424 for configured external-dependency failures, and custom select popovers use their rendered height when positioning above a field.\n\n\n### 0.14.6\nFixes dynamic custom-select duplication in History → Energy and hides Energy history controls when no energy source is configured.\n\n### 0.14.5\nWeb UI single-choice controls now use one consistent custom dropdown, language packs are loaded on demand, and the dashboard outdoor temperature opens a 24-hour history modal.\n\n### 0.14.4\nHome Assistant connection tests validate only the configured server/API token and require no entity. `outdoor_entity_id` is the global Home Assistant outdoor-temperature sensor; zones can override it with `ha_outdoor_entity_id`. Room-temperature Home Assistant sensors remain configured per zone with `ha_entity_id`. Global/per-zone outdoor sensors are integrated with aliases, metrics/history and Visual Flow entity suggestions.\n\n### 0.14.2\nAdds independent vertical/horizontal swing controls across manual device control, thermostat zones, legacy automations, Visual Flow and Home Assistant zone climates. Swing is treated as an auxiliary one-shot unit setting and does not replace thermostat ownership.\n\n### 0.14.1\nAdds split/multisplit installation groups with shared energy and outdoor-temperature sources, multi-target energy charts with period comparison, cleaner Cloud status/alerts and safe MQTT cleanup after the last Cloud device is removed.\n\n### 0.14.0\nAdds first-class GREE Cloud MQTT status/control, reconnect/resubscribe lifecycle, Cloud diagnostics, per-device capabilities, Cloud/HA energy sources and energy-history aggregation while preserving the LAN transport contract.", "license": { "name": "MIT" } @@ -5900,7 +5900,7 @@ }, "version": { "type": "string", - "example": "0.15.0" + "example": "0.14.25" }, "uptime_seconds": { "type": "integer", @@ -5973,38 +5973,16 @@ "maximum": 5 }, "swing_vertical": { - "oneOf": [ - { - "type": "integer", - "minimum": 0, - "maximum": 11 - }, - { - "type": "boolean", - "deprecated": true - }, - { - "type": "null" - } - ], - "description": "Optional raw GREE SwUpDn louver position. Legacy booleans are accepted for compatibility (false=0, true=1)." + "type": [ + "boolean", + "null" + ] }, "swing_horizontal": { - "oneOf": [ - { - "type": "integer", - "minimum": 0, - "maximum": 6 - }, - { - "type": "boolean", - "deprecated": true - }, - { - "type": "null" - } - ], - "description": "Optional raw GREE SwingLfRig louver position. Legacy booleans are accepted for compatibility (false=0, true=1)." + "type": [ + "boolean", + "null" + ] }, "quiet": { "type": [ @@ -6095,38 +6073,16 @@ "maximum": 5 }, "swing_vertical": { - "oneOf": [ - { - "type": "integer", - "minimum": 0, - "maximum": 11 - }, - { - "type": "boolean", - "deprecated": true - }, - { - "type": "null" - } - ], - "description": "Optional raw GREE SwUpDn louver position. Legacy booleans are accepted for compatibility (false=0, true=1)." + "type": [ + "boolean", + "null" + ] }, "swing_horizontal": { - "oneOf": [ - { - "type": "integer", - "minimum": 0, - "maximum": 6 - }, - { - "type": "boolean", - "deprecated": true - }, - { - "type": "null" - } - ], - "description": "Optional raw GREE SwingLfRig louver position. Legacy booleans are accepted for compatibility (false=0, true=1)." + "type": [ + "boolean", + "null" + ] }, "quiet": { "type": [ @@ -6347,16 +6303,10 @@ "maximum": 5 }, "swing_vertical": { - "type": "integer", - "minimum": 0, - "maximum": 11, - "description": "Raw GREE SwUpDn louver position. 0=off/default, 1=full swing; fixed positions use 2..6, and vertical partial swing ranges use 7..11." + "type": "boolean" }, "swing_horizontal": { - "type": "integer", - "minimum": 0, - "maximum": 6, - "description": "Raw GREE SwingLfRig louver position. 0=off/default, 1=full swing; fixed positions use 2..6." + "type": "boolean" }, "quiet": { "type": "boolean" diff --git a/ha-addon/home-assistant/README.md b/ha-addon/home-assistant/README.md index 169eebf..8d572b4 100644 --- a/ha-addon/home-assistant/README.md +++ b/ha-addon/home-assistant/README.md @@ -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, 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. The card UI translates those modes using the Home Assistant language while the underlying mode IDs (for example `fixed_upper`) remain unchanged for services, scripts and automations. +The climate proxy supports power/turn on/off, HVAC modes, target temperature, fan mode, vertical swing and horizontal swing. Automation plan: diff --git a/ha-addon/home-assistant/custom_components/gree_controller/climate.py b/ha-addon/home-assistant/custom_components/gree_controller/climate.py index 650eb1a..6c06b2a 100644 --- a/ha-addon/home-assistant/custom_components/gree_controller/climate.py +++ b/ha-addon/home-assistant/custom_components/gree_controller/climate.py @@ -39,40 +39,6 @@ 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, @@ -160,15 +126,14 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat """Home Assistant climate entity controlled through the Rust service.""" _attr_has_entity_name = True - _attr_translation_key = "direct_control" _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_min_temp = 8.0 _attr_max_temp = 30.0 _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 = list(VERTICAL_SWING_TO_VALUE) - _attr_swing_horizontal_modes = list(HORIZONTAL_SWING_TO_VALUE) + _attr_swing_modes = [SWING_OFF, SWING_ON] + _attr_swing_horizontal_modes = [SWING_OFF, SWING_ON] _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE @@ -235,11 +200,11 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat @property def swing_mode(self) -> str: - return _louver_mode(self._device.get("swing_vertical", 0), VALUE_TO_VERTICAL_SWING) + return SWING_ON if self._device.get("swing_vertical", False) else SWING_OFF @property def swing_horizontal_mode(self) -> str: - return _louver_mode(self._device.get("swing_horizontal", 0), VALUE_TO_HORIZONTAL_SWING) + return SWING_ON if self._device.get("swing_horizontal", False) else SWING_OFF @property def extra_state_attributes(self) -> dict[str, Any]: @@ -254,8 +219,6 @@ 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"), } @@ -287,20 +250,15 @@ 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: - value = VERTICAL_SWING_TO_VALUE.get(swing_mode) - if value is not None: - await self._command({"swing_vertical": value}) + await self._command({"swing_vertical": swing_mode == SWING_ON}) async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None: - value = HORIZONTAL_SWING_TO_VALUE.get(swing_horizontal_mode) - if value is not None: - await self._command({"swing_horizontal": value}) + await self._command({"swing_horizontal": swing_horizontal_mode == SWING_ON}) class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], ClimateEntity): """Full climate entity for a controller thermostat zone.""" _attr_has_entity_name = True - _attr_translation_key = "zone_thermostat" _attr_name = "Thermostat" _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_min_temp = 8.0 @@ -308,8 +266,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 = list(VERTICAL_SWING_TO_VALUE) - _attr_swing_horizontal_modes = list(HORIZONTAL_SWING_TO_VALUE) + _attr_swing_modes = [SWING_OFF, SWING_ON] + _attr_swing_horizontal_modes = [SWING_OFF, SWING_ON] def __init__(self, coordinator: GreeControllerCoordinator, zone_id: str) -> None: super().__init__(coordinator) @@ -385,11 +343,11 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl @property def swing_mode(self) -> str: - return _louver_mode(self._device.get("swing_vertical", 0), VALUE_TO_VERTICAL_SWING) + return SWING_ON if self._device.get("swing_vertical", False) else SWING_OFF @property def swing_horizontal_mode(self) -> str: - return _louver_mode(self._device.get("swing_horizontal", 0), VALUE_TO_HORIZONTAL_SWING) + return SWING_ON if self._device.get("swing_horizontal", False) else SWING_OFF @property def extra_state_attributes(self) -> dict[str, Any]: @@ -420,8 +378,6 @@ 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: @@ -450,15 +406,17 @@ 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() - 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}) + if device_id: + await self.coordinator.async_device_command( + device_id, {"swing_vertical": swing_mode == SWING_ON} + ) async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None: device_id = str(self._zone.get("device_id") or "").strip() - 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}) + if device_id: + await self.coordinator.async_device_command( + device_id, {"swing_horizontal": swing_horizontal_mode == SWING_ON} + ) async def async_turn_on(self) -> None: await self.coordinator.async_zone_control(self._zone_id, {"power": True}) diff --git a/ha-addon/home-assistant/custom_components/gree_controller/coordinator.py b/ha-addon/home-assistant/custom_components/gree_controller/coordinator.py index b59a630..fc3634d 100644 --- a/ha-addon/home-assistant/custom_components/gree_controller/coordinator.py +++ b/ha-addon/home-assistant/custom_components/gree_controller/coordinator.py @@ -67,10 +67,6 @@ 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: diff --git a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json index ee5b094..9f669d6 100644 --- a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json +++ b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "gree_controller", "name": "GREE Controller", - "version": "0.15.0", + "version": "0.14.25", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/ha-addon/home-assistant/custom_components/gree_controller/translations/en.json b/ha-addon/home-assistant/custom_components/gree_controller/translations/en.json index 26bb1dd..898f7a1 100644 --- a/ha-addon/home-assistant/custom_components/gree_controller/translations/en.json +++ b/ha-addon/home-assistant/custom_components/gree_controller/translations/en.json @@ -18,91 +18,5 @@ "abort": { "already_configured": "GREE Controller is already configured" } - }, - "entity": { - "climate": { - "direct_control": { - "name": "Direct control", - "state_attributes": { - "fan_mode": { - "state": { - "auto": "Auto", - "low": "Low", - "medium_low": "Medium-low", - "medium": "Medium", - "medium_high": "Medium-high", - "high": "High" - } - }, - "swing_mode": { - "state": { - "off": "Off", - "on": "Full range (Auto)", - "fixed_upper": "Fixed: Top", - "fixed_upper_middle": "Fixed: Upper-middle", - "fixed_middle": "Fixed: Middle", - "fixed_lower_middle": "Fixed: Lower-middle", - "fixed_lower": "Fixed: Bottom", - "swing_upper": "Swing: Top", - "swing_upper_middle": "Swing: Upper-middle", - "swing_middle": "Swing: Middle", - "swing_lower_middle": "Swing: Lower-middle", - "swing_lower": "Swing: Bottom" - } - }, - "swing_horizontal_mode": { - "state": { - "off": "Off", - "on": "Full range (Auto)", - "fixed_left": "Fixed: Left", - "fixed_left_middle": "Fixed: Left-middle", - "fixed_middle": "Fixed: Middle", - "fixed_right_middle": "Fixed: Middle-right", - "fixed_right": "Fixed: Right" - } - } - } - }, - "zone_thermostat": { - "name": "Thermostat", - "state_attributes": { - "preset_mode": { - "state": { - "auto": "Auto schedule", - "comfort": "Comfort", - "sleep": "Sleep", - "away": "Away" - } - }, - "swing_mode": { - "state": { - "off": "Off", - "on": "Full range (Auto)", - "fixed_upper": "Fixed: Top", - "fixed_upper_middle": "Fixed: Upper-middle", - "fixed_middle": "Fixed: Middle", - "fixed_lower_middle": "Fixed: Lower-middle", - "fixed_lower": "Fixed: Bottom", - "swing_upper": "Swing: Top", - "swing_upper_middle": "Swing: Upper-middle", - "swing_middle": "Swing: Middle", - "swing_lower_middle": "Swing: Lower-middle", - "swing_lower": "Swing: Bottom" - } - }, - "swing_horizontal_mode": { - "state": { - "off": "Off", - "on": "Full range (Auto)", - "fixed_left": "Fixed: Left", - "fixed_left_middle": "Fixed: Left-middle", - "fixed_middle": "Fixed: Middle", - "fixed_right_middle": "Fixed: Middle-right", - "fixed_right": "Fixed: Right" - } - } - } - } - } } } diff --git a/ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json b/ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json index d998502..bf877a9 100644 --- a/ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json +++ b/ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json @@ -18,91 +18,5 @@ "abort": { "already_configured": "GREE Controller jest już skonfigurowany" } - }, - "entity": { - "climate": { - "direct_control": { - "name": "Sterowanie bezpośrednie", - "state_attributes": { - "fan_mode": { - "state": { - "auto": "Automatyczny", - "low": "Niski", - "medium_low": "Średnio-niski", - "medium": "Średni", - "medium_high": "Średnio-wysoki", - "high": "Wysoki" - } - }, - "swing_mode": { - "state": { - "off": "Wyłączony", - "on": "Pełny zakres (Auto)", - "fixed_upper": "Stała: Góra", - "fixed_upper_middle": "Stała: Środek-góra", - "fixed_middle": "Stała: Środek", - "fixed_lower_middle": "Stała: Środek-dół", - "fixed_lower": "Stała: Dół", - "swing_upper": "Ruch: Góra", - "swing_upper_middle": "Ruch: Środek-góra", - "swing_middle": "Ruch: Środek", - "swing_lower_middle": "Ruch: Środek-dół", - "swing_lower": "Ruch: Dół" - } - }, - "swing_horizontal_mode": { - "state": { - "off": "Wyłączony", - "on": "Pełny zakres (Auto)", - "fixed_left": "Stała: Lewo", - "fixed_left_middle": "Stała: Lewo-środek", - "fixed_middle": "Stała: Środek", - "fixed_right_middle": "Stała: Środek-prawo", - "fixed_right": "Stała: Prawo" - } - } - } - }, - "zone_thermostat": { - "name": "Termostat", - "state_attributes": { - "preset_mode": { - "state": { - "auto": "Harmonogram (Auto)", - "comfort": "Komfort", - "sleep": "Sen", - "away": "Poza domem" - } - }, - "swing_mode": { - "state": { - "off": "Wyłączony", - "on": "Pełny zakres (Auto)", - "fixed_upper": "Stała: Góra", - "fixed_upper_middle": "Stała: Środek-góra", - "fixed_middle": "Stała: Środek", - "fixed_lower_middle": "Stała: Środek-dół", - "fixed_lower": "Stała: Dół", - "swing_upper": "Ruch: Góra", - "swing_upper_middle": "Ruch: Środek-góra", - "swing_middle": "Ruch: Środek", - "swing_lower_middle": "Ruch: Środek-dół", - "swing_lower": "Ruch: Dół" - } - }, - "swing_horizontal_mode": { - "state": { - "off": "Wyłączony", - "on": "Pełny zakres (Auto)", - "fixed_left": "Stała: Lewo", - "fixed_left_middle": "Stała: Lewo-środek", - "fixed_middle": "Stała: Środek", - "fixed_right_middle": "Stała: Środek-prawo", - "fixed_right": "Stała: Prawo" - } - } - } - } - } } } diff --git a/ha-addon/repository/gree-controller/CHANGELOG.md b/ha-addon/repository/gree-controller/CHANGELOG.md index dc331d1..135810b 100644 --- a/ha-addon/repository/gree-controller/CHANGELOG.md +++ b/ha-addon/repository/gree-controller/CHANGELOG.md @@ -1,11 +1,10 @@ # Changelog -## 0.15.0 +## 0.14.25 -- 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. -- Home Assistant climate cards localize direct-control names, fan modes, thermostat presets and all vertical/horizontal louver positions while keeping stable technical mode IDs for services and automations. +- WebSocket disconnects now show a yellow reconnecting state instead of an immediate application-style error. +- A persistent WebSocket outage becomes a connection error after 15 seconds while automatic reconnect attempts and HTTP fallback continue unchanged. +- Successful reconnects clear the outage state immediately and stale socket callbacks no longer overwrite the active connection status. ## 0.14.24 diff --git a/ha-addon/repository/gree-controller/config.yaml b/ha-addon/repository/gree-controller/config.yaml index 9b0608a..f257d3a 100644 --- a/ha-addon/repository/gree-controller/config.yaml +++ b/ha-addon/repository/gree-controller/config.yaml @@ -1,5 +1,5 @@ name: "GREE Controller" -version: "0.15.0" +version: "0.14.25" 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/" diff --git a/lang/en.json b/lang/en.json index bfc82a8..6c1f036 100644 --- a/lang/en.json +++ b/lang/en.json @@ -8,6 +8,7 @@ "translations": { "meta.description": "Local GREE air conditioner controller", "status.connecting": "Connecting…", + "status.reconnecting": "Reconnecting…", "status.connected": "Connected", "status.disconnected": "Disconnected", "status.connectionError": "Connection error", @@ -1000,7 +1001,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. 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.", + "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.", "settings.haConnection": "Home Assistant connection", "settings.haConnectionHint": "Server address and credentials used by all Home Assistant features.", "settings.haThermostatSources": "Thermostat temperature sources", @@ -1327,28 +1328,10 @@ "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 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.", + "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.", "history.tabNetwork": "Pings", "history.networkTarget": "Endpoint", "history.allNetworkTargets": "All endpoints", diff --git a/lang/pl.json b/lang/pl.json index ebd2717..4fe77bc 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -8,6 +8,7 @@ "translations": { "meta.description": "Lokalny sterownik klimatyzatorów GREE", "status.connecting": "Łączenie…", + "status.reconnecting": "Ponowne łączenie…", "status.connected": "Połączono", "status.disconnected": "Rozłączono", "status.connectionError": "Błąd połączenia", @@ -1000,7 +1001,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. 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.", + "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.", "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,28 +1328,10 @@ "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": "Ż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", + "devices.swingVertical": "Swing pionowy", + "devices.swingHorizontal": "Swing poziomy", "flow.thermostatDeviceOptions": "Sterowanie żaluzjami jednostki", - "flow.thermostatDeviceOptionsHint": "Pozycje żaluzji pionowej i poziomej są wysyłane do jednostki niezależnie i nie zmieniają logiki termostatu.", + "flow.thermostatDeviceOptionsHint": "Swing pionowy i poziomy są wysyłane do jednostki jako niezależne ustawienia i nie zmieniają logiki termostatu.", "history.tabNetwork": "Pingi", "history.networkTarget": "Punkt pomiaru", "history.allNetworkTargets": "Wszystkie punkty", diff --git a/scripts/live_realtime_test.js b/scripts/live_realtime_test.js index e1b026d..d1c4fcc 100755 --- a/scripts/live_realtime_test.js +++ b/scripts/live_realtime_test.js @@ -31,7 +31,7 @@ const app = { settings: { house_mode: 'cool', debug: { overlay_enabled: false }, home_assistant: {} }, system: {}, outdoorTemperature: null, controlPlan: null, controlPlanRevision: null, controlPlanPushReady: false, controlPlanTimer: null, currentView: 'dashboard', - sensorAliases: {}, flowSharedInputs: [], flowDraft: null, ws: null, wsTimer: null, + sensorAliases: {}, flowSharedInputs: [], flowDraft: null, ws: null, wsTimer: null, wsErrorTimer: null, wsOutageStartedAt: 0, token: '', debugBacklogLoaded: false, }; @@ -278,7 +278,21 @@ async function main() { socket.readyState = FakeWebSocket.CLOSED; socket.onclose(); assert.equal(app.controlPlanPushReady, false); - assert(timers.length >= beforeCloseTimers + 2, 'close must schedule fallback and reconnect'); + assert.equal(calls['connection:reconnecting'], 1, 'short WS outage must show reconnecting state'); + assert(timers.length >= beforeCloseTimers + 3, 'close must schedule fallback, reconnect and delayed error state'); + const errorTimer = timers.find(item => !item.cleared && item.delay === 15000); + assert(errorTimer, 'persistent WS outage must schedule a delayed connection-error state'); + errorTimer.fn(); + assert.equal(calls['connection:connectionError'], 1, 'persistent WS outage must become a connection error'); + + const reconnectTimer = timers.find(item => !item.cleared && item.delay === 3000); + assert(reconnectTimer, 'WS close must schedule reconnect'); + reconnectTimer.fn(); + const reconnectedSocket = FakeWebSocket.instances[FakeWebSocket.instances.length - 1]; + reconnectedSocket.readyState = FakeWebSocket.OPEN; + reconnectedSocket.onopen(); + assert.equal(calls['connection:connected'], 2, 'successful reconnect must restore connected state'); + assert.equal(app.wsOutageStartedAt, 0, 'successful reconnect must clear outage tracking'); console.log(`Live realtime test OK (${apiCalls.length} API fallback/resync calls observed)`); } diff --git a/src/api/configuration.rs b/src/api/configuration.rs index 8a829ce..2b205f0 100644 --- a/src/api/configuration.rs +++ b/src/api/configuration.rs @@ -594,8 +594,8 @@ fn sanitize_imported_device(device: &mut Device, now: chrono::DateTime) { device.mode = "cool".into(); device.target_temperature = 23.0; device.fan_speed = 0; - device.swing_vertical = 0; - device.swing_horizontal = 0; + device.swing_vertical = false; + device.swing_horizontal = false; device.quiet = false; device.turbo = false; device.light = false; diff --git a/src/api/devices.rs b/src/api/devices.rs index c69259c..8a7606d 100644 --- a/src/api/devices.rs +++ b/src/api/devices.rs @@ -116,8 +116,8 @@ async fn add_device( mode: "cool".into(), target_temperature: 24.0, fan_speed: 0, - swing_vertical: 0, - swing_horizontal: 0, + swing_vertical: false, + swing_horizontal: false, quiet: false, quiet_wire_value: None, turbo: false, diff --git a/src/api/flows.rs b/src/api/flows.rs index edb041c..161ac8b 100644 --- a/src/api/flows.rs +++ b/src/api/flows.rs @@ -53,15 +53,6 @@ fn flow_u8(config: &Value, key: &str) -> Option { fn flow_bool(config: &Value, key: &str) -> Option { config.get(key).and_then(Value::as_bool) } -fn flow_louver_position(config: &Value, key: &str) -> Option { - 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()); @@ -1117,8 +1108,8 @@ fn compile_flow( && preset_for_schedule != "auto" && mode_for_schedule == "auto" && flow_bool(&action_node.config, "power").is_none() - && flow_louver_position(&action_node.config, "swing_vertical").is_none() - && flow_louver_position(&action_node.config, "swing_horizontal").is_none(); + && flow_bool(&action_node.config, "swing_vertical").is_none() + && flow_bool(&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()))?; @@ -1296,11 +1287,8 @@ fn compile_flow( } item.action.mode = Some(mode); } - 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)?; + item.action.swing_vertical = flow_bool(&action_node.config, "swing_vertical"); + item.action.swing_horizontal = flow_bool(&action_node.config, "swing_horizontal"); } "device_action" => { let id = flow_string(&action_node.config, "device_id") @@ -1316,10 +1304,8 @@ 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_louver_position(&action_node.config, "swing_vertical"); - item.action.swing_horizontal = - flow_louver_position(&action_node.config, "swing_horizontal"); + item.action.swing_vertical = flow_bool(&action_node.config, "swing_vertical"); + item.action.swing_horizontal = flow_bool(&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"); @@ -1747,8 +1733,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_louver_position(&action.config, "swing_vertical"); - command.swing_horizontal = flow_louver_position(&action.config, "swing_horizontal"); + command.swing_vertical = flow_bool(&action.config, "swing_vertical"); + command.swing_horizontal = flow_bool(&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"); diff --git a/src/api/gree_cloud.rs b/src/api/gree_cloud.rs index 9e3e491..4918dcb 100644 --- a/src/api/gree_cloud.rs +++ b/src/api/gree_cloud.rs @@ -300,8 +300,8 @@ async fn add_gree_cloud_device( mode: "cool".into(), target_temperature: 24.0, fan_speed: 0, - swing_vertical: 0, - swing_horizontal: 0, + swing_vertical: false, + swing_horizontal: false, quiet: false, quiet_wire_value: None, turbo: false, diff --git a/src/engine.rs b/src/engine.rs index c1a34cc..36fbd12 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -4,7 +4,6 @@ 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, diff --git a/src/engine/automations.rs b/src/engine/automations.rs index 48ee876..2a2e54a 100644 --- a/src/engine/automations.rs +++ b/src/engine/automations.rs @@ -264,18 +264,6 @@ 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 { Some(match field { "enabled" => json!(device.enabled), @@ -615,14 +603,8 @@ 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 = 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), - ); + 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); (flow_compare_value(&actual, c.get("operator").and_then(Value::as_str).unwrap_or("eq"), &expected), actual) } "zone_state" => { diff --git a/src/engine/polling.rs b/src/engine/polling.rs index 2eb1127..3928aba 100644 --- a/src/engine/polling.rs +++ b/src/engine/polling.rs @@ -392,20 +392,6 @@ 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())); diff --git a/src/engine/tests.rs b/src/engine/tests.rs index 2352448..d5c821f 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -708,8 +708,8 @@ mod tests { )); assert!(!automation_action_conflicts_with_thermostat( &DeviceCommand { - swing_vertical: Some(1), - swing_horizontal: Some(0), + swing_vertical: Some(true), + swing_horizontal: Some(false), ..Default::default() } )); diff --git a/src/models/device.rs b/src/models/device.rs index 97efa1a..fa4d7eb 100644 --- a/src/models/device.rs +++ b/src/models/device.rs @@ -89,41 +89,6 @@ 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 -where - D: serde::Deserializer<'de>, -{ - Ok(louver_position_compat(LouverPositionCompat::deserialize( - deserializer, - )?)) -} - -fn deserialize_optional_louver_position<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - Ok(Option::::deserialize(deserializer)?.map(louver_position_compat)) -} - #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] #[serde(rename_all = "snake_case")] pub enum EnergySourcePreference { @@ -211,12 +176,10 @@ pub struct Device { pub target_temperature: f64, #[serde(default = "default_fan")] pub fan_speed: u8, - /// 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 swing_vertical: bool, + #[serde(default)] + pub swing_horizontal: bool, #[serde(default)] pub quiet: bool, /// Raw non-zero value last reported by the unit for the GREE `Quiet` property. @@ -327,8 +290,8 @@ impl Device { mode: "cool".into(), target_temperature: 23.0, fan_speed: 0, - swing_vertical: 0, - swing_horizontal: 0, + swing_vertical: false, + swing_horizontal: false, quiet: false, quiet_wire_value: None, turbo: false, @@ -426,12 +389,8 @@ pub struct DeviceCommand { pub mode: Option, pub target_temperature: Option, pub fan_speed: Option, - /// 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, - /// 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, + pub swing_vertical: Option, + pub swing_horizontal: Option, pub quiet: Option, pub turbo: Option, pub light: Option, @@ -517,10 +476,10 @@ impl DeviceCommand { device.fan_speed = v.min(5); } if let Some(v) = self.swing_vertical { - device.swing_vertical = v.min(VERTICAL_SWING_MAX); + device.swing_vertical = v; } if let Some(v) = self.swing_horizontal { - device.swing_horizontal = v.min(HORIZONTAL_SWING_MAX); + device.swing_horizontal = v; } if let Some(v) = self.quiet { device.quiet = v; @@ -585,27 +544,4 @@ 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)); - } } diff --git a/src/protocol/gree.rs b/src/protocol/gree.rs index 720ecf6..59c5251 100644 --- a/src/protocol/gree.rs +++ b/src/protocol/gree.rs @@ -1,10 +1,7 @@ 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, HORIZONTAL_SWING_MAX, - VERTICAL_SWING_MAX, -}; +use crate::models::{ApiEvent, ConnectionStatus, ConnectionType, Device, DeviceCommand}; use anyhow::{anyhow, bail, Context, Result}; use chrono::Utc; use serde_json::{json, Value}; diff --git a/src/protocol/gree/commands.rs b/src/protocol/gree/commands.rs index 6dd75dc..7e5c644 100644 --- a/src/protocol/gree/commands.rs +++ b/src/protocol/gree/commands.rs @@ -191,11 +191,11 @@ impl GreeClient { } if let Some(v) = command.swing_vertical { opt.push("SwUpDn"); - values.push(json!(v.min(VERTICAL_SWING_MAX))); + values.push(json!(if v { 1 } else { 0 })); } if let Some(v) = command.swing_horizontal { opt.push("SwingLfRig"); - values.push(json!(v.min(HORIZONTAL_SWING_MAX))); + values.push(json!(if v { 1 } else { 0 })); } if let Some(v) = command.quiet { opt.push("Quiet"); diff --git a/src/protocol/gree/discovery.rs b/src/protocol/gree/discovery.rs index f2fa412..8b7ec9a 100644 --- a/src/protocol/gree/discovery.rs +++ b/src/protocol/gree/discovery.rs @@ -201,8 +201,8 @@ impl GreeClient { mode: "cool".into(), target_temperature: 24.0, fan_speed: 0, - swing_vertical: 0, - swing_horizontal: 0, + swing_vertical: false, + swing_horizontal: false, quiet: false, quiet_wire_value: None, turbo: false, diff --git a/src/protocol/gree/polling.rs b/src/protocol/gree/polling.rs index 5f07518..0cd9982 100644 --- a/src/protocol/gree/polling.rs +++ b/src/protocol/gree/polling.rs @@ -193,20 +193,8 @@ impl GreeClient { } next.fan_speed = raw as u8; } - "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; - } + "SwUpDn" => next.swing_vertical = status_i64(name, value)? != 0, + "SwingLfRig" => next.swing_horizontal = status_i64(name, value)? != 0, "Quiet" => { let raw = status_i64(name, value)?; if raw < 0 { diff --git a/src/provider.rs b/src/provider.rs index 1c0343d..4955fdc 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,7 +1,6 @@ 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}, @@ -604,9 +603,9 @@ impl GreeCloudProvider { if let Some(value) = command.swing_vertical { simple.push(( "SwUpDn", - json!(value.min(VERTICAL_SWING_MAX)), + json!(if value { 1 } else { 0 }), DeviceCommand { - swing_vertical: Some(value.min(VERTICAL_SWING_MAX)), + swing_vertical: Some(value), ..Default::default() }, )); @@ -614,9 +613,9 @@ impl GreeCloudProvider { if let Some(value) = command.swing_horizontal { simple.push(( "SwingLfRig", - json!(value.min(HORIZONTAL_SWING_MAX)), + json!(if value { 1 } else { 0 }), DeviceCommand { - swing_horizontal: Some(value.min(HORIZONTAL_SWING_MAX)), + swing_horizontal: Some(value), ..Default::default() }, )); @@ -1676,14 +1675,10 @@ pub fn apply_cloud_properties(device: &mut Device, props: &BTreeMap 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; @@ -2918,7 +2891,8 @@ button.outside-pill:focus-visible { box-shadow: 0 0 0 3px rgba(239, 91, 91, .14), 0 0 10px rgba(239, 91, 91, .24); } -.connection-dot.connecting { +.connection-dot.connecting, +.connection-dot.reconnecting { background: #e2a93b; box-shadow: 0 0 0 3px rgba(226, 169, 59, .14); animation: connection-pulse 1.25s ease-in-out infinite; @@ -4054,10 +4028,6 @@ 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)); } @@ -4128,17 +4098,6 @@ 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)); diff --git a/web/index.html b/web/index.html index 1e59ed3..49543d9 100644 --- a/web/index.html +++ b/web/index.html @@ -1760,30 +1760,15 @@ name="action_target_temperature">
- -
${controls.join('')}
` : ''; -} - function manualLocalDeviceCard(device) { const caps = device.capabilities || {}; const modes = ['auto', 'cool', 'dry', 'fan', 'heat']; @@ -163,8 +153,9 @@ function manualLocalDeviceCard(device) { ${compressorQueuePanel(managedZone)}
${modes.map(mode => ``).join('')}
${fans.map(fan => ``).join('')}
- ${deviceLouverControls(device)} -
+
+ ${caps.vertical_swing === false ? '' : ``} + ${caps.horizontal_swing === false ? '' : ``} ${device.supports_quiet === false ? '' : ``} ${device.supports_turbo === false ? '' : ``}
@@ -201,8 +192,9 @@ function manualCloudDeviceCard(device) { ${compressorQueuePanel(managedZone)} ${modes.length ? `
${modes.map(mode => ``).join('')}
` : ''} ${fans.length ? `
${fans.map(fan => ``).join('')}
` : ''} - ${deviceLouverControls(device)} -
+
+ ${caps.vertical_swing === false ? '' : ``} + ${caps.horizontal_swing === false ? '' : ``} ${device.supports_quiet === false ? '' : ``} ${device.supports_turbo === false ? '' : ``}
@@ -572,7 +564,7 @@ function zoneCard(zone, detailed = true) { ${['auto', 'comfort', 'sleep', 'away'].map(preset => ``).join('')}
- ${device ? deviceLouverControls(device, { disabled: deviceUnavailable }) : ''} + ${device ? `
${device.capabilities?.vertical_swing === false ? '' : ``}${device.capabilities?.horizontal_swing === false ? '' : ``}
` : ''}
${esc(runtimeStatus)}${controlGroup ? ` ${esc(controlGroup.name)}` : ''}${esc(override)}
${esc(tr('zones.controlOwner'))}: ${esc(zoneControlOwnerLabel(zone))}${esc(zoneControlOwnerMeta(zone))}
${compressorQueuePanel(zone)} diff --git a/web/js-dynamic/events.js b/web/js-dynamic/events.js index 5561332..6d41aae 100644 --- a/web/js-dynamic/events.js +++ b/web/js-dynamic/events.js @@ -8,15 +8,6 @@ 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; } @@ -203,6 +194,7 @@ 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 }); @@ -357,7 +349,6 @@ $('#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); } @@ -643,7 +634,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 = Number(raw.action_swing_vertical); if (!groupTarget && raw.action_swing_horizontal !== '') action.swing_horizontal = Number(raw.action_swing_horizontal); + 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 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')); diff --git a/web/js-dynamic/flows.js b/web/js-dynamic/flows.js index 07c8578..4c29b50 100644 --- a/web/js-dynamic/flows.js +++ b/web/js-dynamic/flows.js @@ -42,13 +42,6 @@ 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 }; @@ -56,7 +49,6 @@ 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' }; @@ -233,8 +225,6 @@ 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); @@ -331,11 +321,7 @@ 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') { - 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 === '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 === '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'); @@ -344,10 +330,8 @@ 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)} ${value}` + ? `${item.name} · ${sharedFlowInputSourceSummary(item)} · ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}` : `${item.name} · ${tr('flow.selectOperator')}`; } return `${item.name} · ${sharedFlowInputSourceSummary(item)}`; @@ -497,14 +481,12 @@ 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 ? `` : ''}${fullOperators ? flowOperatorOptions(selected) : [['eq', '='], ['neq', '≠']].map(([value, label]) => ``).join('')}`; const value = config.value ?? defaults.value ?? ''; let valueField = ``; - if (louverAxis) valueField = ``; - else if (numeric) valueField = ``; + if (numeric) valueField = ``; else if (item.kind === 'house_mode') valueField = ``; return `

${esc(tr('flow.sharedInputFlowComparisonHint'))}

`; } @@ -534,7 +516,7 @@ function renderFlowInspector() { else if (node.kind === 'ha_attribute') fields = `${flowTextComparisonFields(c, true)}`; else if (node.kind === 'ha_available') fields = `

${esc(tr('flow.haAvailableHint'))}

`; else if (node.kind === 'house_mode') fields = `
`; - else if (node.kind === 'device_state') fields = `${['swing_vertical', 'swing_horizontal'].includes(c.field) ? flowLouverComparisonFields(c, c.field === 'swing_horizontal' ? 'horizontal' : 'vertical') : flowTextComparisonFields(c)}`; + else if (node.kind === 'device_state') fields = `${flowTextComparisonFields(c)}`; else if (node.kind === 'zone_state') fields = `${flowTextComparisonFields(c)}`; else if (node.kind === 'group_state') fields = `${flowTextComparisonFields(c)}`; else if (node.kind === 'night_mode') fields = `

${esc(tr('flow.nightModeHint'))}

`; @@ -548,7 +530,7 @@ function renderFlowInspector() { else if (node.kind === 'logic_and') fields = `

${esc(tr('flow.andHint'))}

`; else if (node.kind === 'logic_or') fields = `

${esc(tr('flow.orHint'))}

`; else if (node.kind === 'logic_not') fields = `

${esc(tr('flow.notHint'))}

`; - else if (node.kind === 'zone_thermostat') fields = `
${esc(tr('flow.thermostatDeviceOptions'))}
${flowOptionalLouverField(c, 'swing_vertical', 'vertical')}${flowOptionalLouverField(c, 'swing_horizontal', 'horizontal')}

${esc(tr('flow.thermostatDeviceOptionsHint'))}

`; + else if (node.kind === 'zone_thermostat') fields = `
${esc(tr('flow.thermostatDeviceOptions'))}
${flowOptionalBoolField(c, 'swing_vertical')}${flowOptionalBoolField(c, 'swing_horizontal')}

${esc(tr('flow.thermostatDeviceOptionsHint'))}

`; else if (node.kind === 'device_action') fields = `${flowActionFields(c, false)}`; else if (node.kind === 'group_action') fields = `${flowActionFields(c, true)}`; else if (node.kind === 'ha_service_action') fields = `

${esc(tr('flow.serviceExample'))}

`; @@ -556,18 +538,10 @@ function renderFlowInspector() { } function flowTextComparisonFields(c, numeric = false) { const options = numeric ? flowOperatorOptions(c.operator || 'eq') : [['eq', '='], ['neq', '≠']].map(([v, l]) => ``).join(''); return `
`; } -function flowLouverComparisonFields(c, axis) { - const options = [['eq', '='], ['neq', '≠']].map(([v, l]) => ``).join(''); - const value = Number.isInteger(Number(c.value)) ? Number(c.value) : 0; - return `
`; -} function flowComparisonFields(c, temperature = true) { return `
`; } function flowOptionalBoolField(c, key) { return ``; } -function flowOptionalLouverField(c, key, axis) { - return ``; -} function flowActionFields(c, group) { const modes = group ? ['auto', 'house', 'heat', 'cool'] : ['auto', 'cool', 'dry', 'fan', 'heat']; const modeOptions = modes.map(v => ``).join(''); @@ -575,7 +549,7 @@ function flowActionFields(c, group) { const target = group ? `` : ``; - const deviceOptions = group ? '' : `
${esc(tr('flow.deviceOptions'))}
${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')}

${esc(tr('flow.deviceOptionsHint'))}

`; + const deviceOptions = group ? '' : `
${esc(tr('flow.deviceOptions'))}
${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')}

${esc(tr('flow.deviceOptionsHint'))}

`; return `${base}${target}${deviceOptions}`; } @@ -980,28 +954,13 @@ 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); - const control = louverAxis - ? `` - : ``; - return ``; + return ``; }).join('') : `

${esc(tr('flow.noSimulationOverrides'))}

`; } function collectFlowSimulationOverrides() { @@ -1010,7 +969,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 && (flowSimulationLouverAxis(node) || ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(effectiveKind))) value = Number(value); + if (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; }); @@ -1166,18 +1125,10 @@ 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 = Boolean(sharedFlowInputLouverAxis(item)) || ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(item?.kind); + const numeric = ['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 (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 (['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 (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)); diff --git a/web/js-dynamic/realtime.js b/web/js-dynamic/realtime.js index 511d407..419b321 100644 --- a/web/js-dynamic/realtime.js +++ b/web/js-dynamic/realtime.js @@ -131,17 +131,58 @@ async function handleWebSocketMessage(event) { } catch (_) { } } +const WEBSOCKET_RECONNECT_DELAY_MS = 3000; +const WEBSOCKET_ERROR_DELAY_MS = 15000; + +function clearWebSocketOutage() { + app.wsOutageStartedAt = 0; + clearTimeout(app.wsErrorTimer); + app.wsErrorTimer = null; +} + +function markWebSocketOutage() { + if (!app.wsOutageStartedAt) app.wsOutageStartedAt = Date.now(); + const elapsed = Date.now() - app.wsOutageStartedAt; + if (elapsed >= WEBSOCKET_ERROR_DELAY_MS) { + updateConnectionIndicator('connectionError'); + return; + } + updateConnectionIndicator('reconnecting'); + if (app.wsErrorTimer) return; + app.wsErrorTimer = setTimeout(() => { + app.wsErrorTimer = null; + if (app.ws?.readyState === WebSocket.OPEN || !app.wsOutageStartedAt) return; + updateConnectionIndicator('connectionError'); + }, WEBSOCKET_ERROR_DELAY_MS - elapsed); +} + function connectWebSocket() { if (app.ws && [WebSocket.OPEN, WebSocket.CONNECTING].includes(app.ws.readyState)) return; clearTimeout(app.wsTimer); + app.wsTimer = null; const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; const query = app.token ? `?token=${encodeURIComponent(app.token)}` : ''; const ws = new WebSocket(`${protocol}//${location.host}${withBase('/ws')}${query}`); app.ws = ws; - ws.onopen = () => updateConnectionIndicator('connected'); - ws.onclose = () => { app.controlPlanPushReady = false; updateConnectionIndicator('disconnected'); scheduleControlPlanLoad(0); app.wsTimer = setTimeout(connectWebSocket, 3000); }; - ws.onerror = () => updateConnectionIndicator('connectionError'); + ws.onopen = () => { + if (app.ws !== ws) return; + clearWebSocketOutage(); + updateConnectionIndicator('connected'); + }; + ws.onclose = () => { + if (app.ws !== ws) return; + app.controlPlanPushReady = false; + markWebSocketOutage(); + scheduleControlPlanLoad(0); + app.wsTimer = setTimeout(connectWebSocket, WEBSOCKET_RECONNECT_DELAY_MS); + }; + // Browser WebSocket errors are transport-level signals and are normally followed by `close`. + // Let `close` drive reconnect state so a transient error does not look like an application failure. + ws.onerror = () => { + if (app.ws === ws) console.warn('WebSocket transport error; waiting for close/reconnect'); + }; let messageQueue = Promise.resolve(); ws.onmessage = event => { + if (app.ws !== ws) return; messageQueue = messageQueue.then(() => handleWebSocketMessage(event)).catch(() => { }); }; } diff --git a/web/js-dynamic/settings-ui.js b/web/js-dynamic/settings-ui.js index d22c54e..1ef854d 100644 --- a/web/js-dynamic/settings-ui.js +++ b/web/js-dynamic/settings-ui.js @@ -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')} · ${deviceCommandFieldLabel(c.field || 'state')}`; + 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 === '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 });