v0.15.2
This commit is contained in:
Generated
+1
-1
@@ -700,7 +700,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.15.1"
|
||||
version = "0.15.2"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.15.1"
|
||||
version = "0.15.2"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+19
-19
@@ -1,14 +1,14 @@
|
||||
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
852768ed3da99ac2e9bbe6c9b67e9a4a667e7115ad9482b6ce4a29ff72d8f914 ./Cargo.lock
|
||||
8e58dd01d95c93ea4f5d0cc5e379cb192221705c9dda41b7b2b7182c49dbff0f ./Cargo.toml
|
||||
82bb41f94fdf5c66ebd9e6caa92e0f1794431067298e0a27c95c8e5bfd68de89 ./Cargo.lock
|
||||
39bdb16196253559e3487aa912ff770c09c7b08cdfbb21e9db1d8af1304ea7e0 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
4a6570c14ba56fc9a4921de6726816264e961a5b82a7fe3fcf475ef96417dad9 ./README.md
|
||||
283ffafdffd463f5d670acaba251fb92fab7c88e51a6fc1e06732f6cade88b5e ./SOURCE_MANIFEST.sha256
|
||||
84a0803bc75dc3199e23ef56c11c6950e8bd888c686ecda7cc369829e8e3073f ./README.md
|
||||
f21732756af313565bfe0788230e45cc11c384cee1187f006d426c5463183c0c ./SOURCE_MANIFEST.sha256
|
||||
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
|
||||
9873e774746783d65e447b439d069f84c13d2f4e556661e4eb509af59293c2f7 ./docs/API.md
|
||||
58b79297607303151c1de2da74344cd4f6277bc53d80ebe04ed4567d906c963b ./docs/FLOW.md
|
||||
9483ad7cb1e0c8e38f08c722600b6ba059d4273cfb9dcd1af57c11de522d187b ./docs/openapi.json
|
||||
604a795dec077dd7e4f3462a564fdda68e8084aef07da0fcf3dfb6053a46cc58 ./docs/API.md
|
||||
a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md
|
||||
8babaa6830fa70b2d3d8208ef51808e79c9d68c4e007b9ba1537fe582ba03b3d ./docs/openapi.json
|
||||
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
|
||||
0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile
|
||||
adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md
|
||||
@@ -22,7 +22,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
|
||||
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
|
||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
|
||||
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
|
||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
|
||||
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
|
||||
@@ -30,10 +30,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
|
||||
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
|
||||
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
|
||||
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
|
||||
ea5c4eb4ca2656f8d9a2be37a35ba04bb4c53d735c9c04b416b112e456699cff ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
e3a227ffe23368b152d0ba4a4e81a95b70d35e9870b2a8dc7b2048d6901a7f05 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.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
|
||||
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
|
||||
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
|
||||
@@ -43,8 +43,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
|
||||
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
|
||||
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
f7fc1aee1129cee46a30d5876e3e53f11d2661925eae19cb1e14b47deae83e9e ./lang/en.json
|
||||
c05d100e9efa1a82747ffc709157a9bdd6d109b7a80c6e140d582a28d8c3de13 ./lang/pl.json
|
||||
e5c6a6051381e28d74b75e48232de13d02eb87519d29c847b7ab7979ce47d082 ./lang/en.json
|
||||
02c650f603bca1bd17d9d59193c176ee6ddccb8304f2e658d7ba7730f8ff5f83 ./lang/pl.json
|
||||
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
||||
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
|
||||
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
|
||||
@@ -107,7 +107,7 @@ b24092f34a46626d64628b510eba82193f0b203fd097f6a621b91af5d91eb571 ./src/api/auth
|
||||
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
|
||||
5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs
|
||||
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
|
||||
b9a9a68e47fdaa577c28063fa68bf95075bc355bff81addca4ee448ef70dd818 ./src/api/flows.rs
|
||||
2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs
|
||||
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
3cc64b9c992805f3fbf01c0785cf29ce3121750bf30bd6420ed6e5cf2f8df422 ./src/api/history.rs
|
||||
@@ -203,19 +203,19 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree
|
||||
2822a98a4d7f72f2b89b70878aa0f9bd3f391fde4ea04ba1b94e89a527dcd1fb ./web/css/styles.css
|
||||
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
263d5554e62d2dd26b251f2c43a7e4ec14c8d8d790fa67e55c9bebf16f9d55c0 ./web/index.html
|
||||
d5115a9dc59b4e753dcff9239a77fe49886951d7a4ae7456be80d9201cc42365 ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
|
||||
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.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
|
||||
0e1992a7b93767f1fe4634121ae5aa43900d99f8c2614c2a4cbd5770a49b87c1 ./web/js-dynamic/entities.js
|
||||
eeeef31f767f0aa3efb78bc44a2fea2b9eb86f4d4c50cf1ccae69b5ca30cdfda ./web/js-dynamic/events.js
|
||||
22da18288de3282245f88f9f525d9ad29ec027571e7ea893720a9470170eccd2 ./web/js-dynamic/flows.js
|
||||
6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js
|
||||
a48cb69ae4a2d0d6d512baef346fe499a5a6ea267e071856829df982d4a0f129 ./web/js-dynamic/events.js
|
||||
8e0f294ceeec1d258edb56271469a8a05381e6443a475f76fbeded6637d17503 ./web/js-dynamic/flows.js
|
||||
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
|
||||
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
|
||||
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
|
||||
86031d4f7f7d1b46412c8fbb09ff1ad1b2b4cd84eb949bc749b27663f1dc27fa ./web/js-dynamic/navigation.js
|
||||
c974b6ef41ce8b6def35f5349849a06855c46ec31cb4122a39fcb5c78f6d362a ./web/js-dynamic/navigation.js
|
||||
0e398bad2cde85d4a62d8733c06e73f2b6a5b07db791d4570feba38c7ff8741f ./web/js-dynamic/realtime.js
|
||||
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
|
||||
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
|
||||
|
||||
**Version: 0.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.
|
||||
|
||||
|
||||
+18
-18
@@ -1,13 +1,13 @@
|
||||
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
852768ed3da99ac2e9bbe6c9b67e9a4a667e7115ad9482b6ce4a29ff72d8f914 ./Cargo.lock
|
||||
8e58dd01d95c93ea4f5d0cc5e379cb192221705c9dda41b7b2b7182c49dbff0f ./Cargo.toml
|
||||
82bb41f94fdf5c66ebd9e6caa92e0f1794431067298e0a27c95c8e5bfd68de89 ./Cargo.lock
|
||||
39bdb16196253559e3487aa912ff770c09c7b08cdfbb21e9db1d8af1304ea7e0 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
4a6570c14ba56fc9a4921de6726816264e961a5b82a7fe3fcf475ef96417dad9 ./README.md
|
||||
84a0803bc75dc3199e23ef56c11c6950e8bd888c686ecda7cc369829e8e3073f ./README.md
|
||||
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
|
||||
9873e774746783d65e447b439d069f84c13d2f4e556661e4eb509af59293c2f7 ./docs/API.md
|
||||
58b79297607303151c1de2da74344cd4f6277bc53d80ebe04ed4567d906c963b ./docs/FLOW.md
|
||||
9483ad7cb1e0c8e38f08c722600b6ba059d4273cfb9dcd1af57c11de522d187b ./docs/openapi.json
|
||||
604a795dec077dd7e4f3462a564fdda68e8084aef07da0fcf3dfb6053a46cc58 ./docs/API.md
|
||||
a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md
|
||||
8babaa6830fa70b2d3d8208ef51808e79c9d68c4e007b9ba1537fe582ba03b3d ./docs/openapi.json
|
||||
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
|
||||
0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile
|
||||
adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md
|
||||
@@ -21,7 +21,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
|
||||
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
|
||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
|
||||
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
|
||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
|
||||
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
|
||||
@@ -29,10 +29,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
|
||||
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
|
||||
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
|
||||
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
|
||||
ea5c4eb4ca2656f8d9a2be37a35ba04bb4c53d735c9c04b416b112e456699cff ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
e3a227ffe23368b152d0ba4a4e81a95b70d35e9870b2a8dc7b2048d6901a7f05 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.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
|
||||
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
|
||||
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
|
||||
@@ -42,8 +42,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
|
||||
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
|
||||
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
f7fc1aee1129cee46a30d5876e3e53f11d2661925eae19cb1e14b47deae83e9e ./lang/en.json
|
||||
c05d100e9efa1a82747ffc709157a9bdd6d109b7a80c6e140d582a28d8c3de13 ./lang/pl.json
|
||||
e5c6a6051381e28d74b75e48232de13d02eb87519d29c847b7ab7979ce47d082 ./lang/en.json
|
||||
02c650f603bca1bd17d9d59193c176ee6ddccb8304f2e658d7ba7730f8ff5f83 ./lang/pl.json
|
||||
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
||||
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
|
||||
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
|
||||
@@ -106,7 +106,7 @@ b24092f34a46626d64628b510eba82193f0b203fd097f6a621b91af5d91eb571 ./src/api/auth
|
||||
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
|
||||
5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs
|
||||
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
|
||||
b9a9a68e47fdaa577c28063fa68bf95075bc355bff81addca4ee448ef70dd818 ./src/api/flows.rs
|
||||
2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs
|
||||
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
3cc64b9c992805f3fbf01c0785cf29ce3121750bf30bd6420ed6e5cf2f8df422 ./src/api/history.rs
|
||||
@@ -202,19 +202,19 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree
|
||||
2822a98a4d7f72f2b89b70878aa0f9bd3f391fde4ea04ba1b94e89a527dcd1fb ./web/css/styles.css
|
||||
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
263d5554e62d2dd26b251f2c43a7e4ec14c8d8d790fa67e55c9bebf16f9d55c0 ./web/index.html
|
||||
d5115a9dc59b4e753dcff9239a77fe49886951d7a4ae7456be80d9201cc42365 ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
|
||||
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.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
|
||||
0e1992a7b93767f1fe4634121ae5aa43900d99f8c2614c2a4cbd5770a49b87c1 ./web/js-dynamic/entities.js
|
||||
eeeef31f767f0aa3efb78bc44a2fea2b9eb86f4d4c50cf1ccae69b5ca30cdfda ./web/js-dynamic/events.js
|
||||
22da18288de3282245f88f9f525d9ad29ec027571e7ea893720a9470170eccd2 ./web/js-dynamic/flows.js
|
||||
6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js
|
||||
a48cb69ae4a2d0d6d512baef346fe499a5a6ea267e071856829df982d4a0f129 ./web/js-dynamic/events.js
|
||||
8e0f294ceeec1d258edb56271469a8a05381e6443a475f76fbeded6637d17503 ./web/js-dynamic/flows.js
|
||||
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
|
||||
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
|
||||
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
|
||||
86031d4f7f7d1b46412c8fbb09ff1ad1b2b4cd84eb949bc749b27663f1dc27fa ./web/js-dynamic/navigation.js
|
||||
c974b6ef41ce8b6def35f5349849a06855c46ec31cb4122a39fcb5c78f6d362a ./web/js-dynamic/navigation.js
|
||||
0e398bad2cde85d4a62d8733c06e73f2b6a5b07db791d4570feba38c7ff8741f ./web/js-dynamic/realtime.js
|
||||
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
|
||||
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
|
||||
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
# 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)
|
||||
|
||||
@@ -240,7 +240,7 @@ Response:
|
||||
{
|
||||
"status": "ok",
|
||||
"name": "gree-controller",
|
||||
"version": "0.15.1",
|
||||
"version": "0.15.2",
|
||||
"uptime_seconds": 1234,
|
||||
"control_ready": true,
|
||||
"time": "2026-08-30T06:54:00Z"
|
||||
@@ -303,7 +303,7 @@ Returns the initial Web UI snapshot:
|
||||
"control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []},
|
||||
"control_plan_revision": 42,
|
||||
"system": {
|
||||
"version": "0.15.1",
|
||||
"version": "0.15.2",
|
||||
"uptime_seconds": 1234,
|
||||
"auth_required": false,
|
||||
"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
|
||||
|
||||
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
@@ -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.
|
||||
- `group_action` calls the existing `control_group` path, including custom group targets.
|
||||
- `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.
|
||||
- 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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
+3
-3
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.15.1",
|
||||
"version": "0.15.2",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 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
|
||||
|
||||
- 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"
|
||||
version: "0.15.1"
|
||||
version: "0.15.2"
|
||||
slug: "gree_controller"
|
||||
description: "Local GREE HVAC controller with Web UI and Home Assistant integration"
|
||||
url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/"
|
||||
|
||||
@@ -652,6 +652,9 @@
|
||||
"groups.noZones": "Create thermostat zones before creating a group.",
|
||||
"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.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.fullscreen": "Full screen",
|
||||
"simulation.exitFullscreen": "Exit full screen",
|
||||
@@ -952,6 +955,7 @@
|
||||
"flow.node.not": "NOT",
|
||||
"flow.node.thermostat": "Thermostat",
|
||||
"flow.node.greeDevice": "GREE device",
|
||||
"flow.node.greeFeature": "Single GREE function",
|
||||
"flow.node.group": "Group",
|
||||
"common.yes": "Yes",
|
||||
"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.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.deviceFeature": "Unit function",
|
||||
"flow.deviceFeatureHint": "This action sends exactly one DeviceCommand field. All other unit settings remain unchanged.",
|
||||
"settings.haConnection": "Home Assistant connection",
|
||||
"settings.haConnectionHint": "Server address and credentials used by all Home Assistant features.",
|
||||
"settings.haThermostatSources": "Thermostat temperature sources",
|
||||
|
||||
@@ -652,6 +652,9 @@
|
||||
"groups.noZones": "Najpierw utwórz strefy termostatów.",
|
||||
"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.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.fullscreen": "Pełny ekran",
|
||||
"simulation.exitFullscreen": "Wyjdź z pełnego ekranu",
|
||||
@@ -952,6 +955,7 @@
|
||||
"flow.node.not": "NOT",
|
||||
"flow.node.thermostat": "Termostat",
|
||||
"flow.node.greeDevice": "Urządzenie GREE",
|
||||
"flow.node.greeFeature": "Pojedyncza funkcja GREE",
|
||||
"flow.node.group": "Grupa",
|
||||
"common.yes": "Tak",
|
||||
"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.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.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.haConnectionHint": "Adres serwera i dane dostępu używane przez wszystkie funkcje Home Assistant.",
|
||||
"settings.haThermostatSources": "Źródła temperatury sterowania",
|
||||
|
||||
+114
-17
@@ -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 {
|
||||
let digest = Sha256::digest(format!("{flow_id}:{action_node_id}").as_bytes());
|
||||
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 {
|
||||
matches!(
|
||||
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()));
|
||||
}
|
||||
}
|
||||
"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" => {
|
||||
let domain = flow_string(&action_node.config, "domain").ok_or_else(|| {
|
||||
AppError::BadRequest("Home Assistant action needs a domain".into())
|
||||
@@ -1708,7 +1784,7 @@ fn dry_run_block_reason(
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
"device_action" => {
|
||||
"device_action" | "device_feature_action" => {
|
||||
let Some(device_id) = flow_string(&action.config, "device_id") else {
|
||||
return Ok(Some("missing_device".into()));
|
||||
};
|
||||
@@ -1737,25 +1813,30 @@ fn dry_run_block_reason(
|
||||
}) {
|
||||
return Ok(Some("temporary_quick_thermostat".into()));
|
||||
}
|
||||
let command = if action.kind == "device_feature_action" {
|
||||
flow_device_feature_command(&action.config)?
|
||||
} else {
|
||||
let mut command = DeviceCommand::default();
|
||||
command.power = flow_bool(&action.config, "power");
|
||||
command.mode = flow_string(&action.config, "mode");
|
||||
command.target_temperature = flow_f64(&action.config, "target_temperature");
|
||||
command.fan_speed = flow_u8(&action.config, "fan_speed");
|
||||
command.swing_vertical = flow_louver_position(&action.config, "swing_vertical");
|
||||
command.swing_horizontal = flow_louver_position(&action.config, "swing_horizontal");
|
||||
command.quiet = flow_bool(&action.config, "quiet");
|
||||
command.turbo = flow_bool(&action.config, "turbo");
|
||||
command.light = flow_bool(&action.config, "light");
|
||||
command.air = flow_bool(&action.config, "air");
|
||||
command.xfan = flow_bool(&action.config, "xfan");
|
||||
command.health = flow_bool(&action.config, "health");
|
||||
command.sleep = flow_bool(&action.config, "sleep");
|
||||
command
|
||||
};
|
||||
if zones.iter().any(|z| z.device_id == device_id && !z.enabled)
|
||||
&& flow_bool(&action.config, "power") != Some(true)
|
||||
&& command.power != Some(true)
|
||||
{
|
||||
return Ok(Some("zone_disabled".into()));
|
||||
}
|
||||
let mut command = DeviceCommand::default();
|
||||
command.power = flow_bool(&action.config, "power");
|
||||
command.mode = flow_string(&action.config, "mode");
|
||||
command.target_temperature = flow_f64(&action.config, "target_temperature");
|
||||
command.fan_speed = flow_u8(&action.config, "fan_speed");
|
||||
command.swing_vertical = flow_louver_position(&action.config, "swing_vertical");
|
||||
command.swing_horizontal = flow_louver_position(&action.config, "swing_horizontal");
|
||||
command.quiet = flow_bool(&action.config, "quiet");
|
||||
command.turbo = flow_bool(&action.config, "turbo");
|
||||
command.light = flow_bool(&action.config, "light");
|
||||
command.air = flow_bool(&action.config, "air");
|
||||
command.xfan = flow_bool(&action.config, "xfan");
|
||||
command.health = flow_bool(&action.config, "health");
|
||||
command.sleep = flow_bool(&action.config, "sleep");
|
||||
if zones.iter().any(|z| z.device_id == device_id)
|
||||
&& 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_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
@@ -982,7 +982,8 @@
|
||||
<div class="flow-palette-group block-category-action"><span data-i18n="flow.actions">Akcje</span>
|
||||
<button type="button" data-flow-add="zone_thermostat"
|
||||
data-i18n="flow.node.thermostat">Termostat</button><button type="button" data-flow-add="device_action"
|
||||
data-i18n="flow.node.greeDevice">Urządzenie GREE</button><button type="button" data-flow-add="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>
|
||||
</div>
|
||||
<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>
|
||||
</select></label>
|
||||
</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
|
||||
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
|
||||
|
||||
@@ -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 deviceCommandFieldLabel = key => {
|
||||
const translationKey = {
|
||||
power: 'common.power',
|
||||
mode: 'common.mode',
|
||||
target_temperature: 'common.targetC',
|
||||
fan_speed: 'common.fan',
|
||||
swing_vertical: 'devices.swingVertical',
|
||||
swing_horizontal: 'devices.swingHorizontal',
|
||||
|
||||
@@ -712,6 +712,18 @@ function renderSchedules() {
|
||||
}).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() {
|
||||
$('#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;
|
||||
@@ -720,8 +732,10 @@ function renderAutomations() {
|
||||
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 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>
|
||||
<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>`;
|
||||
}).join('') : `<div class="empty"><strong>${esc(tr('automations.emptyTitle'))}</strong>${esc(tr('automations.emptyText'))}</div>`;
|
||||
}
|
||||
|
||||
@@ -643,7 +643,7 @@ $('#scheduleForm').addEventListener('submit', async event => {
|
||||
$('#automationForm').addEventListener('submit', async event => {
|
||||
event.preventDefault(); const form = event.currentTarget, raw = Object.fromEntries(new FormData(form)); const id = raw.id;
|
||||
const groupTarget = raw.action_target_kind === 'group';
|
||||
const action = {}; if (raw.action_power !== '') action.power = raw.action_power === 'true'; if (raw.action_mode) action.mode = raw.action_mode; if (!groupTarget && raw.action_target_temperature !== '') action.target_temperature = parseDecimal(raw.action_target_temperature); if (!groupTarget && raw.action_swing_vertical !== '') action.swing_vertical = 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) };
|
||||
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'));
|
||||
|
||||
+45
-1
@@ -29,6 +29,7 @@ const FLOW_NODE_META = Object.freeze({
|
||||
logic_not: { titleKey: 'flow.node.not', category: 'logic' },
|
||||
zone_thermostat: { titleKey: 'flow.node.thermostat', category: 'action' },
|
||||
device_action: { titleKey: 'flow.node.greeDevice', category: 'action' },
|
||||
device_feature_action: { titleKey: 'flow.node.greeFeature', category: 'action' },
|
||||
group_action: { titleKey: 'flow.node.group', category: 'action' },
|
||||
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 === '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_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 === 'ha_service_action') return { domain: 'switch', service: 'turn_on', entity_id: '', data: {}, cooldown_seconds: 60 };
|
||||
return {};
|
||||
@@ -357,6 +359,7 @@ function flowNodeSummary(node) {
|
||||
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 === '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 === 'ha_service_action') return `${c.domain || 'domain'}.${c.service || 'service'} · ${c.entity_id || 'entity_id'}`;
|
||||
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 === '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_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 === '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>`;
|
||||
@@ -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>`;
|
||||
}
|
||||
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) {
|
||||
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);
|
||||
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') {
|
||||
const wasLouver = ['swing_vertical', 'swing_horizontal'].includes(node.config.field);
|
||||
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 === '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 (['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;
|
||||
app.flowDirty = true; renderFlowEditor();
|
||||
}
|
||||
|
||||
@@ -547,16 +547,19 @@ function updateAutomationTargetFields() {
|
||||
$('#automationGroupPreset').hidden = !groupTarget;
|
||||
$('#automationTargetTemperature').hidden = groupTarget;
|
||||
$('#automationDeviceSwingOptions').hidden = groupTarget;
|
||||
$('#automationDeviceFeatureOptions').hidden = groupTarget;
|
||||
$('#automationGroupHint').hidden = !groupTarget;
|
||||
form.action_device_id.required = !groupTarget;
|
||||
form.action_group_id.required = groupTarget;
|
||||
form.action_target_temperature.disabled = groupTarget;
|
||||
form.action_swing_vertical.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) {
|
||||
form.action_target_temperature.value = '';
|
||||
form.action_swing_vertical.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 => {
|
||||
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_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_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;
|
||||
updateAutomationTargetFields();
|
||||
openDialog('automationDialog');
|
||||
|
||||
Reference in New Issue
Block a user