This commit is contained in:
Mateusz Gruszczyński
2026-09-21 10:05:00 +02:00
parent 334102d9cb
commit 09c10d8008
17 changed files with 612 additions and 52 deletions
Generated
+1 -1
View File
@@ -719,7 +719,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.15.11"
version = "0.15.12"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
version = "0.15.11"
version = "0.15.12"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+16 -16
View File
@@ -1,14 +1,14 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
2cce9f785964fb3c7c8ca9a7c37983f433e8d36f4eeb38feb8ce6bf0e432d837 ./Cargo.lock
d328063759f1b237ab9c4d83f9341c2f99e61fe0f172627a5b82786f7d3b2847 ./Cargo.toml
fe38ab5ecf7e55633da8da754161d6e91ed089517010592dc69b39dd65600475 ./Cargo.lock
074e24db6677b4390a5e11e8340fa69c98a264f2506ffaf9500f3f5196051250 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
0688a06f2ad42c4ac6cd55de4347a4607c0dcf5c89effe23ffbc072b7ace8979 ./README.md
f12d116d020194917075a25902769811ad0c397352501edf04c6a70df7775107 ./SOURCE_MANIFEST.sha256
1020631c965ab87428c272bc4df202848cecf3b214e2c93fe1c1978eb1153345 ./README.md
fd9dcd01ade48fd8df4ff6cd7142d1aefd9a018222fb0fa1dde76cb7abb5e8db ./SOURCE_MANIFEST.sha256
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
9470febdeefedea0cd3e11a520d23ce902bc5f3e716db3e2a46766c0c689247e ./docs/API.md
19de46a570f96954c39cfd70663f217f96c6948ac81323a7660e39a387f8dad4 ./docs/API.md
43af7941ec4d427f9db7ab3cc6baf3d182a72dba7a6791f14094fee138324d8d ./docs/FLOW.md
516f42b4eb703d852907f1a305ef66cf0ac6871d0186b635558f9e3bae997180 ./docs/openapi.json
9c8e35060b37f1cc3a0d6e6f57d7227fd37234efd9722cd73d041a33b0cad0b3 ./docs/openapi.json
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./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
337a9267c67f4a80db4adaed866407ead703c1a062d37adcb910829d745345d3 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
69cdb6e2f4a5d0114be0c45d53f7c4796fa93007e2d11c2384debf47f4e53847 ./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
21c6c44328caaf94ed326baa999a9f6a387286f79ae348bf30d3ff6ac3cd3f1e ./ha-addon/repository/gree-controller/CHANGELOG.md
3cf1d09992ad73435644c1d2f9d81fa6a65e5165ae8cbcd389a1721a18f34a51 ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
b780c1f7a4cb91fab9731afe7bba1cbaa332a383c2f1d529cb87f88c4ade1f4b ./ha-addon/repository/gree-controller/README.md
b87304c0aa4c9860a877c86fa6fde8b49b5e214b1b8aec5724cebeffb9d5e8bd ./ha-addon/repository/gree-controller/config.yaml
674e3d11fcd7366e25ec057cb25cab96523da615d87e06ae00a07c5773d1013c ./ha-addon/repository/gree-controller/README.md
a872cdef7404ad2067e788c31165625b25b3598d00f8134bc23e7f08713cf028 ./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
4c08eb68d926820016d18a4def98cde4afb8b5a872741d51dc783b219728efd9 ./lang/en.json
3709aed65ef9ab9608691f05eccc81b2d596323eccd7c919e5986c179a524d7f ./lang/pl.json
e6a37278711ada7aa00f4fefde0d2dbde29fef27ad7cbd90c77fbff0045667b4 ./lang/en.json
9261b3e8850a35317c070ec5f8671a189f0bcb22f5f3ca20bdf18be53cca8b6f ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -85,7 +85,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
5de58548e81c7ba571a875090c59a2bdbcab42aeaaa8ccc0e0925274b4b05f9c ./scripts/api_dev_test.py
2b48874658103715b2434ff22da54c29bf1d1acddac8bc22ed35d9be6a9aeffb ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -199,10 +199,10 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
8275cbe14577317709baff1ea7efb2b9b2708c3aefb350f771d4bd24530d3c02 ./web/css/styles.css
223b22c1f025a6d4257dec56901b467c0614a24db7ff13a558f6b8cb1ef2277b ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
c2e94a624d3d4e113cb9d98b2ba314a39e4ae6a2ad9c23ddc8d6558d1327324e ./web/index.html
92847143a358e6869ed4f21b156b10bda3f8abfcb9a12fc86256221c93be3b09 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
@@ -210,7 +210,7 @@ e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynam
752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js
6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js
b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynamic/events.js
f165bbf2148cf1e593d8866b4cdc71887da903d3eaa9a32d4c033aff944c5d01 ./web/js-dynamic/flows.js
53482b11bab852d0bd19d66fc3c2b2417ea628dde33f449af0f472ae395f1f30 ./web/js-dynamic/flows.js
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
+4 -4
View File
@@ -2,9 +2,9 @@
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
**Version: 0.15.11**
**Version: 0.15.12**
Version 0.15.11 refines Visual Flow editing: keyboard actions use a compact in-editor status instead of intrusive success toasts, right-click opens a context menu for block/canvas/connection actions, and `Ctrl/Cmd+Z` undoes graph and setting edits. Direct Flow export from the Flow list remains available.
Version 0.15.12 adds fast Flow-list workflows: descriptions can be edited without opening the visual editor, and every saved Flow has a simple non-mutating simulator that shows whether actions will run for the selected time and sensor/state scenario.
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
> Built-in API: `/api/*` and `/ws`; interactive Swagger docs: `/api-docs`; OpenAPI JSON: `/api-docs/openapi.json`.
@@ -503,6 +503,6 @@ Flow reuses the existing thermostat, group and device-control domains instead of
The editor supports optimistic revisions, so saving an older copy from another browser/tab returns HTTP 409 instead of overwriting a newer graph. Flow compilation/replacement is serialized with configuration, automation, schedule and thermostat-cycle locks, and generated schedules/automations plus the Flow source are replaced in one SQLite transaction. Runtime condition evaluation happens before the automation execution lock; after acquiring it the rule is reloaded and stale snapshots are discarded. Same-cycle actions claim their target devices deterministically, and Flow/device thermostat writes take the schedule -> thermostat-cycle -> zone -> device path. Home Assistant read failures fail closed instead of making NOT/neq logic accidentally true.
Diagnostics include a non-mutating dry-run endpoint/UI with a selectable simulation time, optional per-block sensor/state overrides, a per-node condition trace and an ownership/block reason. Shared Home Assistant Flow inputs can also be tested directly in their editor against the current HA entity state. Flow-scoped execution and dry-run events can be viewed from the editor.
Diagnostics include a non-mutating dry-run endpoint/UI with a selectable simulation time, optional per-block sensor/state overrides, a per-node condition trace and an ownership/block reason. The Flow list also exposes a simpler scenario simulator beside the editor button: it uses live values by default and reduces the result to clear will-run / will-not-run / blocked outcomes with readable condition checks. Shared Home Assistant Flow inputs can also be tested directly in their editor against the current HA entity state. Flow-scoped execution and dry-run events can be viewed from the editor.
Individual Flows can be exported/imported as versioned `gree-controller-flow` JSON. A saved Flow can be exported directly from its card in the Flow list without opening the editor; the editor export remains available for saved and unsaved drafts. Generated schedules and automations are intentionally excluded from the portable document and are regenerated from the source graph on import. Incomplete Flows can also be saved as disabled drafts; drafts preserve the editor graph but intentionally generate no schedules or automations until completed and saved normally. Shared inputs show which Flows reference them and link directly to those editors. The preset library includes 37 categorized scenarios for comfort, energy, safety, night, reliability, Home Assistant heat-source coordination and advanced multi-branch logic.
Individual Flows can be exported/imported as versioned `gree-controller-flow` JSON. A saved Flow can be exported directly from its card in the Flow list without opening the editor, and its description can be edited there through a quick modal; the editor export remains available for saved and unsaved drafts. Generated schedules and automations are intentionally excluded from the portable document and are regenerated from the source graph on import. Incomplete Flows can also be saved as disabled drafts; drafts preserve the editor graph but intentionally generate no schedules or automations until completed and saved normally. Shared inputs show which Flows reference them and link directly to those editors. The preset library includes 37 categorized scenarios for comfort, energy, safety, night, reliability, Home Assistant heat-source coordination and advanced multi-branch logic.
+15 -15
View File
@@ -1,13 +1,13 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
2cce9f785964fb3c7c8ca9a7c37983f433e8d36f4eeb38feb8ce6bf0e432d837 ./Cargo.lock
d328063759f1b237ab9c4d83f9341c2f99e61fe0f172627a5b82786f7d3b2847 ./Cargo.toml
fe38ab5ecf7e55633da8da754161d6e91ed089517010592dc69b39dd65600475 ./Cargo.lock
074e24db6677b4390a5e11e8340fa69c98a264f2506ffaf9500f3f5196051250 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
0688a06f2ad42c4ac6cd55de4347a4607c0dcf5c89effe23ffbc072b7ace8979 ./README.md
1020631c965ab87428c272bc4df202848cecf3b214e2c93fe1c1978eb1153345 ./README.md
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
9470febdeefedea0cd3e11a520d23ce902bc5f3e716db3e2a46766c0c689247e ./docs/API.md
19de46a570f96954c39cfd70663f217f96c6948ac81323a7660e39a387f8dad4 ./docs/API.md
43af7941ec4d427f9db7ab3cc6baf3d182a72dba7a6791f14094fee138324d8d ./docs/FLOW.md
516f42b4eb703d852907f1a305ef66cf0ac6871d0186b635558f9e3bae997180 ./docs/openapi.json
9c8e35060b37f1cc3a0d6e6f57d7227fd37234efd9722cd73d041a33b0cad0b3 ./docs/openapi.json
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./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
337a9267c67f4a80db4adaed866407ead703c1a062d37adcb910829d745345d3 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
69cdb6e2f4a5d0114be0c45d53f7c4796fa93007e2d11c2384debf47f4e53847 ./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
21c6c44328caaf94ed326baa999a9f6a387286f79ae348bf30d3ff6ac3cd3f1e ./ha-addon/repository/gree-controller/CHANGELOG.md
3cf1d09992ad73435644c1d2f9d81fa6a65e5165ae8cbcd389a1721a18f34a51 ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
b780c1f7a4cb91fab9731afe7bba1cbaa332a383c2f1d529cb87f88c4ade1f4b ./ha-addon/repository/gree-controller/README.md
b87304c0aa4c9860a877c86fa6fde8b49b5e214b1b8aec5724cebeffb9d5e8bd ./ha-addon/repository/gree-controller/config.yaml
674e3d11fcd7366e25ec057cb25cab96523da615d87e06ae00a07c5773d1013c ./ha-addon/repository/gree-controller/README.md
a872cdef7404ad2067e788c31165625b25b3598d00f8134bc23e7f08713cf028 ./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
4c08eb68d926820016d18a4def98cde4afb8b5a872741d51dc783b219728efd9 ./lang/en.json
3709aed65ef9ab9608691f05eccc81b2d596323eccd7c919e5986c179a524d7f ./lang/pl.json
e6a37278711ada7aa00f4fefde0d2dbde29fef27ad7cbd90c77fbff0045667b4 ./lang/en.json
9261b3e8850a35317c070ec5f8671a189f0bcb22f5f3ca20bdf18be53cca8b6f ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -84,7 +84,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
5de58548e81c7ba571a875090c59a2bdbcab42aeaaa8ccc0e0925274b4b05f9c ./scripts/api_dev_test.py
2b48874658103715b2434ff22da54c29bf1d1acddac8bc22ed35d9be6a9aeffb ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -198,10 +198,10 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
8275cbe14577317709baff1ea7efb2b9b2708c3aefb350f771d4bd24530d3c02 ./web/css/styles.css
223b22c1f025a6d4257dec56901b467c0614a24db7ff13a558f6b8cb1ef2277b ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
c2e94a624d3d4e113cb9d98b2ba314a39e4ae6a2ad9c23ddc8d6558d1327324e ./web/index.html
92847143a358e6869ed4f21b156b10bda3f8abfcb9a12fc86256221c93be3b09 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
@@ -209,7 +209,7 @@ e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynam
752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js
6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js
b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynamic/events.js
f165bbf2148cf1e593d8866b4cdc71887da903d3eaa9a32d4c033aff944c5d01 ./web/js-dynamic/flows.js
53482b11bab852d0bd19d66fc3c2b2417ea628dde33f449af0f472ae395f1f30 ./web/js-dynamic/flows.js
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
+3 -3
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.15.11**.
HTTP and WebSocket API for GREE Controller **0.15.12**.
[← Main documentation](../README.md)
@@ -241,7 +241,7 @@ Response:
{
"status": "ok",
"name": "gree-controller",
"version": "0.15.11",
"version": "0.15.12",
"uptime_seconds": 1234,
"control_ready": true,
"time": "2026-08-30T06:54:00Z"
@@ -304,7 +304,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.11",
"version": "0.15.12",
"uptime_seconds": 1234,
"auth_required": false,
"control_ready": true,
+2 -2
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "GREE Controller API",
"version": "0.15.11",
"version": "0.15.12",
"summary": "Local HTTP/WebSocket API for GREE Controller",
"description": "Local API used by the GREE Controller Web UI and Home Assistant integration.",
"license": {
@@ -5967,7 +5967,7 @@
},
"version": {
"type": "string",
"example": "0.15.11"
"example": "0.15.12"
},
"uptime_seconds": {
"type": "integer",
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.15.11",
"version": "0.15.12",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
@@ -1,5 +1,11 @@
# Changelog
## 0.15.12
- Flow cards now allow editing the description directly from the Flow list without opening the visual editor.
- Adds a dedicated simple Flow simulator next to the editor button. It opens in a modal, uses the saved Flow, current live values by default, and allows quick scenario overrides plus simulation date/time.
- Simulator results are intentionally simplified to clear “will run / will not run / blocked” outcomes with readable checked conditions and friendly block reasons, while remaining non-mutating.
## 0.15.11
- Routine Visual Flow edit confirmations now appear as a compact status inside the editor instead of global toasts for save/copy/cut/paste/duplicate/undo actions.
@@ -2,7 +2,7 @@
Local GREE HVAC controller packaged as a Home Assistant add-on, with Web UI, HTTP/WebSocket API, built-in Swagger API documentation, UDP discovery/control, `amd64` and `aarch64` support, ingress, and database backup from `/data/gree-controller.db`.
Version 0.15.11 refines Visual Flow editing with compact in-editor shortcut feedback, right-click context menus and `Ctrl/Cmd+Z` undo while retaining keyboard-first authoring and direct Flow export from the Flow list.
Version 0.15.12 adds quick Flow description editing from the Flow list and a simple non-mutating scenario simulator next to the editor button, with clear action outcomes and readable condition checks.
![Home Assistant OS + GREE + VLAN topology](https://git.linuxiarz.pl/gru/gree-controller-ha-addon/raw/branch/master/gree-controller/topology-ha-vlan-gree-en.png)
@@ -1,5 +1,5 @@
name: "GREE Controller"
version: "0.15.11"
version: "0.15.12"
slug: "gree_controller"
description: "Local GREE HVAC controller with Web UI, API, and Home Assistant integration"
url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/"
+40
View File
@@ -901,6 +901,46 @@
"flow.defaultDescription": "Visual control logic",
"flow.compilesAutomatically": "Automatically translated into schedules and automations",
"flow.openEditor": "Open editor",
"flow.editDescription": "Edit description",
"flow.description": "Description",
"flow.descriptionPlaceholder": "Describe what this Flow does…",
"flow.descriptionQuickHint": "Only the description is changed. You do not need to open the Flow editor.",
"flow.descriptionSaved": "Flow description saved.",
"flow.quickEdit": "Quick edit",
"flow.simpleSimulator": "Simulator",
"flow.simpleSimulatorEyebrow": "Simple Flow preview",
"flow.simpleSimulatorHint": "Set a situation and see whether each Flow action would run.",
"flow.simpleSimulatorSafe": "Preview only — nothing is changed",
"flow.simulatorDraftHint": "Save this Flow as a complete Flow before using the simulator.",
"flow.simpleScenario": "Situation to check",
"flow.simpleScenarioHint": "Leave values unchanged to use the current application and Home Assistant state.",
"flow.simpleCheck": "Check what happens",
"flow.simpleResult": "What will happen?",
"flow.simpleEffect": "Effect",
"flow.simpleWillRun": "Will run",
"flow.simpleWillNotRun": "Will not run",
"flow.simpleBlocked": "Blocked",
"flow.simpleWillRunHint": "The conditions match and this action can run in this situation.",
"flow.simpleWillNotRunHint": "At least one required condition does not match in this situation.",
"flow.simpleBlockedHint": "The conditions match, but execution is blocked: {reason}.",
"flow.simpleConditions": "Checked conditions",
"flow.simpleCurrentValue": "Value: {value}",
"flow.simpleNoData": "no data",
"flow.simpleNoActions": "No actions in this Flow",
"flow.simpleNoActionsHint": "Add an action in the Flow editor to simulate an effect.",
"flow.simpleBlockedFlowDisabled": "the Flow is disabled",
"flow.simpleBlockedMissingZone": "the target zone does not exist",
"flow.simpleBlockedMissingDevice": "the target device does not exist",
"flow.simpleBlockedMissingGroup": "the target group does not exist",
"flow.simpleBlockedManual": "manual device control is active",
"flow.simpleBlockedLocalThermostat": "local thermostat override is active",
"flow.simpleBlockedTemporary": "Temporary Quick Thermostat is active",
"flow.simpleBlockedZoneDisabled": "the target zone is disabled",
"flow.simpleBlockedDeviceDisabled": "the target device is disabled",
"flow.simpleBlockedThermostatConflict": "the action conflicts with thermostat control",
"flow.simpleBlockedGroupDisabled": "group control is disabled",
"flow.simpleBlockedUnsupported": "this action is not supported by the simulator",
"flow.simpleBlockedUnknown": "another control rule blocks the action",
"flow.emptyTitle": "No Flows",
"flow.emptyText": "Create your first Flow and build the logic from blocks.",
"flow.editorEyebrow": "Flow editor",
+40
View File
@@ -901,6 +901,46 @@
"flow.defaultDescription": "Wizualna logika sterowania",
"flow.compilesAutomatically": "Automatycznie tłumaczone na harmonogramy i automatyzacje",
"flow.openEditor": "Otwórz edytor",
"flow.editDescription": "Edytuj opis",
"flow.description": "Opis",
"flow.descriptionPlaceholder": "Opisz krótko, co robi ten Flow…",
"flow.descriptionQuickHint": "Zmieni się tylko opis. Nie musisz otwierać edytora Flow.",
"flow.descriptionSaved": "Opis Flow zapisany.",
"flow.quickEdit": "Szybka edycja",
"flow.simpleSimulator": "Symulator",
"flow.simpleSimulatorEyebrow": "Prosty podgląd Flow",
"flow.simpleSimulatorHint": "Ustaw sytuację i sprawdź, czy poszczególne akcje Flow się wykonają.",
"flow.simpleSimulatorSafe": "Tylko podgląd — niczego nie zmienia",
"flow.simulatorDraftHint": "Najpierw zapisz ten Flow jako kompletny, aby użyć symulatora.",
"flow.simpleScenario": "Sytuacja do sprawdzenia",
"flow.simpleScenarioHint": "Zostaw wartości bez zmian, aby użyć aktualnego stanu aplikacji i Home Assistant.",
"flow.simpleCheck": "Sprawdź, co się stanie",
"flow.simpleResult": "Co się wydarzy?",
"flow.simpleEffect": "Efekt",
"flow.simpleWillRun": "Wykona się",
"flow.simpleWillNotRun": "Nie wykona się",
"flow.simpleBlocked": "Zablokowana",
"flow.simpleWillRunHint": "Warunki są spełnione i ta akcja może się wykonać w tej sytuacji.",
"flow.simpleWillNotRunHint": "Co najmniej jeden wymagany warunek nie jest spełniony w tej sytuacji.",
"flow.simpleBlockedHint": "Warunki są spełnione, ale wykonanie blokuje: {reason}.",
"flow.simpleConditions": "Sprawdzone warunki",
"flow.simpleCurrentValue": "Wartość: {value}",
"flow.simpleNoData": "brak danych",
"flow.simpleNoActions": "Ten Flow nie ma akcji",
"flow.simpleNoActionsHint": "Dodaj akcję w edytorze Flow, aby zasymulować efekt.",
"flow.simpleBlockedFlowDisabled": "Flow jest wyłączony",
"flow.simpleBlockedMissingZone": "docelowa strefa nie istnieje",
"flow.simpleBlockedMissingDevice": "docelowe urządzenie nie istnieje",
"flow.simpleBlockedMissingGroup": "docelowa grupa nie istnieje",
"flow.simpleBlockedManual": "aktywne jest ręczne sterowanie urządzeniem",
"flow.simpleBlockedLocalThermostat": "aktywny jest lokalny override termostatu",
"flow.simpleBlockedTemporary": "aktywny jest Tymczasowy szybki termostat",
"flow.simpleBlockedZoneDisabled": "docelowa strefa jest wyłączona",
"flow.simpleBlockedDeviceDisabled": "docelowe urządzenie jest wyłączone",
"flow.simpleBlockedThermostatConflict": "akcja koliduje ze sterowaniem termostatu",
"flow.simpleBlockedGroupDisabled": "sterowanie grupą jest wyłączone",
"flow.simpleBlockedUnsupported": "symulator nie obsługuje tej akcji",
"flow.simpleBlockedUnknown": "inna reguła sterowania blokuje akcję",
"flow.emptyTitle": "Brak Flow",
"flow.emptyText": "Utwórz pierwszy przepływ i ułóż logikę z bloków.",
"flow.editorEyebrow": "Edytor Flow",
+3 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Developer smoke/integration tests for GREE Controller API 0.15.11.
"""Developer smoke/integration tests for GREE Controller API 0.15.12.
Default mode is read-only and safe to run against a real controller.
Use --settings-write to additionally round-trip all split settings resources and
@@ -26,7 +26,7 @@ from typing import Any, Callable, Iterable, Optional
DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787")
DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "")
DEFAULT_EXPECTED_VERSION = "0.15.11"
DEFAULT_EXPECTED_VERSION = "0.15.12"
SETTINGS_PATHS: dict[str, set[str]] = {
"/api/settings/application": {"simulator_enabled"},
@@ -599,7 +599,7 @@ def run_suite(args: argparse.Namespace) -> int:
print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n")
runner.run("public health", lambda: test_health(client, args.expected_version))
runner.run("OpenAPI 0.15.11 contract", lambda: test_openapi(client, args.expected_version))
runner.run("OpenAPI 0.15.12 contract", lambda: test_openapi(client, args.expected_version))
runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client))
runner.run("protected API requires auth", lambda: test_protected_auth(client))
+255
View File
@@ -10534,3 +10534,258 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr
@media (max-width: 760px) {
.flow-editor-notice { max-width: 42vw; }
}
/* Flow quick actions: description editing and simple simulator */
.flow-card-copy {
flex: 1 1 auto;
min-width: 0;
}
.flow-card-description {
display: flex;
align-items: flex-start;
gap: 6px;
}
.flow-card-description p {
flex: 1 1 auto;
margin: .35rem 0 0;
}
.flow-description-edit {
flex: 0 0 auto;
display: grid;
place-items: center;
width: 28px;
height: 28px;
margin-top: 2px;
padding: 0;
border: 0;
background: transparent;
color: var(--muted);
opacity: .75;
}
.flow-description-edit:hover,
.flow-description-edit:focus-visible {
color: var(--text);
opacity: 1;
background: var(--surface-muted);
}
.flow-card .flow-simulate-button {
border: 1px solid var(--line);
background: var(--surface-muted);
color: var(--text);
}
.flow-card .flow-simulate-button:hover:not(:disabled) {
border-color: var(--line-strong);
}
.flow-card .flow-simulate-button:disabled {
opacity: .45;
cursor: not-allowed;
}
.flow-simple-simulation-dialog .dialog-form {
gap: 16px;
}
.flow-simple-intro {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--surface-2);
}
.flow-simple-intro > div {
display: grid;
gap: 3px;
}
.flow-simple-intro > div > span {
color: var(--muted);
font-size: .88rem;
}
.flow-simple-scenario,
.flow-simple-results-section {
display: grid;
gap: 12px;
}
.flow-simple-results-section {
padding-top: 4px;
border-top: 1px solid var(--line);
}
.flow-simple-section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.flow-simple-section-head p {
margin: 4px 0 0;
}
.flow-simple-time {
max-width: 340px;
}
.flow-simple-simulation-overrides {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 10px;
}
.flow-simple-scenario-field {
display: grid;
gap: 7px;
padding: 11px;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--surface-2);
}
.flow-simple-scenario-field > span {
display: grid;
gap: 2px;
}
.flow-simple-scenario-field small {
color: var(--muted);
line-height: 1.35;
}
.flow-simple-results {
display: grid;
gap: 10px;
}
.flow-simple-result {
display: grid;
gap: 10px;
padding: 13px;
border: 1px solid var(--line);
border-left-width: 4px;
border-radius: 12px;
background: var(--surface-2);
}
.flow-simple-result.pass {
border-left-color: var(--teal);
}
.flow-simple-result.blocked,
.flow-simple-result.skip {
border-left-color: var(--line-strong);
}
.flow-simple-result-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.flow-simple-result-head > div {
display: grid;
gap: 3px;
}
.flow-simple-result-head > div > small {
color: var(--muted);
font-weight: 700;
text-transform: uppercase;
letter-spacing: .05em;
}
.flow-simple-result > p {
margin: 0;
color: var(--muted);
}
.flow-simple-details summary {
cursor: pointer;
color: var(--text);
font-weight: 700;
}
.flow-simple-condition-list {
display: grid;
gap: 6px;
margin-top: 9px;
}
.flow-simple-condition {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
padding-top: 7px;
border-top: 1px solid var(--line);
}
.flow-simple-condition > span:nth-child(2) {
display: grid;
gap: 2px;
min-width: 0;
}
.flow-simple-condition small,
.flow-simple-condition em {
color: var(--muted);
font-size: .82rem;
font-style: normal;
}
.flow-simple-condition-mark {
display: grid;
place-items: center;
width: 22px;
height: 22px;
}
.flow-simple-condition.pass .flow-simple-condition-mark {
color: var(--teal);
}
@media (max-width: 760px) {
.flow-card .card-footer {
align-items: flex-start;
}
.flow-card .card-footer > small {
max-width: 100%;
white-space: normal;
}
.flow-card .card-menu {
flex-wrap: wrap;
}
.flow-simple-intro,
.flow-simple-result-head {
align-items: flex-start;
flex-direction: column;
}
.flow-simple-simulation-overrides {
grid-template-columns: 1fr;
}
.flow-simple-condition {
grid-template-columns: auto minmax(0, 1fr);
}
.flow-simple-condition em {
grid-column: 2;
}
}
+44
View File
@@ -1162,6 +1162,50 @@
</div>
</dialog>
<dialog id="flowDescriptionDialog">
<form id="flowDescriptionForm" class="dialog-form">
<div class="dialog-head">
<div><span class="eyebrow" data-i18n="flow.quickEdit">Quick edit</span>
<h2 data-i18n="flow.editDescription">Edit description</h2>
</div><button type="button" data-close data-ui-icon="close"></button>
</div>
<p class="field-note"><strong id="flowDescriptionName"></strong></p>
<label><span data-i18n="flow.description">Description</span><textarea name="description" rows="6" maxlength="1200"
data-i18n-placeholder="flow.descriptionPlaceholder" placeholder="Describe what this Flow does…"></textarea></label>
<p class="field-note" data-i18n="flow.descriptionQuickHint">Only the description is changed. You do not need to open the Flow editor.</p>
<div class="form-actions"><button type="button" class="secondary" data-close
data-i18n="actions.cancel">Cancel</button><button type="submit" class="primary"
data-i18n="actions.save">Save</button></div>
</form>
</dialog>
<dialog id="flowSimpleSimulationDialog" class="dialog-wide flow-simple-simulation-dialog">
<div class="dialog-form">
<div class="dialog-head">
<div><span class="eyebrow" data-i18n="flow.simpleSimulatorEyebrow">Simple preview</span>
<h2 data-i18n="flow.simpleSimulator">Simulator</h2>
</div><button type="button" data-close data-ui-icon="close"></button>
</div>
<div class="flow-simple-intro">
<div><strong id="flowSimpleSimulationName"></strong><span data-i18n="flow.simpleSimulatorHint">Set a situation and see whether each Flow action would run.</span></div>
<span class="badge" data-i18n="flow.simpleSimulatorSafe">Preview only — nothing is changed</span>
</div>
<section class="flow-simple-scenario">
<div class="flow-simple-section-head"><div><strong data-i18n="flow.simpleScenario">Situation to check</strong><p class="field-note"
data-i18n="flow.simpleScenarioHint">Leave values unchanged to use the current application and Home Assistant state.</p></div></div>
<label class="flow-simple-time"><span data-i18n="flow.simulationTime">Simulation date and time</span><input
id="flowSimpleSimulationAt" type="datetime-local"></label>
<div id="flowSimpleSimulationOverrides" class="flow-simple-simulation-overrides"></div>
<div class="form-actions"><button class="primary" type="button" data-action="run-flow-simple-simulation"
data-i18n="flow.simpleCheck">Check what happens</button></div>
</section>
<section class="flow-simple-results-section">
<div class="flow-simple-section-head"><strong data-i18n="flow.simpleResult">What will happen?</strong></div>
<div id="flowSimpleSimulationResults" class="flow-simple-results"></div>
</section>
</div>
</dialog>
<dialog id="flowTestDialog" class="dialog-wide">
<div class="dialog-form">
<div class="dialog-head">
+179 -4
View File
@@ -108,12 +108,55 @@ function renderFlows() {
const host = $('#flowList'); if (!host) return;
const count = $('#flowListCount'); if (count) count.textContent = tr('flow.listCount', { count: app.flows.length });
host.innerHTML = app.flows.length ? app.flows.map(flow => `<article class="list-card flow-card ${flow.enabled && !flow.draft ? '' : 'is-disabled'} ${flow.draft ? 'is-draft' : ''}" data-flow-card-id="${esc(flow.id)}" tabindex="0" aria-label="${esc(`${tr('flow.openEditor')}: ${flow.name}`)}">
<div class="list-card-head"><div><h3>${esc(flow.name)}${flow.draft ? ` <span class="badge flow-draft-badge">${esc(tr('flow.draft'))}</span>` : ''}</h3><p>${esc(flow.description || tr('flow.defaultDescription'))}</p></div>${flow.draft ? `<button type="button" class="enable-toggle" disabled title="${esc(tr('flow.draftDisabledHint'))}"><span>${uiIcon('edit')}</span>${esc(tr('flow.disabledState'))}</button>` : `<button type="button" class="enable-toggle ${flow.enabled ? 'active' : ''}" data-action="toggle-flow-enabled" data-id="${esc(flow.id)}" data-value="${flow.enabled ? 'false' : 'true'}" aria-label="${esc(tr(flow.enabled ? 'flow.disable' : 'flow.enable'))}" title="${esc(tr('flow.quickToggleHint'))}"><span>${uiIcon(flow.enabled ? 'check' : 'circle')}</span>${esc(flow.enabled ? tr('flow.enabledState') : tr('flow.disabledState'))}</button>`}</div>
<div class="list-card-head"><div class="flow-card-copy"><h3>${esc(flow.name)}${flow.draft ? ` <span class="badge flow-draft-badge">${esc(tr('flow.draft'))}</span>` : ''}</h3><div class="flow-card-description"><p>${esc(flow.description || tr('flow.defaultDescription'))}</p><button type="button" class="flow-description-edit" data-action="edit-flow-description" data-id="${esc(flow.id)}" aria-label="${esc(tr('flow.editDescription'))}" title="${esc(tr('flow.editDescription'))}">${uiIcon('edit')}</button></div></div>${flow.draft ? `<button type="button" class="enable-toggle" disabled title="${esc(tr('flow.draftDisabledHint'))}"><span>${uiIcon('edit')}</span>${esc(tr('flow.disabledState'))}</button>` : `<button type="button" class="enable-toggle ${flow.enabled ? 'active' : ''}" data-action="toggle-flow-enabled" data-id="${esc(flow.id)}" data-value="${flow.enabled ? 'false' : 'true'}" aria-label="${esc(tr(flow.enabled ? 'flow.disable' : 'flow.enable'))}" title="${esc(tr('flow.quickToggleHint'))}"><span>${uiIcon(flow.enabled ? 'check' : 'circle')}</span>${esc(flow.enabled ? tr('flow.enabledState') : tr('flow.disabledState'))}</button>`}</div>
<div class="card-stats"><div class="card-stat"><small>${esc(tr('flow.blocks'))}</small><strong>${flow.nodes?.length || 0}</strong></div><div class="card-stat"><small>${esc(tr('nav.schedules'))}</small><strong>${flow.compiled_schedule_ids?.length || 0}</strong></div><div class="card-stat"><small>${esc(tr('nav.automations'))}</small><strong>${flow.compiled_automation_ids?.length || 0}</strong></div></div>
<div class="card-footer"><small>${esc(flow.draft ? tr('flow.draftNoExecution') : tr('flow.compileCount', { schedules: flow.compiled_schedule_ids?.length || 0, automations: flow.compiled_automation_ids?.length || 0 }))}</small><div class="card-menu"><button class="card-primary-action" data-action="edit-flow" data-id="${esc(flow.id)}">${esc(tr('flow.openEditor'))}</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 data-action="export-flow-by-id" data-id="${esc(flow.id)}">${esc(tr('flow.export'))}</button><button class="danger" data-action="delete-flow" data-id="${esc(flow.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
<div class="card-footer"><small>${esc(flow.draft ? tr('flow.draftNoExecution') : tr('flow.compileCount', { schedules: flow.compiled_schedule_ids?.length || 0, automations: flow.compiled_automation_ids?.length || 0 }))}</small><div class="card-menu"><button class="flow-simulate-button" data-action="simulate-flow" data-id="${esc(flow.id)}" ${flow.draft ? `disabled title="${esc(tr('flow.simulatorDraftHint'))}"` : ''}>${esc(tr('flow.simpleSimulator'))}</button><button class="card-primary-action" data-action="edit-flow" data-id="${esc(flow.id)}">${esc(tr('flow.openEditor'))}</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 data-action="export-flow-by-id" data-id="${esc(flow.id)}">${esc(tr('flow.export'))}</button><button class="danger" data-action="delete-flow" data-id="${esc(flow.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
</article>`).join('') : `<div class="empty"><strong>${esc(tr('flow.emptyTitle'))}</strong>${esc(tr('flow.emptyText'))}</div>`;
}
let flowDescriptionTargetId = null;
let flowSimpleSimulationFlow = null;
function flowSavedPayload(flow, changes = {}) {
return {
name: flow.name,
enabled: flow.enabled === true,
draft: flow.draft === true,
description: flow.description || '',
nodes: flow.nodes || [],
edges: flow.edges || [],
expected_revision: Number(flow.revision || 0),
...changes,
};
}
function openFlowDescriptionEditor(id) {
const flow = app.flows.find(item => item.id === id); if (!flow) return toast(tr('flow.notFound'), true);
flowDescriptionTargetId = id;
const form = $('#flowDescriptionForm'); if (!form) return;
$('#flowDescriptionName').textContent = flow.name;
form.elements.description.value = flow.description || '';
$('#flowDescriptionDialog')?.showModal();
requestAnimationFrame(() => form.elements.description?.focus());
}
async function saveFlowDescription() {
const flow = app.flows.find(item => item.id === flowDescriptionTargetId); if (!flow) return toast(tr('flow.notFound'), true);
const form = $('#flowDescriptionForm'); if (!form) return;
const button = form.querySelector('button[type="submit"]');
const description = String(form.elements.description?.value || '').trim();
if (button) button.disabled = true;
try {
const saved = await api(`/api/flows/${encodeURIComponent(flow.id)}`, { method: 'PUT', body: flowSavedPayload(flow, { description }) });
const index = app.flows.findIndex(item => item.id === saved.id); if (index >= 0) app.flows[index] = saved;
$('#flowDescriptionDialog')?.close(); flowDescriptionTargetId = null;
renderFlows(); await loadBootstrap(); toast(tr('flow.descriptionSaved'));
} catch (error) {
toast(error.message, true);
if (error.status === 409) await loadBootstrap();
} finally { if (button) button.disabled = false; }
}
function flowDraftFrom(flow) {
return flow ? JSON.parse(JSON.stringify(flow)) : {
@@ -1165,8 +1208,8 @@ function localDateTimeInputValue(date = new Date()) {
const pad = value => String(value).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
function flowSimulationOverrideNodes() {
return (app.flowDraft?.nodes || []).filter(node => ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_state', 'ha_numeric', 'ha_attribute', 'ha_available', 'house_mode', 'device_state', 'zone_state', 'group_state', 'night_mode', 'shared_input'].includes(node.kind));
function flowSimulationOverrideNodes(flow = app.flowDraft) {
return (flow?.nodes || []).filter(node => ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_state', 'ha_numeric', 'ha_attribute', 'ha_available', 'house_mode', 'device_state', 'zone_state', 'group_state', 'night_mode', 'shared_input'].includes(node.kind));
}
function flowSimulationLouverAxis(node) {
if (node?.kind === 'device_state') {
@@ -1204,6 +1247,134 @@ function collectFlowSimulationOverrides() {
});
return result;
}
function flowSimulationEffectiveSource(node) {
if (node?.kind !== 'shared_input') return { kind: node?.kind || '', config: node?.config || {} };
const item = (app.flowSharedInputs || []).find(value => value.id === node.config?.input_id);
return { kind: item?.kind || 'shared_input', config: item?.config || {}, item };
}
function flowSimpleBooleanSource(node) {
const source = flowSimulationEffectiveSource(node);
if (['ha_available', 'night_mode'].includes(source.kind)) return true;
if (source.kind === 'group_state') return source.config.field === 'power_enabled';
if (source.kind === 'device_state') return ['enabled', 'online', 'power', 'quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep'].includes(source.config.field);
if (source.kind === 'zone_state') return ['enabled', 'demand', 'device_manual_override', 'local_thermostat_power'].includes(source.config.field);
return false;
}
function flowSimpleSimulationControl(node) {
const source = flowSimulationEffectiveSource(node);
const louverAxis = flowSimulationLouverAxis(node);
const live = `<option value="">${esc(tr('flow.useLiveValue'))}</option>`;
if (louverAxis) return `<select data-flow-simple-sim-node="${esc(node.id)}">${live}${louverOptions(louverAxis, null)}</select>`;
if (['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(source.kind)) {
return `<input type="number" step="0.1" data-flow-simple-sim-node="${esc(node.id)}" placeholder="${esc(tr('flow.useLiveValue'))}">`;
}
if (source.kind === 'house_mode') {
return `<select data-flow-simple-sim-node="${esc(node.id)}">${live}${['cool', 'heat', 'off'].map(value => `<option value="${value}">${esc(value === 'off' ? tr('common.off') : tr(`mode.${value}`))}</option>`).join('')}</select>`;
}
if (flowSimpleBooleanSource(node)) {
return `<select data-flow-simple-sim-node="${esc(node.id)}">${live}<option value="true">${esc(tr('common.yes'))}</option><option value="false">${esc(tr('common.no'))}</option></select>`;
}
return `<input type="text" data-flow-simple-sim-node="${esc(node.id)}" placeholder="${esc(tr('flow.useLiveValue'))}">`;
}
function renderFlowSimpleSimulationOverrides(flow) {
const host = $('#flowSimpleSimulationOverrides'); if (!host) return;
const nodes = flowSimulationOverrideNodes(flow);
host.innerHTML = nodes.length ? nodes.map(node => `<label class="flow-simple-scenario-field"><span><strong>${esc(flowNodeTitle(FLOW_NODE_META[node.kind]))}</strong><small>${esc(flowNodeSummary(node))}</small></span>${flowSimpleSimulationControl(node)}</label>`).join('') : `<p class="field-note">${esc(tr('flow.noSimulationOverrides'))}</p>`;
}
function collectFlowSimpleSimulationOverrides(flow) {
const byId = new Map((flow?.nodes || []).map(node => [node.id, node]));
const result = {};
$$('[data-flow-simple-sim-node]', $('#flowSimpleSimulationOverrides')).forEach(input => {
const raw = String(input.value || '').trim(); if (raw === '') return;
const node = byId.get(input.dataset.flowSimpleSimNode); if (!node) return;
const source = flowSimulationEffectiveSource(node);
let value = raw;
if (flowSimulationLouverAxis(node) || ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(source.kind)) value = Number(raw);
else if (/^(true|false)$/i.test(raw)) value = raw.toLowerCase() === 'true';
result[node.id] = value;
});
return result;
}
function flowSimpleSimulationActualText(node, actual) {
if (actual === null || actual === undefined) return tr('flow.simpleNoData');
if (typeof actual === 'object') {
if (actual.error) return tr('flow.simpleNoData');
return '';
}
const source = flowSimulationEffectiveSource(node);
const louverAxis = flowSimulationLouverAxis(node);
if (louverAxis && Number.isFinite(Number(actual))) return louverPositionLabel(louverAxis, Number(actual));
if (['outdoor_temperature', 'device_temperature', 'zone_temperature'].includes(source.kind) && Number.isFinite(Number(actual))) return fmtTemp(Number(actual));
if (source.kind === 'house_mode') return houseModeLabel(String(actual));
if (typeof actual === 'boolean') return actual ? tr('common.yes') : tr('common.no');
if (node?.kind === 'weekday' && Number.isFinite(Number(actual))) return tr(`day.${Number(actual)}`);
return String(actual);
}
function flowSimpleBlockedReason(reason) {
const key = {
flow_disabled: 'flow.simpleBlockedFlowDisabled', missing_zone: 'flow.simpleBlockedMissingZone', missing_device: 'flow.simpleBlockedMissingDevice',
missing_group: 'flow.simpleBlockedMissingGroup', device_manual_override: 'flow.simpleBlockedManual', local_thermostat_override: 'flow.simpleBlockedLocalThermostat',
temporary_quick_thermostat: 'flow.simpleBlockedTemporary', zone_disabled: 'flow.simpleBlockedZoneDisabled', device_disabled: 'flow.simpleBlockedDeviceDisabled',
thermostat_owner_conflict: 'flow.simpleBlockedThermostatConflict', group_control_disabled: 'flow.simpleBlockedGroupDisabled', unsupported_action: 'flow.simpleBlockedUnsupported',
}[reason];
return key ? tr(key) : (reason || tr('flow.simpleBlockedUnknown'));
}
function renderFlowSimpleSimulationResult(result) {
const host = $('#flowSimpleSimulationResults'); const flow = flowSimpleSimulationFlow;
if (!host || !flow) return;
const byId = new Map((flow.nodes || []).map(node => [node.id, node]));
if (!(result.actions || []).length) {
host.innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.simpleNoActions'))}</strong><span>${esc(tr('flow.simpleNoActionsHint'))}</span></div>`;
return;
}
host.innerHTML = (result.actions || []).map(action => {
const actionNode = byId.get(action.node_id);
const state = action.would_execute ? 'pass' : action.matched ? 'blocked' : 'skip';
const stateLabel = action.would_execute ? tr('flow.simpleWillRun') : action.matched ? tr('flow.simpleBlocked') : tr('flow.simpleWillNotRun');
const explanation = action.would_execute ? tr('flow.simpleWillRunHint') : action.matched ? tr('flow.simpleBlockedHint', { reason: flowSimpleBlockedReason(action.blocked_reason) }) : tr('flow.simpleWillNotRunHint');
const trace = (action.trace || []).map(item => {
const node = byId.get(item.node_id); if (!node || ['logic_and', 'logic_or', 'logic_not'].includes(node.kind)) return '';
const actual = flowSimpleSimulationActualText(node, item.actual);
return `<div class="flow-simple-condition ${item.matched ? 'pass' : 'fail'}"><span class="flow-simple-condition-mark">${uiIcon(item.matched ? 'check' : 'close')}</span><span><strong>${esc(flowNodeTitle(FLOW_NODE_META[node.kind] || { title: node.kind }))}</strong><small>${esc(flowNodeSummary(node))}</small></span>${actual ? `<em>${esc(tr('flow.simpleCurrentValue', { value: actual }))}</em>` : ''}</div>`;
}).join('');
const actionType = actionNode ? flowNodeTitle(FLOW_NODE_META[actionNode.kind] || { title: actionNode.kind }) : tr('flow.simpleEffect');
return `<article class="flow-simple-result ${state}"><div class="flow-simple-result-head"><div><small>${esc(`${tr('flow.simpleEffect')} · ${actionType}`)}</small><strong>${esc(actionNode ? flowNodeSummary(actionNode) : action.node_id)}</strong></div><span class="badge ${action.would_execute ? 'active' : ''}">${esc(stateLabel)}</span></div><p>${esc(explanation)}</p>${trace ? `<details class="flow-simple-details" ${action.would_execute ? '' : 'open'}><summary>${esc(tr('flow.simpleConditions'))}</summary><div class="flow-simple-condition-list">${trace}</div></details>` : ''}</article>`;
}).join('');
}
async function runFlowSimpleSimulation() {
const flow = flowSimpleSimulationFlow; if (!flow) return;
const input = $('#flowSimpleSimulationAt')?.value;
const at = input ? new Date(input).toISOString() : new Date().toISOString();
const host = $('#flowSimpleSimulationResults'); if (host) host.innerHTML = `<p class="field-note">${esc(tr('flow.runningSimulation'))}</p>`;
const button = $('[data-action="run-flow-simple-simulation"]', $('#flowSimpleSimulationDialog')); if (button) button.disabled = true;
try {
const result = await api('/api/flows/simulate', { method: 'POST', body: { flow: flowSavedPayload(flow, { expected_revision: undefined }), flow_id: flow.id, at, overrides: collectFlowSimpleSimulationOverrides(flow), log: false } });
renderFlowSimpleSimulationResult(result);
} catch (error) {
if (host) host.innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.simulationFailed'))}</strong><span>${esc(error.message)}</span></div>`;
} finally { if (button) button.disabled = false; }
}
function openFlowSimpleSimulation(id) {
const flow = app.flows.find(item => item.id === id); if (!flow) return toast(tr('flow.notFound'), true);
if (flow.draft) return toast(tr('flow.simulatorDraftHint'), true);
flowSimpleSimulationFlow = JSON.parse(JSON.stringify(flow));
$('#flowSimpleSimulationName').textContent = flow.name;
$('#flowSimpleSimulationAt').value = localDateTimeInputValue();
$('#flowSimpleSimulationResults').innerHTML = '';
renderFlowSimpleSimulationOverrides(flowSimpleSimulationFlow);
$('#flowSimpleSimulationDialog')?.showModal();
runFlowSimpleSimulation();
}
function openFlowDryRun() {
if (!app.flowDraft) return;
$('#flowTestTitle').textContent = tr('flow.dryRun'); $('#flowSimulationControls').hidden = false; $('#flowTestResults').innerHTML = '';
@@ -1702,6 +1873,8 @@ document.addEventListener('click', event => {
if (mobileActionsDialog?.open && action !== 'flow-mobile-actions') mobileActionsDialog.close();
if (action === 'new-flow') openFlowEditor();
else if (action === 'edit-flow') openFlowEditor(actionButton.dataset.id);
else if (action === 'edit-flow-description') openFlowDescriptionEditor(actionButton.dataset.id);
else if (action === 'simulate-flow') openFlowSimpleSimulation(actionButton.dataset.id);
else if (action === 'delete-flow') deleteFlow(actionButton.dataset.id);
else if (action === 'export-flow-by-id') exportFlowById(actionButton.dataset.id);
else if (action === 'toggle-flow-enabled') toggleFlowEnabled(actionButton.dataset.id, actionButton.dataset.value === 'true');
@@ -1722,6 +1895,7 @@ document.addEventListener('click', event => {
else if (action === 'flow-templates') openFlowTemplates();
else if (action === 'flow-dry-run') openFlowDryRun();
else if (action === 'run-flow-dry-run') runFlowDryRun();
else if (action === 'run-flow-simple-simulation') runFlowSimpleSimulation();
else if (action === 'flow-logs') openFlowLogs();
else if (action === 'flow-select-all') selectAllFlowNodes();
else if (action === 'flow-duplicate-selection') duplicateSelectedFlowNodes();
@@ -1743,6 +1917,7 @@ document.addEventListener('click', event => {
document.addEventListener('change', event => {
if (event.target.matches?.('[data-flow-config]')) updateFlowConfig(event.target);
});
$('#flowDescriptionForm')?.addEventListener('submit', event => { event.preventDefault(); saveFlowDescription(); });
$('#flowImportFile')?.addEventListener('change', event => importFlowFile(event.target.files?.[0]));
$('#flowTemplateSearch')?.addEventListener('input', event => { flowPresetSearch = event.target.value || ''; renderFlowPresetBrowser(flowPresetActiveCategory); });
$('#flowBlockSearch')?.addEventListener('input', event => renderFlowBlockLibrary(event.target.value));