This commit is contained in:
Mateusz Gruszczyński
2026-09-17 21:46:10 +02:00
parent cafbf6e5fa
commit 7d9e6c8189
20 changed files with 281 additions and 71 deletions
Generated
+1 -1
View File
@@ -700,7 +700,7 @@ dependencies = [
[[package]] [[package]]
name = "gree-controller" name = "gree-controller"
version = "0.15.1" version = "0.15.2"
dependencies = [ dependencies = [
"aes", "aes",
"aes-gcm", "aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "gree-controller" name = "gree-controller"
version = "0.15.1" version = "0.15.2"
edition = "2021" edition = "2021"
authors = ["GREE Controller contributors"] authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support" description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+19 -19
View File
@@ -1,14 +1,14 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
852768ed3da99ac2e9bbe6c9b67e9a4a667e7115ad9482b6ce4a29ff72d8f914 ./Cargo.lock 82bb41f94fdf5c66ebd9e6caa92e0f1794431067298e0a27c95c8e5bfd68de89 ./Cargo.lock
8e58dd01d95c93ea4f5d0cc5e379cb192221705c9dda41b7b2b7182c49dbff0f ./Cargo.toml 39bdb16196253559e3487aa912ff770c09c7b08cdfbb21e9db1d8af1304ea7e0 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
4a6570c14ba56fc9a4921de6726816264e961a5b82a7fe3fcf475ef96417dad9 ./README.md 84a0803bc75dc3199e23ef56c11c6950e8bd888c686ecda7cc369829e8e3073f ./README.md
283ffafdffd463f5d670acaba251fb92fab7c88e51a6fc1e06732f6cade88b5e ./SOURCE_MANIFEST.sha256 f21732756af313565bfe0788230e45cc11c384cee1187f006d426c5463183c0c ./SOURCE_MANIFEST.sha256
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
9873e774746783d65e447b439d069f84c13d2f4e556661e4eb509af59293c2f7 ./docs/API.md 604a795dec077dd7e4f3462a564fdda68e8084aef07da0fcf3dfb6053a46cc58 ./docs/API.md
58b79297607303151c1de2da74344cd4f6277bc53d80ebe04ed4567d906c963b ./docs/FLOW.md a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md
9483ad7cb1e0c8e38f08c722600b6ba059d4273cfb9dcd1af57c11de522d187b ./docs/openapi.json 8babaa6830fa70b2d3d8208ef51808e79c9d68c4e007b9ba1537fe582ba03b3d ./docs/openapi.json
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile 0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile
adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md
@@ -22,7 +22,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py 6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
a6d69fad7ea1f22436e9f643ceb36c206f1450d8337aede0c5d7a1f0613baeeb ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json d467809d894b8cb9e8e04a2cc279e0cec3227c1d1c689098bdc3c0380aa9043e ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py 39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
@@ -30,10 +30,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json 50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
ea5c4eb4ca2656f8d9a2be37a35ba04bb4c53d735c9c04b416b112e456699cff ./ha-addon/repository/gree-controller/CHANGELOG.md e3a227ffe23368b152d0ba4a4e81a95b70d35e9870b2a8dc7b2048d6901a7f05 ./ha-addon/repository/gree-controller/CHANGELOG.md
1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.md 1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.md
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
c16d31ae815dc4e324ce56b0b46529bcc78c5223688c33aa30d7463abe4f1184 ./ha-addon/repository/gree-controller/config.yaml 21b82216d0d84cc66ecbed69457caf61174fa9304528fe051c98e8ef7c634025 ./ha-addon/repository/gree-controller/config.yaml
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png 3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png 0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml 2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
@@ -43,8 +43,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
f7fc1aee1129cee46a30d5876e3e53f11d2661925eae19cb1e14b47deae83e9e ./lang/en.json e5c6a6051381e28d74b75e48232de13d02eb87519d29c847b7ab7979ce47d082 ./lang/en.json
c05d100e9efa1a82747ffc709157a9bdd6d109b7a80c6e140d582a28d8c3de13 ./lang/pl.json 02c650f603bca1bd17d9d59193c176ee6ddccb8304f2e658d7ba7730f8ff5f83 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -107,7 +107,7 @@ b24092f34a46626d64628b510eba82193f0b203fd097f6a621b91af5d91eb571 ./src/api/auth
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs 5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
b9a9a68e47fdaa577c28063fa68bf95075bc355bff81addca4ee448ef70dd818 ./src/api/flows.rs 2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
3cc64b9c992805f3fbf01c0785cf29ce3121750bf30bd6420ed6e5cf2f8df422 ./src/api/history.rs 3cc64b9c992805f3fbf01c0785cf29ce3121750bf30bd6420ed6e5cf2f8df422 ./src/api/history.rs
@@ -203,19 +203,19 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree
2822a98a4d7f72f2b89b70878aa0f9bd3f391fde4ea04ba1b94e89a527dcd1fb ./web/css/styles.css 2822a98a4d7f72f2b89b70878aa0f9bd3f391fde4ea04ba1b94e89a527dcd1fb ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
263d5554e62d2dd26b251f2c43a7e4ec14c8d8d790fa67e55c9bebf16f9d55c0 ./web/index.html d5115a9dc59b4e753dcff9239a77fe49886951d7a4ae7456be80d9201cc42365 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
bb2b49361103145bd8c502f2867962d48ced409c1a1474af1214c4d8d0b378b2 ./web/js-dynamic/core.js b5fd43eeb9c15fe8289b9e718dfbc2651417379462c4b16b88e090b436abf7c5 ./web/js-dynamic/core.js
752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js 752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js
0e1992a7b93767f1fe4634121ae5aa43900d99f8c2614c2a4cbd5770a49b87c1 ./web/js-dynamic/entities.js 6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js
eeeef31f767f0aa3efb78bc44a2fea2b9eb86f4d4c50cf1ccae69b5ca30cdfda ./web/js-dynamic/events.js a48cb69ae4a2d0d6d512baef346fe499a5a6ea267e071856829df982d4a0f129 ./web/js-dynamic/events.js
22da18288de3282245f88f9f525d9ad29ec027571e7ea893720a9470170eccd2 ./web/js-dynamic/flows.js 8e0f294ceeec1d258edb56271469a8a05381e6443a475f76fbeded6637d17503 ./web/js-dynamic/flows.js
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js 3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
86031d4f7f7d1b46412c8fbb09ff1ad1b2b4cd84eb949bc749b27663f1dc27fa ./web/js-dynamic/navigation.js c974b6ef41ce8b6def35f5349849a06855c46ec31cb4122a39fcb5c78f6d362a ./web/js-dynamic/navigation.js
0e398bad2cde85d4a62d8733c06e73f2b6a5b07db791d4570feba38c7ff8741f ./web/js-dynamic/realtime.js 0e398bad2cde85d4a62d8733c06e73f2b6a5b07db791d4570feba38c7ff8741f ./web/js-dynamic/realtime.js
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js 1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
+2 -2
View File
@@ -2,9 +2,9 @@
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API. Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
**Version: 0.15.1** **Version: 0.15.2**
Version 0.15.1 improves WebSocket connection-state handling in the Web UI: transient disconnects show a reconnecting state, persistent failures escalate after a short grace period, and successful reconnects restore the connected state without affecting the existing HTTP fallback. Version 0.15.2 adds granular single-function unit actions to Visual Flow and exposes fan/Quiet/Turbo/Panel Light/Air/X-Fan/Health/Sleep controls in legacy automations, so automations can change one unit feature without touching unrelated settings.
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples. > [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
+18 -18
View File
@@ -1,13 +1,13 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
852768ed3da99ac2e9bbe6c9b67e9a4a667e7115ad9482b6ce4a29ff72d8f914 ./Cargo.lock 82bb41f94fdf5c66ebd9e6caa92e0f1794431067298e0a27c95c8e5bfd68de89 ./Cargo.lock
8e58dd01d95c93ea4f5d0cc5e379cb192221705c9dda41b7b2b7182c49dbff0f ./Cargo.toml 39bdb16196253559e3487aa912ff770c09c7b08cdfbb21e9db1d8af1304ea7e0 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
4a6570c14ba56fc9a4921de6726816264e961a5b82a7fe3fcf475ef96417dad9 ./README.md 84a0803bc75dc3199e23ef56c11c6950e8bd888c686ecda7cc369829e8e3073f ./README.md
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
9873e774746783d65e447b439d069f84c13d2f4e556661e4eb509af59293c2f7 ./docs/API.md 604a795dec077dd7e4f3462a564fdda68e8084aef07da0fcf3dfb6053a46cc58 ./docs/API.md
58b79297607303151c1de2da74344cd4f6277bc53d80ebe04ed4567d906c963b ./docs/FLOW.md a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md
9483ad7cb1e0c8e38f08c722600b6ba059d4273cfb9dcd1af57c11de522d187b ./docs/openapi.json 8babaa6830fa70b2d3d8208ef51808e79c9d68c4e007b9ba1537fe582ba03b3d ./docs/openapi.json
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile 0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile
adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md
@@ -21,7 +21,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py 6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
a6d69fad7ea1f22436e9f643ceb36c206f1450d8337aede0c5d7a1f0613baeeb ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json d467809d894b8cb9e8e04a2cc279e0cec3227c1d1c689098bdc3c0380aa9043e ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py 39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
@@ -29,10 +29,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json 50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
ea5c4eb4ca2656f8d9a2be37a35ba04bb4c53d735c9c04b416b112e456699cff ./ha-addon/repository/gree-controller/CHANGELOG.md e3a227ffe23368b152d0ba4a4e81a95b70d35e9870b2a8dc7b2048d6901a7f05 ./ha-addon/repository/gree-controller/CHANGELOG.md
1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.md 1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.md
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
c16d31ae815dc4e324ce56b0b46529bcc78c5223688c33aa30d7463abe4f1184 ./ha-addon/repository/gree-controller/config.yaml 21b82216d0d84cc66ecbed69457caf61174fa9304528fe051c98e8ef7c634025 ./ha-addon/repository/gree-controller/config.yaml
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png 3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png 0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml 2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
@@ -42,8 +42,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
f7fc1aee1129cee46a30d5876e3e53f11d2661925eae19cb1e14b47deae83e9e ./lang/en.json e5c6a6051381e28d74b75e48232de13d02eb87519d29c847b7ab7979ce47d082 ./lang/en.json
c05d100e9efa1a82747ffc709157a9bdd6d109b7a80c6e140d582a28d8c3de13 ./lang/pl.json 02c650f603bca1bd17d9d59193c176ee6ddccb8304f2e658d7ba7730f8ff5f83 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -106,7 +106,7 @@ b24092f34a46626d64628b510eba82193f0b203fd097f6a621b91af5d91eb571 ./src/api/auth
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs 5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
b9a9a68e47fdaa577c28063fa68bf95075bc355bff81addca4ee448ef70dd818 ./src/api/flows.rs 2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
3cc64b9c992805f3fbf01c0785cf29ce3121750bf30bd6420ed6e5cf2f8df422 ./src/api/history.rs 3cc64b9c992805f3fbf01c0785cf29ce3121750bf30bd6420ed6e5cf2f8df422 ./src/api/history.rs
@@ -202,19 +202,19 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree
2822a98a4d7f72f2b89b70878aa0f9bd3f391fde4ea04ba1b94e89a527dcd1fb ./web/css/styles.css 2822a98a4d7f72f2b89b70878aa0f9bd3f391fde4ea04ba1b94e89a527dcd1fb ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
263d5554e62d2dd26b251f2c43a7e4ec14c8d8d790fa67e55c9bebf16f9d55c0 ./web/index.html d5115a9dc59b4e753dcff9239a77fe49886951d7a4ae7456be80d9201cc42365 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
bb2b49361103145bd8c502f2867962d48ced409c1a1474af1214c4d8d0b378b2 ./web/js-dynamic/core.js b5fd43eeb9c15fe8289b9e718dfbc2651417379462c4b16b88e090b436abf7c5 ./web/js-dynamic/core.js
752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js 752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js
0e1992a7b93767f1fe4634121ae5aa43900d99f8c2614c2a4cbd5770a49b87c1 ./web/js-dynamic/entities.js 6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js
eeeef31f767f0aa3efb78bc44a2fea2b9eb86f4d4c50cf1ccae69b5ca30cdfda ./web/js-dynamic/events.js a48cb69ae4a2d0d6d512baef346fe499a5a6ea267e071856829df982d4a0f129 ./web/js-dynamic/events.js
22da18288de3282245f88f9f525d9ad29ec027571e7ea893720a9470170eccd2 ./web/js-dynamic/flows.js 8e0f294ceeec1d258edb56271469a8a05381e6443a475f76fbeded6637d17503 ./web/js-dynamic/flows.js
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js 3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
86031d4f7f7d1b46412c8fbb09ff1ad1b2b4cd84eb949bc749b27663f1dc27fa ./web/js-dynamic/navigation.js c974b6ef41ce8b6def35f5349849a06855c46ec31cb4122a39fcb5c78f6d362a ./web/js-dynamic/navigation.js
0e398bad2cde85d4a62d8733c06e73f2b6a5b07db791d4570feba38c7ff8741f ./web/js-dynamic/realtime.js 0e398bad2cde85d4a62d8733c06e73f2b6a5b07db791d4570feba38c7ff8741f ./web/js-dynamic/realtime.js
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js 1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
+9 -3
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference # GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.15.1**. HTTP and WebSocket API for GREE Controller **0.15.2**.
[← Main documentation](../README.md) [← Main documentation](../README.md)
@@ -240,7 +240,7 @@ Response:
{ {
"status": "ok", "status": "ok",
"name": "gree-controller", "name": "gree-controller",
"version": "0.15.1", "version": "0.15.2",
"uptime_seconds": 1234, "uptime_seconds": 1234,
"control_ready": true, "control_ready": true,
"time": "2026-08-30T06:54:00Z" "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": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []},
"control_plan_revision": 42, "control_plan_revision": 42,
"system": { "system": {
"version": "0.15.1", "version": "0.15.2",
"uptime_seconds": 1234, "uptime_seconds": 1234,
"auth_required": false, "auth_required": false,
"control_ready": true, "control_ready": true,
@@ -1694,6 +1694,12 @@ Additional Flow condition blocks are `house_mode`, `device_state`, `zone_state`,
## Single-function unit automation — 0.15.2
Visual Flow adds `device_feature_action`, a dedicated action block that targets one GREE unit and sends exactly one `DeviceCommand` field. Supported fields are power, HVAC mode, target temperature, fan speed, vertical/horizontal louver position, Quiet, Turbo, Panel Light, Air, X-Fan, Health and Sleep. The block compiles to the normal Flow-owned `Automation` path, so cooldown, ownership checks and thermostat conflict protection remain unchanged.
Legacy direct-device automations expose fan speed and the same optional Quiet/Turbo/Panel Light/Air/X-Fan/Health/Sleep fields. Leaving all other fields as `No change` sends only the selected function.
## Granular louver positions — 0.15.0 ## 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. 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.
+2 -1
View File
@@ -17,6 +17,7 @@ Flow is an authoring/orchestration layer. It does not replace the thermostat, sc
- `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 granular vertical/horizontal louver commands to the assigned unit; these auxiliary fields do not take over thermostat ownership.
- `group_action` calls the existing `control_group` path, including custom group targets. - `group_action` calls the existing `control_group` path, including custom group targets.
- `device_action` uses the existing `DeviceCommand` / automatic-device path. - `device_action` uses the existing `DeviceCommand` / automatic-device path.
- `device_feature_action` uses the same path but compiles exactly one selected `DeviceCommand` field, leaving all other unit settings untouched.
- Thermostat hysteresis, compressor protection, schedule hand-back, manual/local ownership and Temporary Quick Thermostat keep their existing priority. - 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/louver/air/xfan/health remain available through the normal command path.
@@ -61,7 +62,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. 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, full GREE device command, single GREE function, climate group, and generic Home Assistant service calls (`domain`, `service`, optional `entity_id`, JSON service data). Direct GREE actions expose power, HVAC mode, target temperature, fan speed, vertical/horizontal louver position, quiet, turbo, light, air, xfan, health and sleep. The single-function block writes exactly one of those fields.
## Built-in presets ## Built-in presets
+3 -3
View File
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
{ {
"domain": "gree_controller", "domain": "gree_controller",
"name": "GREE Controller", "name": "GREE Controller",
"version": "0.15.1", "version": "0.15.2",
"config_flow": true, "config_flow": true,
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
@@ -1,5 +1,10 @@
# Changelog # Changelog
## 0.15.2
- Adds a dedicated Visual Flow action for changing exactly one GREE unit function without modifying unrelated settings.
- Legacy device automations now expose fan speed plus Quiet, Turbo, Panel Light, Air, X-Fan, Health and Sleep as optional single-field controls.
## 0.15.1 ## 0.15.1
- Web UI now treats temporary WebSocket loss as a reconnecting state, escalates to a connection error only after 15 seconds, and returns cleanly to connected after recovery while preserving the existing HTTP fallback. - Web UI now treats temporary WebSocket loss as a reconnecting state, escalates to a connection error only after 15 seconds, and returns cleanly to connected after recovery while preserving the existing HTTP fallback.
@@ -1,5 +1,5 @@
name: "GREE Controller" name: "GREE Controller"
version: "0.15.1" version: "0.15.2"
slug: "gree_controller" slug: "gree_controller"
description: "Local GREE HVAC controller with Web UI and Home Assistant integration" description: "Local GREE HVAC controller with Web UI and Home Assistant integration"
url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/" url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/"
+6
View File
@@ -652,6 +652,9 @@
"groups.noZones": "Create thermostat zones before creating a group.", "groups.noZones": "Create thermostat zones before creating a group.",
"automations.targetType": "Target type", "automations.targetType": "Target type",
"automations.groupHint": "For a group, Auto means follow the whole-house heating/cooling mode. Group profiles are applied to all member zones.", "automations.groupHint": "For a group, Auto means follow the whole-house heating/cooling mode. Group profiles are applied to all member zones.",
"automations.singleDeviceFunctions": "Single device functions",
"automations.deviceFunction": "Unit function",
"automations.singleDeviceFunctionsHint": "Leave the other fields as No change to modify only the selected unit function. Fan, Quiet and Sleep still follow the existing thermostat ownership rules.",
"simulation.openTab": "Open simulator only in new tab", "simulation.openTab": "Open simulator only in new tab",
"simulation.fullscreen": "Full screen", "simulation.fullscreen": "Full screen",
"simulation.exitFullscreen": "Exit full screen", "simulation.exitFullscreen": "Exit full screen",
@@ -952,6 +955,7 @@
"flow.node.not": "NOT", "flow.node.not": "NOT",
"flow.node.thermostat": "Thermostat", "flow.node.thermostat": "Thermostat",
"flow.node.greeDevice": "GREE device", "flow.node.greeDevice": "GREE device",
"flow.node.greeFeature": "Single GREE function",
"flow.node.group": "Group", "flow.node.group": "Group",
"common.yes": "Yes", "common.yes": "Yes",
"common.no": "No", "common.no": "No",
@@ -1002,6 +1006,8 @@
"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.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.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. 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.deviceFeature": "Unit function",
"flow.deviceFeatureHint": "This action sends exactly one DeviceCommand field. All other unit settings remain unchanged.",
"settings.haConnection": "Home Assistant connection", "settings.haConnection": "Home Assistant connection",
"settings.haConnectionHint": "Server address and credentials used by all Home Assistant features.", "settings.haConnectionHint": "Server address and credentials used by all Home Assistant features.",
"settings.haThermostatSources": "Thermostat temperature sources", "settings.haThermostatSources": "Thermostat temperature sources",
+6
View File
@@ -652,6 +652,9 @@
"groups.noZones": "Najpierw utwórz strefy termostatów.", "groups.noZones": "Najpierw utwórz strefy termostatów.",
"automations.targetType": "Typ celu", "automations.targetType": "Typ celu",
"automations.groupHint": "Dla grupy Auto oznacza dziedziczenie globalnego trybu grzania/chłodzenia. Profil grupy jest stosowany do wszystkich stref w grupie.", "automations.groupHint": "Dla grupy Auto oznacza dziedziczenie globalnego trybu grzania/chłodzenia. Profil grupy jest stosowany do wszystkich stref w grupie.",
"automations.singleDeviceFunctions": "Pojedyncze funkcje urządzenia",
"automations.deviceFunction": "Funkcja jednostki",
"automations.singleDeviceFunctionsHint": "Pozostaw pozostałe pola jako Bez zmian, aby zmienić wyłącznie wybraną funkcję jednostki. Fan, Quiet i Sleep nadal respektują istniejące zasady właściciela termostatu.",
"simulation.openTab": "Otwórz sam symulator w nowej karcie", "simulation.openTab": "Otwórz sam symulator w nowej karcie",
"simulation.fullscreen": "Pełny ekran", "simulation.fullscreen": "Pełny ekran",
"simulation.exitFullscreen": "Wyjdź z pełnego ekranu", "simulation.exitFullscreen": "Wyjdź z pełnego ekranu",
@@ -952,6 +955,7 @@
"flow.node.not": "NOT", "flow.node.not": "NOT",
"flow.node.thermostat": "Termostat", "flow.node.thermostat": "Termostat",
"flow.node.greeDevice": "Urządzenie GREE", "flow.node.greeDevice": "Urządzenie GREE",
"flow.node.greeFeature": "Pojedyncza funkcja GREE",
"flow.node.group": "Grupa", "flow.node.group": "Grupa",
"common.yes": "Tak", "common.yes": "Tak",
"common.no": "Nie", "common.no": "Nie",
@@ -1002,6 +1006,8 @@
"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.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.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. 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.deviceFeature": "Funkcja jednostki",
"flow.deviceFeatureHint": "Ta akcja wysyła dokładnie jedno pole DeviceCommand. Pozostałe ustawienia jednostki nie są zmieniane.",
"settings.haConnection": "Połączenie z Home Assistant", "settings.haConnection": "Połączenie z Home Assistant",
"settings.haConnectionHint": "Adres serwera i dane dostępu używane przez wszystkie funkcje Home Assistant.", "settings.haConnectionHint": "Adres serwera i dane dostępu używane przez wszystkie funkcje Home Assistant.",
"settings.haThermostatSources": "Źródła temperatury sterowania", "settings.haThermostatSources": "Źródła temperatury sterowania",
+104 -7
View File
@@ -63,6 +63,66 @@ fn flow_louver_position(config: &Value, key: &str) -> Option<u8> {
} }
} }
fn flow_device_feature_command(config: &Value) -> Result<DeviceCommand, AppError> {
let feature = flow_string(config, "feature")
.ok_or_else(|| AppError::BadRequest("device feature action needs a feature".into()))?;
let mut command = DeviceCommand::default();
match feature.as_str() {
"power" => {
command.power = Some(flow_bool(config, "value").ok_or_else(|| {
AppError::BadRequest("device feature power needs a boolean value".into())
})?);
}
"mode" => {
command.mode = Some(flow_string(config, "value").ok_or_else(|| {
AppError::BadRequest("device feature mode needs a value".into())
})?);
}
"target_temperature" => {
command.target_temperature = Some(flow_f64(config, "value").ok_or_else(|| {
AppError::BadRequest("device feature target temperature needs a numeric value".into())
})?);
}
"fan_speed" => {
command.fan_speed = Some(flow_u8(config, "value").ok_or_else(|| {
AppError::BadRequest("device feature fan speed needs an integer value".into())
})?);
}
"swing_vertical" => {
command.swing_vertical = Some(flow_louver_position(config, "value").ok_or_else(|| {
AppError::BadRequest("device feature vertical louver needs a position".into())
})?);
}
"swing_horizontal" => {
command.swing_horizontal = Some(flow_louver_position(config, "value").ok_or_else(|| {
AppError::BadRequest("device feature horizontal louver needs a position".into())
})?);
}
"quiet" | "turbo" | "light" | "air" | "xfan" | "health" | "sleep" => {
let value = flow_bool(config, "value").ok_or_else(|| {
AppError::BadRequest("device feature action needs an on/off value".into())
})?;
match feature.as_str() {
"quiet" => command.quiet = Some(value),
"turbo" => command.turbo = Some(value),
"light" => command.light = Some(value),
"air" => command.air = Some(value),
"xfan" => command.xfan = Some(value),
"health" => command.health = Some(value),
"sleep" => command.sleep = Some(value),
_ => unreachable!(),
}
}
_ => {
return Err(AppError::BadRequest(format!(
"unsupported device feature action: {feature}"
)))
}
}
engine::validate_command(&command)?;
Ok(command)
}
fn generated_flow_name(flow_id: &str, action_node_id: &str) -> String { fn generated_flow_name(flow_id: &str, action_node_id: &str) -> String {
let digest = Sha256::digest(format!("{flow_id}:{action_node_id}").as_bytes()); let digest = Sha256::digest(format!("{flow_id}:{action_node_id}").as_bytes());
format!("flow-{}", URL_SAFE_NO_PAD.encode(&digest[..12])) format!("flow-{}", URL_SAFE_NO_PAD.encode(&digest[..12]))
@@ -104,7 +164,11 @@ fn flow_logic_kind(kind: &str) -> bool {
fn flow_action_kind(kind: &str) -> bool { fn flow_action_kind(kind: &str) -> bool {
matches!( matches!(
kind, kind,
"zone_thermostat" | "device_action" | "group_action" | "ha_service_action" "zone_thermostat"
| "device_action"
| "device_feature_action"
| "group_action"
| "ha_service_action"
) )
} }
@@ -1332,6 +1396,18 @@ fn compile_flow(
return Err(AppError::BadRequest("device action cannot be empty".into())); return Err(AppError::BadRequest("device action cannot be empty".into()));
} }
} }
"device_feature_action" => {
let id = flow_string(&action_node.config, "device_id").ok_or_else(|| {
AppError::BadRequest("device feature action needs a device".into())
})?;
if !devices.iter().any(|d| d.id == id) {
return Err(AppError::BadRequest(
"device feature action references a missing device".into(),
));
}
item.action_device_id = id;
item.action = flow_device_feature_command(&action_node.config)?;
}
"ha_service_action" => { "ha_service_action" => {
let domain = flow_string(&action_node.config, "domain").ok_or_else(|| { let domain = flow_string(&action_node.config, "domain").ok_or_else(|| {
AppError::BadRequest("Home Assistant action needs a domain".into()) AppError::BadRequest("Home Assistant action needs a domain".into())
@@ -1708,7 +1784,7 @@ fn dry_run_block_reason(
} }
Ok(None) Ok(None)
} }
"device_action" => { "device_action" | "device_feature_action" => {
let Some(device_id) = flow_string(&action.config, "device_id") else { let Some(device_id) = flow_string(&action.config, "device_id") else {
return Ok(Some("missing_device".into())); return Ok(Some("missing_device".into()));
}; };
@@ -1737,11 +1813,9 @@ fn dry_run_block_reason(
}) { }) {
return Ok(Some("temporary_quick_thermostat".into())); return Ok(Some("temporary_quick_thermostat".into()));
} }
if zones.iter().any(|z| z.device_id == device_id && !z.enabled) let command = if action.kind == "device_feature_action" {
&& flow_bool(&action.config, "power") != Some(true) flow_device_feature_command(&action.config)?
{ } else {
return Ok(Some("zone_disabled".into()));
}
let mut command = DeviceCommand::default(); let mut command = DeviceCommand::default();
command.power = flow_bool(&action.config, "power"); command.power = flow_bool(&action.config, "power");
command.mode = flow_string(&action.config, "mode"); command.mode = flow_string(&action.config, "mode");
@@ -1756,6 +1830,13 @@ fn dry_run_block_reason(
command.xfan = flow_bool(&action.config, "xfan"); command.xfan = flow_bool(&action.config, "xfan");
command.health = flow_bool(&action.config, "health"); command.health = flow_bool(&action.config, "health");
command.sleep = flow_bool(&action.config, "sleep"); command.sleep = flow_bool(&action.config, "sleep");
command
};
if zones.iter().any(|z| z.device_id == device_id && !z.enabled)
&& command.power != Some(true)
{
return Ok(Some("zone_disabled".into()));
}
if zones.iter().any(|z| z.device_id == device_id) if zones.iter().any(|z| z.device_id == device_id)
&& engine::automation_action_conflicts_with_thermostat(&command) && engine::automation_action_conflicts_with_thermostat(&command)
{ {
@@ -1950,4 +2031,20 @@ mod flow_draft_tests {
assert!(flow.compiled_schedule_ids.is_empty()); assert!(flow.compiled_schedule_ids.is_empty());
assert!(flow.compiled_automation_ids.is_empty()); assert!(flow.compiled_automation_ids.is_empty());
} }
#[test]
fn device_feature_action_builds_only_selected_field() {
let command = flow_device_feature_command(&json!({"feature": "light", "value": true}))
.expect("valid feature action");
assert_eq!(command.light, Some(true));
assert!(command.power.is_none());
assert!(command.mode.is_none());
assert!(command.swing_vertical.is_none());
}
#[test]
fn device_feature_action_validates_louver_range() {
let result = flow_device_feature_command(&json!({"feature": "swing_vertical", "value": 99}));
assert!(result.is_err());
}
} }
+24 -1
View File
@@ -982,7 +982,8 @@
<div class="flow-palette-group block-category-action"><span data-i18n="flow.actions">Akcje</span> <div class="flow-palette-group block-category-action"><span data-i18n="flow.actions">Akcje</span>
<button type="button" data-flow-add="zone_thermostat" <button type="button" data-flow-add="zone_thermostat"
data-i18n="flow.node.thermostat">Termostat</button><button type="button" data-flow-add="device_action" data-i18n="flow.node.thermostat">Termostat</button><button type="button" data-flow-add="device_action"
data-i18n="flow.node.greeDevice">Urządzenie GREE</button><button type="button" data-flow-add="group_action" data-i18n="flow.node.greeDevice">Urządzenie GREE</button><button type="button" data-flow-add="device_feature_action"
data-i18n="flow.node.greeFeature">Pojedyncza funkcja GREE</button><button type="button" data-flow-add="group_action"
data-i18n="flow.node.group">Grupa</button> data-i18n="flow.node.group">Grupa</button>
</div> </div>
<div class="flow-palette-group block-category-haaction"><span data-i18n="flow.haActions">Home Assistant</span> <div class="flow-palette-group block-category-haaction"><span data-i18n="flow.haActions">Home Assistant</span>
@@ -1786,6 +1787,28 @@
<option value="6" data-i18n="louver.horizontal.fixedRight">Fixed: Right</option> <option value="6" data-i18n="louver.horizontal.fixedRight">Fixed: Right</option>
</select></label> </select></label>
</div> </div>
<details id="automationDeviceFeatureOptions" class="flow-device-options">
<summary data-i18n="automations.singleDeviceFunctions">Single device functions</summary>
<div class="two">
<label><span data-i18n="common.fan">Fan</span><select name="action_fan_speed">
<option value="" data-i18n="actions.noChange">No change</option>
<option value="0" data-i18n="fan.auto">Auto</option>
<option value="1" data-i18n="fan.low">Low</option>
<option value="2" data-i18n="fan.mediumLow">Medium-low</option>
<option value="3" data-i18n="fan.medium">Medium</option>
<option value="4" data-i18n="fan.mediumHigh">Medium-high</option>
<option value="5" data-i18n="fan.high">High</option>
</select></label>
<label><span data-i18n="devices.quiet">Quiet</span><select name="action_quiet"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
<label><span data-i18n="devices.turbo">Turbo</span><select name="action_turbo"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
<label><span data-i18n="devices.light">Panel light</span><select name="action_light"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
<label><span data-i18n="devices.air">Air</span><select name="action_air"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
<label><span data-i18n="devices.xfan">X-Fan</span><select name="action_xfan"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
<label><span data-i18n="devices.health">Health</span><select name="action_health"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
<label><span data-i18n="devices.sleep">Sleep</span><select name="action_sleep"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="common.on">On</option><option value="false" data-i18n="common.off">Off</option></select></label>
</div>
<p class="field-note" data-i18n="automations.singleDeviceFunctionsHint">Leave every other field as No change to modify only the selected unit function.</p>
</details>
<p class="field-note" id="automationGroupHint" hidden data-i18n="automations.groupHint">For a group, Auto mode <p class="field-note" id="automationGroupHint" hidden data-i18n="automations.groupHint">For a group, Auto mode
means follow the whole-house heating/cooling mode. Group presets are applied to all member zones.</p> means follow the whole-house heating/cooling mode. Group presets are applied to all member zones.</p>
<label class="check"><input type="checkbox" name="enabled" checked> <span <label class="check"><input type="checkbox" name="enabled" checked> <span
+3
View File
@@ -115,6 +115,9 @@ const houseModeLabel = mode => mode === 'off' ? tr('house.noControl') : modeLabe
const fanLabel = value => ({ 0: 'fan.auto', 1: 'fan.low', 2: 'fan.mediumLow', 3: 'fan.medium', 4: 'fan.mediumHigh', 5: 'fan.high' }[value] ? tr({ 0: 'fan.auto', 1: 'fan.low', 2: 'fan.mediumLow', 3: 'fan.medium', 4: 'fan.mediumHigh', 5: 'fan.high' }[value]) : value); const fanLabel = value => ({ 0: 'fan.auto', 1: 'fan.low', 2: 'fan.mediumLow', 3: 'fan.medium', 4: 'fan.mediumHigh', 5: 'fan.high' }[value] ? tr({ 0: 'fan.auto', 1: 'fan.low', 2: 'fan.mediumLow', 3: 'fan.medium', 4: 'fan.mediumHigh', 5: 'fan.high' }[value]) : value);
const deviceCommandFieldLabel = key => { const deviceCommandFieldLabel = key => {
const translationKey = { const translationKey = {
power: 'common.power',
mode: 'common.mode',
target_temperature: 'common.targetC',
fan_speed: 'common.fan', fan_speed: 'common.fan',
swing_vertical: 'devices.swingVertical', swing_vertical: 'devices.swingVertical',
swing_horizontal: 'devices.swingHorizontal', swing_horizontal: 'devices.swingHorizontal',
+15 -1
View File
@@ -712,6 +712,18 @@ function renderSchedules() {
}).join('') : `<div class="empty"><strong>${esc(tr('schedules.emptyTitle'))}</strong>${esc(tr('schedules.emptyText'))}</div>`; }).join('') : `<div class="empty"><strong>${esc(tr('schedules.emptyTitle'))}</strong>${esc(tr('schedules.emptyText'))}</div>`;
} }
function automationDeviceFunctionSummary(action = {}) {
const parts = [];
if (action.target_temperature != null) parts.push(fmtTemp(action.target_temperature));
if (action.fan_speed != null) parts.push(`${deviceCommandFieldLabel('fan_speed')}: ${fanLabel(action.fan_speed)}`);
if (action.swing_vertical != null) parts.push(`${deviceCommandFieldLabel('swing_vertical')}: ${louverPositionLabel('vertical', action.swing_vertical)}`);
if (action.swing_horizontal != null) parts.push(`${deviceCommandFieldLabel('swing_horizontal')}: ${louverPositionLabel('horizontal', action.swing_horizontal)}`);
['quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep'].forEach(key => {
if (action[key] != null) parts.push(`${deviceCommandFieldLabel(key)}: ${action[key] ? tr('common.on') : tr('common.off')}`);
});
return parts.join(' · ') || '—';
}
function renderAutomations() { function renderAutomations() {
$('#automationList').innerHTML = app.automations.length ? app.automations.map(item => { $('#automationList').innerHTML = app.automations.length ? app.automations.map(item => {
const actionZone = item.action_zone_id ? app.zones.find(zone => zone.id === item.action_zone_id) : null; const actionZone = item.action_zone_id ? app.zones.find(zone => zone.id === item.action_zone_id) : null;
@@ -720,8 +732,10 @@ function renderAutomations() {
const targetName = actionZone?.name || actionGroup?.name || actionDevice?.name || tr('common.noDevice'); const targetName = actionZone?.name || actionGroup?.name || actionDevice?.name || tr('common.noDevice');
const mode = item.action.mode === 'auto' && (actionGroup || actionZone) ? tr('groups.followHouse') : (item.action.mode ? modeLabel(item.action.mode) : '—'); const mode = item.action.mode === 'auto' && (actionGroup || actionZone) ? tr('groups.followHouse') : (item.action.mode ? modeLabel(item.action.mode) : '—');
const preset = item.action_zone_preset ? zonePresetLabel(item.action_zone_preset) : (item.action_preset ? zonePresetLabel(item.action_preset) : '—'); const preset = item.action_zone_preset ? zonePresetLabel(item.action_zone_preset) : (item.action_preset ? zonePresetLabel(item.action_preset) : '—');
const deviceFunction = actionZone || actionGroup ? preset : automationDeviceFunctionSummary(item.action);
const deviceFunctionLabel = actionZone || actionGroup ? tr('groups.profile') : tr('automations.deviceFunction');
return `<article class="list-card ${item.flow_id ? 'flow-generated-card' : ''}"><div class="list-card-head"><div><h3>${esc(item.name)}</h3><p>${esc(tr('automations.triggerSummary', { trigger: automationTriggerLabel(item), device: targetName }))}</p></div><span class="badge ${item.enabled ? 'active' : ''}">${esc(item.enabled ? tr('common.active') : tr('common.disabled'))}</span></div> return `<article class="list-card ${item.flow_id ? 'flow-generated-card' : ''}"><div class="list-card-head"><div><h3>${esc(item.name)}</h3><p>${esc(tr('automations.triggerSummary', { trigger: automationTriggerLabel(item), device: targetName }))}</p></div><span class="badge ${item.enabled ? 'active' : ''}">${esc(item.enabled ? tr('common.active') : tr('common.disabled'))}</span></div>
<div class="card-stats"><div class="card-stat"><small>${esc(tr('common.power'))}</small><strong>${item.action.power == null ? '' : item.action.power ? tr('common.on') : tr('common.off')}</strong></div><div class="card-stat"><small>${esc(tr('common.mode'))}</small><strong>${esc(mode)}</strong></div><div class="card-stat"><small>${esc(tr('groups.profile'))}</small><strong>${esc(preset)}</strong></div><div class="card-stat"><small>${esc(tr('automations.last'))}</small><strong>${item.last_fired_at ? new Date(item.last_fired_at).toLocaleTimeString(locale(), { hour: '2-digit', minute: '2-digit' }) : ''}</strong></div></div> <div class="card-stats"><div class="card-stat"><small>${esc(tr('common.power'))}</small><strong>${item.action.power == null ? '' : item.action.power ? tr('common.on') : tr('common.off')}</strong></div><div class="card-stat"><small>${esc(tr('common.mode'))}</small><strong>${esc(mode)}</strong></div><div class="card-stat"><small>${esc(deviceFunctionLabel)}</small><strong>${esc(deviceFunction)}</strong></div><div class="card-stat"><small>${esc(tr('automations.last'))}</small><strong>${item.last_fired_at ? new Date(item.last_fired_at).toLocaleTimeString(locale(), { hour: '2-digit', minute: '2-digit' }) : ''}</strong></div></div>
<div class="card-footer"><small>${esc(item.flow_id ? tr('flow.generatedReadOnly') : (actionGroup ? `${tr('groups.group')}: ${targetName}` : `${tr('common.device')}: ${targetName}`))} · ${esc(tr('common.cooldown'))} ${item.cooldown_seconds}s</small><div class="card-menu">${item.flow_id ? `<button data-action="edit-flow" data-id="${esc(item.flow_id)}">${esc(tr('flow.openEditor'))}</button>` : `<button data-action="edit-automation" data-id="${esc(item.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-automation" data-id="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div></details>`}</div></div></article>`; <div class="card-footer"><small>${esc(item.flow_id ? tr('flow.generatedReadOnly') : (actionGroup ? `${tr('groups.group')}: ${targetName}` : `${tr('common.device')}: ${targetName}`))} · ${esc(tr('common.cooldown'))} ${item.cooldown_seconds}s</small><div class="card-menu">${item.flow_id ? `<button data-action="edit-flow" data-id="${esc(item.flow_id)}">${esc(tr('flow.openEditor'))}</button>` : `<button data-action="edit-automation" data-id="${esc(item.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-automation" data-id="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div></details>`}</div></div></article>`;
}).join('') : `<div class="empty"><strong>${esc(tr('automations.emptyTitle'))}</strong>${esc(tr('automations.emptyText'))}</div>`; }).join('') : `<div class="empty"><strong>${esc(tr('automations.emptyTitle'))}</strong>${esc(tr('automations.emptyText'))}</div>`;
} }
+1 -1
View File
@@ -643,7 +643,7 @@ $('#scheduleForm').addEventListener('submit', async event => {
$('#automationForm').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; event.preventDefault(); const form = event.currentTarget, raw = Object.fromEntries(new FormData(form)); const id = raw.id;
const groupTarget = raw.action_target_kind === 'group'; 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_fan_speed !== '') action.fan_speed = Number(raw.action_fan_speed); 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); if (!groupTarget) ['quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep'].forEach(key => { const value = raw[`action_${key}`]; if (value !== '') action[key] = value === '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) }; 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 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')); 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'));
+45 -1
View File
@@ -29,6 +29,7 @@ const FLOW_NODE_META = Object.freeze({
logic_not: { titleKey: 'flow.node.not', category: 'logic' }, logic_not: { titleKey: 'flow.node.not', category: 'logic' },
zone_thermostat: { titleKey: 'flow.node.thermostat', category: 'action' }, zone_thermostat: { titleKey: 'flow.node.thermostat', category: 'action' },
device_action: { titleKey: 'flow.node.greeDevice', category: 'action' }, device_action: { titleKey: 'flow.node.greeDevice', category: 'action' },
device_feature_action: { titleKey: 'flow.node.greeFeature', category: 'action' },
group_action: { titleKey: 'flow.node.group', category: 'action' }, group_action: { titleKey: 'flow.node.group', category: 'action' },
ha_service_action: { titleKey: 'flow.node.haServiceAction', category: 'haaction' }, ha_service_action: { titleKey: 'flow.node.haServiceAction', category: 'haaction' },
}); });
@@ -97,6 +98,7 @@ function flowDefaultConfig(kind) {
if (kind === 'shared_input') return sharedFlowReferenceDefaultConfig(app.flowSharedInputs?.[0]); if (kind === 'shared_input') return sharedFlowReferenceDefaultConfig(app.flowSharedInputs?.[0]);
if (kind === 'zone_thermostat') return { zone_id: app.zones[0]?.id || '', preset: 'comfort', setpoint: 21, mode: 'auto', power: null, swing_vertical: null, swing_horizontal: null, cooldown_seconds: 60 }; if (kind === 'zone_thermostat') return { zone_id: app.zones[0]?.id || '', preset: 'comfort', setpoint: 21, mode: 'auto', power: null, swing_vertical: null, swing_horizontal: null, cooldown_seconds: 60 };
if (kind === 'device_action') return { device_id: app.devices[0]?.id || '', power: true, mode: '', target_temperature: null, fan_speed: null, swing_vertical: null, swing_horizontal: null, quiet: null, turbo: null, light: null, air: null, xfan: null, health: null, sleep: null, cooldown_seconds: 60 }; if (kind === 'device_action') return { device_id: app.devices[0]?.id || '', power: true, mode: '', target_temperature: null, fan_speed: null, swing_vertical: null, swing_horizontal: null, quiet: null, turbo: null, light: null, air: null, xfan: null, health: null, sleep: null, cooldown_seconds: 60 };
if (kind === 'device_feature_action') return { device_id: app.devices[0]?.id || '', feature: 'light', value: true, cooldown_seconds: 60 };
if (kind === 'group_action') return { group_id: app.groups[0]?.id || '', power: true, mode: '', preset: '', setpoint: 21, cooldown_seconds: 60 }; if (kind === 'group_action') return { group_id: app.groups[0]?.id || '', power: true, mode: '', preset: '', setpoint: 21, cooldown_seconds: 60 };
if (kind === 'ha_service_action') return { domain: 'switch', service: 'turn_on', entity_id: '', data: {}, cooldown_seconds: 60 }; if (kind === 'ha_service_action') return { domain: 'switch', service: 'turn_on', entity_id: '', data: {}, cooldown_seconds: 60 };
return {}; return {};
@@ -357,6 +359,7 @@ function flowNodeSummary(node) {
if (node.kind === 'logic_not') return tr('flow.invertCondition'); if (node.kind === 'logic_not') return tr('flow.invertCondition');
if (node.kind === 'zone_thermostat') { const zone = app.zones.find(z => z.id === c.zone_id); return `${zone?.name || tr('common.noZone')} · ${c.preset === 'custom' ? fmtTemp(c.setpoint) : zonePresetLabel(c.preset || 'comfort')}`; } if (node.kind === 'zone_thermostat') { const zone = app.zones.find(z => z.id === c.zone_id); return `${zone?.name || tr('common.noZone')} · ${c.preset === 'custom' ? fmtTemp(c.setpoint) : zonePresetLabel(c.preset || 'comfort')}`; }
if (node.kind === 'device_action') { const d = app.devices.find(v => v.id === c.device_id); return `${d?.name || tr('common.noDevice')} · ${c.power == null ? tr('actions.noChange') : c.power ? tr('common.on') : tr('common.off')}`; } if (node.kind === 'device_action') { const d = app.devices.find(v => v.id === c.device_id); return `${d?.name || tr('common.noDevice')} · ${c.power == null ? tr('actions.noChange') : c.power ? tr('common.on') : tr('common.off')}`; }
if (node.kind === 'device_feature_action') { const d = app.devices.find(v => v.id === c.device_id); return `${d?.name || tr('common.noDevice')} · ${deviceCommandFieldLabel(c.feature || 'light')}${flowDeviceFeatureValueLabel(c.feature, c.value)}`; }
if (node.kind === 'group_action') { const g = app.groups.find(v => v.id === c.group_id); return `${g?.name || tr('groups.group')} · ${c.power == null ? tr('actions.noChange') : c.power ? tr('common.on') : tr('common.off')}`; } if (node.kind === 'group_action') { const g = app.groups.find(v => v.id === c.group_id); return `${g?.name || tr('groups.group')} · ${c.power == null ? tr('actions.noChange') : c.power ? tr('common.on') : tr('common.off')}`; }
if (node.kind === 'ha_service_action') return `${c.domain || 'domain'}.${c.service || 'service'} · ${c.entity_id || 'entity_id'}`; if (node.kind === 'ha_service_action') return `${c.domain || 'domain'}.${c.service || 'service'} · ${c.entity_id || 'entity_id'}`;
return ''; return '';
@@ -550,6 +553,7 @@ function renderFlowInspector() {
else if (node.kind === 'logic_not') fields = `<p class="field-note">${esc(tr('flow.notHint'))}</p>`; else if (node.kind === 'logic_not') fields = `<p class="field-note">${esc(tr('flow.notHint'))}</p>`;
else if (node.kind === 'zone_thermostat') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label><div class="two"><label><span>${esc(tr('schedules.profile'))}</span><select data-flow-config="preset">${['auto', 'comfort', 'sleep', 'away', 'custom'].map(v => `<option value="${v}" ${c.preset === v ? 'selected' : ''}>${esc(zonePresetLabel(v))}</option>`).join('')}</select></label><label><span>${esc(tr('common.targetC'))}</span><input type="number" min="8" max="30" step="0.1" data-flow-config="setpoint" value="${Number(c.setpoint ?? 21)}"></label></div><div class="two"><label><span>${esc(tr('common.mode'))}</span><select data-flow-config="mode"><option value="auto" ${c.mode === 'auto' ? 'selected' : ''}>Auto</option><option value="heat" ${c.mode === 'heat' ? 'selected' : ''}>${esc(tr('mode.heat'))}</option><option value="cool" ${c.mode === 'cool' ? 'selected' : ''}>${esc(tr('mode.cool'))}</option></select></label><label><span>${esc(tr('common.power'))}</span><select data-flow-config="power"><option value="" ${c.power == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c.power === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c.power === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label></div><details class="flow-device-options"><summary>${esc(tr('flow.thermostatDeviceOptions'))}</summary><div class="two">${flowOptionalLouverField(c, 'swing_vertical', 'vertical')}${flowOptionalLouverField(c, 'swing_horizontal', 'horizontal')}</div><p class="field-note">${esc(tr('flow.thermostatDeviceOptionsHint'))}</p></details><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`; else if (node.kind === 'zone_thermostat') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label><div class="two"><label><span>${esc(tr('schedules.profile'))}</span><select data-flow-config="preset">${['auto', 'comfort', 'sleep', 'away', 'custom'].map(v => `<option value="${v}" ${c.preset === v ? 'selected' : ''}>${esc(zonePresetLabel(v))}</option>`).join('')}</select></label><label><span>${esc(tr('common.targetC'))}</span><input type="number" min="8" max="30" step="0.1" data-flow-config="setpoint" value="${Number(c.setpoint ?? 21)}"></label></div><div class="two"><label><span>${esc(tr('common.mode'))}</span><select data-flow-config="mode"><option value="auto" ${c.mode === 'auto' ? 'selected' : ''}>Auto</option><option value="heat" ${c.mode === 'heat' ? 'selected' : ''}>${esc(tr('mode.heat'))}</option><option value="cool" ${c.mode === 'cool' ? 'selected' : ''}>${esc(tr('mode.cool'))}</option></select></label><label><span>${esc(tr('common.power'))}</span><select data-flow-config="power"><option value="" ${c.power == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c.power === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c.power === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label></div><details class="flow-device-options"><summary>${esc(tr('flow.thermostatDeviceOptions'))}</summary><div class="two">${flowOptionalLouverField(c, 'swing_vertical', 'vertical')}${flowOptionalLouverField(c, 'swing_horizontal', 'horizontal')}</div><p class="field-note">${esc(tr('flow.thermostatDeviceOptionsHint'))}</p></details><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
else if (node.kind === 'device_action') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>${flowActionFields(c, false)}`; else if (node.kind === 'device_action') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>${flowActionFields(c, false)}`;
else if (node.kind === 'device_feature_action') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label><label><span>${esc(tr('flow.deviceFeature'))}</span><select data-flow-config="feature">${flowDeviceFeatureOptions(c.feature)}</select></label>${flowDeviceFeatureValueField(c)}<p class="field-note">${esc(tr('flow.deviceFeatureHint'))}</p><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
else if (node.kind === 'group_action') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-flow-config="group_id">${flowSelectOptions(app.groups, c.group_id)}</select></label>${flowActionFields(c, true)}`; else if (node.kind === 'group_action') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-flow-config="group_id">${flowSelectOptions(app.groups, c.group_id)}</select></label>${flowActionFields(c, true)}`;
else if (node.kind === 'ha_service_action') fields = `<div class="two"><label><span>domain</span><input data-flow-config="domain" value="${esc(c.domain || 'switch')}" placeholder="switch"></label><label><span>service</span><input data-flow-config="service" value="${esc(c.service || 'turn_on')}" placeholder="turn_on"></label></div><p class="field-note">${esc(tr('flow.serviceExample'))}</p><label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="switch.gas"></label><label><span>${esc(tr('flow.serviceData'))}</span><textarea rows="5" data-flow-config="data_json">${esc(JSON.stringify(c.data || {}, null, 2))}</textarea></label><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`; else if (node.kind === 'ha_service_action') fields = `<div class="two"><label><span>domain</span><input data-flow-config="domain" value="${esc(c.domain || 'switch')}" placeholder="switch"></label><label><span>service</span><input data-flow-config="service" value="${esc(c.service || 'turn_on')}" placeholder="turn_on"></label></div><p class="field-note">${esc(tr('flow.serviceExample'))}</p><label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="switch.gas"></label><label><span>${esc(tr('flow.serviceData'))}</span><textarea rows="5" data-flow-config="data_json">${esc(JSON.stringify(c.data || {}, null, 2))}</textarea></label><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
host.innerHTML = `<div class="flow-inspector-head"><div><span class="eyebrow">${esc(tr('flow.blockSettings'))}</span><h3>${esc(flowNodeTitle(meta))}</h3></div><div class="flow-inspector-head-actions"><button type="button" class="secondary flow-inspector-expand" data-action="flow-toggle-inspector" aria-label="${esc(tr('flow.expandSettings'))}">${uiIcon('expand-vertical')}</button><button type="button" class="secondary flow-inspector-close" data-action="flow-clear-selection" aria-label="${esc(tr('flow.clearSelection'))}">${uiIcon('close')}</button><button type="button" class="danger flow-inspector-delete" data-flow-remove="${esc(node.id)}">${esc(tr('actions.delete'))}</button></div></div>${fields}<div class="flow-inspector-meta"><small>ID</small><code>${esc(node.id)}</code></div>`; host.innerHTML = `<div class="flow-inspector-head"><div><span class="eyebrow">${esc(tr('flow.blockSettings'))}</span><h3>${esc(flowNodeTitle(meta))}</h3></div><div class="flow-inspector-head-actions"><button type="button" class="secondary flow-inspector-expand" data-action="flow-toggle-inspector" aria-label="${esc(tr('flow.expandSettings'))}">${uiIcon('expand-vertical')}</button><button type="button" class="secondary flow-inspector-close" data-action="flow-clear-selection" aria-label="${esc(tr('flow.clearSelection'))}">${uiIcon('close')}</button><button type="button" class="danger flow-inspector-delete" data-flow-remove="${esc(node.id)}">${esc(tr('actions.delete'))}</button></div></div>${fields}<div class="flow-inspector-meta"><small>ID</small><code>${esc(node.id)}</code></div>`;
@@ -562,6 +566,40 @@ function flowLouverComparisonFields(c, axis) {
return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${options}</select></label><label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value">${louverOptions(axis, value)}</select></label></div>`; return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${options}</select></label><label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value">${louverOptions(axis, value)}</select></label></div>`;
} }
function flowComparisonFields(c, temperature = true) { return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${flowOperatorOptions(c.operator || 'lt')}</select></label><label><span>${esc(temperature ? tr('automations.thresholdC') : tr('flow.value'))}</span><input type="number" step="0.1" data-flow-config="value" value="${Number(c.value ?? 0)}"></label></div>`; } function flowComparisonFields(c, temperature = true) { return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${flowOperatorOptions(c.operator || 'lt')}</select></label><label><span>${esc(temperature ? tr('automations.thresholdC') : tr('flow.value'))}</span><input type="number" step="0.1" data-flow-config="value" value="${Number(c.value ?? 0)}"></label></div>`; }
const FLOW_DEVICE_FEATURES = Object.freeze(['power', 'mode', 'target_temperature', 'fan_speed', 'swing_vertical', 'swing_horizontal', 'quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep']);
const FLOW_DEVICE_BOOL_FEATURES = Object.freeze(['power', 'quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep']);
function flowDeviceFeatureDefaultValue(feature) {
if (FLOW_DEVICE_BOOL_FEATURES.includes(feature)) return true;
if (feature === 'mode') return 'auto';
if (feature === 'target_temperature') return 22;
if (feature === 'fan_speed') return 0;
if (['swing_vertical', 'swing_horizontal'].includes(feature)) return 1;
return true;
}
function flowDeviceFeatureOptions(selected) {
return FLOW_DEVICE_FEATURES.map(feature => `<option value="${feature}" ${feature === selected ? 'selected' : ''}>${esc(deviceCommandFieldLabel(feature))}</option>`).join('');
}
function flowDeviceFeatureValueLabel(feature, value) {
if (FLOW_DEVICE_BOOL_FEATURES.includes(feature)) return value === false ? tr('common.off') : tr('common.on');
if (feature === 'mode') return modeLabel(value || 'auto');
if (feature === 'target_temperature') return fmtTemp(value);
if (feature === 'fan_speed') return fanLabel(Number(value));
if (feature === 'swing_vertical') return louverPositionLabel('vertical', value);
if (feature === 'swing_horizontal') return louverPositionLabel('horizontal', value);
return value ?? '—';
}
function flowDeviceFeatureValueField(c) {
const feature = c.feature || 'light';
const value = c.value ?? flowDeviceFeatureDefaultValue(feature);
if (FLOW_DEVICE_BOOL_FEATURES.includes(feature)) return `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value"><option value="true" ${value === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${value === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label>`;
if (feature === 'mode') return `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value">${['auto', 'cool', 'dry', 'fan', 'heat'].map(mode => `<option value="${mode}" ${value === mode ? 'selected' : ''}>${esc(modeLabel(mode))}</option>`).join('')}</select></label>`;
if (feature === 'target_temperature') return `<label><span>${esc(tr('flow.value'))}</span><input type="number" min="8" max="30" step="0.5" data-flow-config="value" value="${Number(value)}"></label>`;
if (feature === 'fan_speed') return `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value">${[0, 1, 2, 3, 4, 5].map(v => `<option value="${v}" ${Number(value) === v ? 'selected' : ''}>${esc(fanLabel(v))}</option>`).join('')}</select></label>`;
if (feature === 'swing_vertical') return `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value">${louverOptions('vertical', value)}</select></label>`;
if (feature === 'swing_horizontal') return `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value">${louverOptions('horizontal', value)}</select></label>`;
return '';
}
function flowOptionalBoolField(c, key) { function flowOptionalBoolField(c, key) {
return `<label><span>${esc(deviceCommandFieldLabel(key))}</span><select data-flow-config="${esc(key)}"><option value="" ${c[key] == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c[key] === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c[key] === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label>`; return `<label><span>${esc(deviceCommandFieldLabel(key))}</span><select data-flow-config="${esc(key)}"><option value="" ${c[key] == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c[key] === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c[key] === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label>`;
} }
@@ -1169,6 +1207,12 @@ function updateFlowConfig(input) {
const numeric = Boolean(sharedFlowInputLouverAxis(item)) || ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(item?.kind); const numeric = Boolean(sharedFlowInputLouverAxis(item)) || ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(item?.kind);
node.config.value = numeric ? (input.value === '' ? null : Number(input.value)) : input.value; node.config.value = numeric ? (input.value === '' ? null : Number(input.value)) : input.value;
} }
else if (node.kind === 'device_feature_action' && key === 'feature') { node.config.feature = input.value; node.config.value = flowDeviceFeatureDefaultValue(input.value); }
else if (node.kind === 'device_feature_action' && key === 'value') {
if (FLOW_DEVICE_BOOL_FEATURES.includes(node.config.feature)) node.config.value = input.value === 'true';
else if (['target_temperature', 'fan_speed', 'swing_vertical', 'swing_horizontal'].includes(node.config.feature)) node.config.value = Number(input.value);
else node.config.value = input.value;
}
else if (node.kind === 'device_state' && key === 'field') { else if (node.kind === 'device_state' && key === 'field') {
const wasLouver = ['swing_vertical', 'swing_horizontal'].includes(node.config.field); const wasLouver = ['swing_vertical', 'swing_horizontal'].includes(node.config.field);
node.config.field = input.value; node.config.field = input.value;
@@ -1189,7 +1233,7 @@ function updateFlowConfig(input) {
else if (key === 'value' && node.kind === 'rolling_stat') node.config.value = Number(input.value || 0); else if (key === 'value' && node.kind === 'rolling_stat') node.config.value = Number(input.value || 0);
else if (key === 'min_span' && node.kind === 'oscillates') node.config.min_span = Math.max(0.001, Number(input.value || 0.001)); else if (key === 'min_span' && node.kind === 'oscillates') node.config.min_span = Math.max(0.001, Number(input.value || 0.001));
else if (key === 'min_direction_changes' && node.kind === 'oscillates') node.config.min_direction_changes = Math.max(1, Math.floor(Number(input.value || 1))); else if (key === 'min_direction_changes' && node.kind === 'oscillates') node.config.min_direction_changes = Math.max(1, Math.floor(Number(input.value || 1)));
else if (['value', 'setpoint', 'target_temperature', 'cooldown_seconds', 'fan_speed'].includes(key) && ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric', 'zone_thermostat', 'device_action', 'group_action', 'ha_service_action'].includes(node.kind)) node.config[key] = input.value === '' ? null : Number(input.value); else if (['value', 'setpoint', 'target_temperature', 'cooldown_seconds', 'fan_speed'].includes(key) && ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric', 'zone_thermostat', 'device_action', 'device_feature_action', 'group_action', 'ha_service_action'].includes(node.kind)) node.config[key] = input.value === '' ? null : Number(input.value);
else node.config[key] = input.value; else node.config[key] = input.value;
app.flowDirty = true; renderFlowEditor(); app.flowDirty = true; renderFlowEditor();
} }
+5
View File
@@ -547,16 +547,19 @@ function updateAutomationTargetFields() {
$('#automationGroupPreset').hidden = !groupTarget; $('#automationGroupPreset').hidden = !groupTarget;
$('#automationTargetTemperature').hidden = groupTarget; $('#automationTargetTemperature').hidden = groupTarget;
$('#automationDeviceSwingOptions').hidden = groupTarget; $('#automationDeviceSwingOptions').hidden = groupTarget;
$('#automationDeviceFeatureOptions').hidden = groupTarget;
$('#automationGroupHint').hidden = !groupTarget; $('#automationGroupHint').hidden = !groupTarget;
form.action_device_id.required = !groupTarget; form.action_device_id.required = !groupTarget;
form.action_group_id.required = groupTarget; form.action_group_id.required = groupTarget;
form.action_target_temperature.disabled = groupTarget; form.action_target_temperature.disabled = groupTarget;
form.action_swing_vertical.disabled = groupTarget; form.action_swing_vertical.disabled = groupTarget;
form.action_swing_horizontal.disabled = groupTarget; form.action_swing_horizontal.disabled = groupTarget;
['action_fan_speed', 'action_quiet', 'action_turbo', 'action_light', 'action_air', 'action_xfan', 'action_health', 'action_sleep'].forEach(name => { if (form.elements[name]) form.elements[name].disabled = groupTarget; });
if (groupTarget) { if (groupTarget) {
form.action_target_temperature.value = ''; form.action_target_temperature.value = '';
form.action_swing_vertical.value = ''; form.action_swing_vertical.value = '';
form.action_swing_horizontal.value = ''; form.action_swing_horizontal.value = '';
['action_fan_speed', 'action_quiet', 'action_turbo', 'action_light', 'action_air', 'action_xfan', 'action_health', 'action_sleep'].forEach(name => { if (form.elements[name]) form.elements[name].value = ''; });
} }
[...form.action_mode.options].forEach(option => { [...form.action_mode.options].forEach(option => {
if (!['dry', 'fan'].includes(option.value)) return; if (!['dry', 'fan'].includes(option.value)) return;
@@ -703,6 +706,8 @@ function populateAutomation(id) {
form.action_target_temperature.value = item.action.target_temperature ?? ''; form.action_target_temperature.value = item.action.target_temperature ?? '';
form.action_swing_vertical.value = item.action.swing_vertical == null ? '' : String(item.action.swing_vertical); form.action_swing_vertical.value = item.action.swing_vertical == null ? '' : String(item.action.swing_vertical);
form.action_swing_horizontal.value = item.action.swing_horizontal == null ? '' : String(item.action.swing_horizontal); form.action_swing_horizontal.value = item.action.swing_horizontal == null ? '' : String(item.action.swing_horizontal);
form.action_fan_speed.value = item.action.fan_speed == null ? '' : String(item.action.fan_speed);
['quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep'].forEach(key => { const field = form.elements[`action_${key}`]; if (field) field.value = item.action[key] == null ? '' : String(item.action[key]); });
form.enabled.checked = item.enabled; form.enabled.checked = item.enabled;
updateAutomationTargetFields(); updateAutomationTargetFields();
openDialog('automationDialog'); openDialog('automationDialog');