v0.15.12
This commit is contained in:
Generated
+1
-1
@@ -719,7 +719,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.15.11"
|
||||
version = "0.15.12"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user