From 09c10d80080cc06336d3bc81afb72b712cf42374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 21 Sep 2026 10:05:00 +0200 Subject: [PATCH] v0.15.12 --- Cargo.lock | 2 +- Cargo.toml | 2 +- FILE_MANIFEST.sha256 | 32 +-- README.md | 8 +- SOURCE_MANIFEST.sha256 | 30 +-- docs/API.md | 6 +- docs/openapi.json | 4 +- .../gree_controller/manifest.json | 2 +- .../repository/gree-controller/CHANGELOG.md | 6 + ha-addon/repository/gree-controller/README.md | 2 +- .../repository/gree-controller/config.yaml | 2 +- lang/en.json | 40 +++ lang/pl.json | 40 +++ scripts/api_dev_test.py | 6 +- web/css/styles.css | 255 ++++++++++++++++++ web/index.html | 44 +++ web/js-dynamic/flows.js | 183 ++++++++++++- 17 files changed, 612 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc5dfa3..158a3a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -719,7 +719,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.15.11" +version = "0.15.12" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 96a8402..10ce1f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index 74e9972..21a73b1 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -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 diff --git a/README.md b/README.md index 14496a2..adff5d5 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API. -**Version: 0.15.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. diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256 index eb650e1..e65dcec 100644 --- a/SOURCE_MANIFEST.sha256 +++ b/SOURCE_MANIFEST.sha256 @@ -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 diff --git a/docs/API.md b/docs/API.md index 204a2ec..abd670a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # GREE Controller API reference -HTTP and WebSocket API for GREE Controller **0.15.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, diff --git a/docs/openapi.json b/docs/openapi.json index 921ca83..5b53704 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -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", diff --git a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json index 9b2184b..74c7282 100644 --- a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json +++ b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "gree_controller", "name": "GREE Controller", - "version": "0.15.11", + "version": "0.15.12", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/ha-addon/repository/gree-controller/CHANGELOG.md b/ha-addon/repository/gree-controller/CHANGELOG.md index 36af118..1d2f0df 100644 --- a/ha-addon/repository/gree-controller/CHANGELOG.md +++ b/ha-addon/repository/gree-controller/CHANGELOG.md @@ -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. diff --git a/ha-addon/repository/gree-controller/README.md b/ha-addon/repository/gree-controller/README.md index 55d90b7..aeba6a3 100644 --- a/ha-addon/repository/gree-controller/README.md +++ b/ha-addon/repository/gree-controller/README.md @@ -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) diff --git a/ha-addon/repository/gree-controller/config.yaml b/ha-addon/repository/gree-controller/config.yaml index cb3d9e9..62e3af6 100644 --- a/ha-addon/repository/gree-controller/config.yaml +++ b/ha-addon/repository/gree-controller/config.yaml @@ -1,5 +1,5 @@ name: "GREE Controller" -version: "0.15.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/" diff --git a/lang/en.json b/lang/en.json index a3fd6a9..8ceeb24 100644 --- a/lang/en.json +++ b/lang/en.json @@ -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", diff --git a/lang/pl.json b/lang/pl.json index 11aca05..e3e2c9b 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -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", diff --git a/scripts/api_dev_test.py b/scripts/api_dev_test.py index a4b60b6..71dde8c 100644 --- a/scripts/api_dev_test.py +++ b/scripts/api_dev_test.py @@ -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)) diff --git a/web/css/styles.css b/web/css/styles.css index 230f70c..c817b30 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -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; + } +} diff --git a/web/index.html b/web/index.html index b297165..c8629d6 100644 --- a/web/index.html +++ b/web/index.html @@ -1162,6 +1162,50 @@ + +
+
+
Quick edit +

Edit description

+
+
+

+ +

Only the description is changed. You do not need to open the Flow editor.

+
+
+
+ + +
+
+
Simple preview +

Simulator

+
+
+
+
Set a situation and see whether each Flow action would run.
+ Preview only — nothing is changed +
+
+
Situation to check

Leave values unchanged to use the current application and Home Assistant state.

+ +
+
+
+
+
What will happen?
+
+
+
+
+
diff --git a/web/js-dynamic/flows.js b/web/js-dynamic/flows.js index 3a36251..68c052c 100644 --- a/web/js-dynamic/flows.js +++ b/web/js-dynamic/flows.js @@ -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 => `
-

${esc(flow.name)}${flow.draft ? ` ${esc(tr('flow.draft'))}` : ''}

${esc(flow.description || tr('flow.defaultDescription'))}

${flow.draft ? `` : ``}
+

${esc(flow.name)}${flow.draft ? ` ${esc(tr('flow.draft'))}` : ''}

${esc(flow.description || tr('flow.defaultDescription'))}

${flow.draft ? `` : ``}
${esc(tr('flow.blocks'))}${flow.nodes?.length || 0}
${esc(tr('nav.schedules'))}${flow.compiled_schedule_ids?.length || 0}
${esc(tr('nav.automations'))}${flow.compiled_automation_ids?.length || 0}
- +
`).join('') : `
${esc(tr('flow.emptyTitle'))}${esc(tr('flow.emptyText'))}
`; } +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 = ``; + if (louverAxis) return ``; + if (['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(source.kind)) { + return ``; + } + if (source.kind === 'house_mode') { + return ``; + } + if (flowSimpleBooleanSource(node)) { + return ``; + } + return ``; +} + +function renderFlowSimpleSimulationOverrides(flow) { + const host = $('#flowSimpleSimulationOverrides'); if (!host) return; + const nodes = flowSimulationOverrideNodes(flow); + host.innerHTML = nodes.length ? nodes.map(node => ``).join('') : `

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

`; +} + +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 = `
${esc(tr('flow.simpleNoActions'))}${esc(tr('flow.simpleNoActionsHint'))}
`; + 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 `
${uiIcon(item.matched ? 'check' : 'close')}${esc(flowNodeTitle(FLOW_NODE_META[node.kind] || { title: node.kind }))}${esc(flowNodeSummary(node))}${actual ? `${esc(tr('flow.simpleCurrentValue', { value: actual }))}` : ''}
`; + }).join(''); + const actionType = actionNode ? flowNodeTitle(FLOW_NODE_META[actionNode.kind] || { title: actionNode.kind }) : tr('flow.simpleEffect'); + return `
${esc(`${tr('flow.simpleEffect')} · ${actionType}`)}${esc(actionNode ? flowNodeSummary(actionNode) : action.node_id)}
${esc(stateLabel)}

${esc(explanation)}

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

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

`; + 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 = `
${esc(tr('flow.simulationFailed'))}${esc(error.message)}
`; + } 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));