This commit is contained in:
Mateusz Gruszczyński
2026-09-18 23:57:28 +02:00
parent 00cbe975bb
commit be3708f14d
17 changed files with 424 additions and 83 deletions
Generated
+1 -1
View File
@@ -719,7 +719,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.15.7"
version = "0.15.8"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
version = "0.15.7"
version = "0.15.8"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+16 -16
View File
@@ -1,14 +1,14 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
bc531fc3bd260aa91bc4b3f000768d740222f6f80cd780cf2bbd4522d82d8841 ./Cargo.lock
8555c5b50711059f76fdbaae495a6a56cae16813af2efd7c69a7fde34762949d ./Cargo.toml
ddda584a029e67d057e3afbe93246795e6579d7efd2f3e9ebe4cfd2bb164e907 ./Cargo.lock
d09e3c8438adcfac6aa9d16de9aade898a1fa6f64f7edf17bcd70afb714db12f ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
4d648f23faf4e57d2900f898ce5416dd0b2be9b842f18e67e8f6b31312c511dc ./README.md
54c111977ab409876fd32101fedac6757f3026106d71b187b34c4c36b53424e9 ./SOURCE_MANIFEST.sha256
5b5586d2e3e7b80c0ffe2d241517f8f7a8c4d7e80169a6cb3cf46221024f54e9 ./README.md
54e2716acb43f0813c6b979df802b8a0f7e3f8f03b1e42b45c746945ed21d098 ./SOURCE_MANIFEST.sha256
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
fe1bd7a84b16782f9ba2c3c301592b52b194698d7e71f0ba806a88ed3c5a9688 ./docs/API.md
a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md
f70388d4fd9a5b35c1e32c43a9a4b4599adc6bd458e388ac571684a9cb1dc20d ./docs/openapi.json
70a8217b46a12ae47e6b461f6f619fa19facb0b422fd8d850208b55237abc752 ./docs/API.md
c6b1aded8ef6f4c840a2b1f2ffa3f0b22eddc080a4750adb34ee950f12f64382 ./docs/FLOW.md
9debc0d7d3f663c2a46ab17fd385a37aa732757e6bb7c75d6d60281ea89092b5 ./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
396c1e2aa1dce367bd1b773ff2e9b80e09729ca58900dd5141085bf30a47f8f0 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
00fc79f5195f43cd8d076b3d9a7a782263f0bde2a6b58b310c2d7a8b307a9f88 ./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
861a1df8677b0539720978aa0e84e7ffc7dea96d37e6d99c6adca2ced0851a44 ./ha-addon/repository/gree-controller/CHANGELOG.md
2976822c9663e505d22297e2749192b4ca119bb4e46ccc1c29c813522c84b3c5 ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md
40d18f2415fb84fe83d79ec19a9c9ca20232d751e02e5713786bac4d38f715a1 ./ha-addon/repository/gree-controller/config.yaml
5f0bd0d1ea4aca47844f9e870e4ba1676ad8b90369577db75a1bb0d00228a9da ./ha-addon/repository/gree-controller/README.md
f43723e7e356c7cc637b36b5ade0e34cfb69c50c4cf653cca7fc013e926cd7c9 ./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
@@ -85,7 +85,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
9083c4a8c602698a53d926864f2fe7e8c9658379f3d82fcb4171f44ec946a7ad ./scripts/api_dev_test.py
3a3287a4ca516f91a1b1bf14257aeef7b1b3027d592200c0a6da1cc70afa9c32 ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -107,7 +107,7 @@ b53e1afab342634fedc218ea4517446f19a5ba7985dee599e191b37d9cc8058a ./src/api.rs
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs
ca5bc21e051222dcaf0c507e84cb3e60f52a0b612a6056847fa0aee3a4d1a672 ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs
@@ -129,7 +129,7 @@ a8fe3636ac79df191e2f2c9d8c1eec1e5181e048ef8c9036cfa18f84aef1a462 ./src/db/confi
4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs
7bc79d9d33726fbdd33b485c8c2e839cedbb8fe29fee872611da978ed149d6a0 ./src/db/energy_history.rs
96aac6510ddda8c18fd7e29802a937b96e8a74badf504980846c80cd7933a4c6 ./src/db/events_tokens.rs
bb5048ee60053f39ce3c1203dc1f24c7047310538d2357f105a052c016d21ec4 ./src/db/flows.rs
bba6e3fb9f4800ae06d0ca67772b06a8c389d308243306719442d0c1f2623420 ./src/db/flows.rs
5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs
4fc007c08304e96818ad1d78997a5b2458e31aa9c48ad68b5be2a007533924bb ./src/db/network_history.rs
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
@@ -199,7 +199,7 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
0b783d6cea599e53fed4ed82fa95e2048c493057c6d57b0dfa423e7cfa10a7ad ./web/css/styles.css
1043dbcfb24a474ce088acb6aa86479e84db74688fb4802febeb3f3f564ca65f ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
cc1509fd76f0b0bd4c069db63e1e104210c2bed05f32a3eebac7642f94ef8a99 ./web/index.html
@@ -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
8e0f294ceeec1d258edb56271469a8a05381e6443a475f76fbeded6637d17503 ./web/js-dynamic/flows.js
879a92e95575ace4775b2f9a7f189e78ad0082ca5c12d012ef456e6477010886 ./web/js-dynamic/flows.js
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
+3 -3
View File
@@ -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.7**
**Version: 0.15.8**
Version 0.15.7 fixes mixed V1/V2 Local discovery: scan-envelope encryption is no longer mistaken for the device protocol, Auto binding verifies the real protocol, and V2-capable modules advertised through legacy discovery are classified correctly from their discovery metadata.
Version 0.15.8 improves Visual Flow portability and editor ergonomics: exported Flows carry the Shared Input definitions they use, imports can create or safely remap missing Shared Inputs, and the Interpretation / Flow cycle panel is collapsed by default on every viewport.
> [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`.
@@ -271,7 +271,7 @@ Schedules select profiles or a custom target for selected ISO weekdays (`1=Monda
The reusable Flow preset library is stored as standalone JSON files in `presets/`. Each file contains its name, description and complete Flow graph, while the UI loads the embedded preset catalog dynamically instead of hard-coding graphs in JavaScript. The library has category tabs, search, local favorites/recent presets, a graph preview and requirement checks before applying a preset.
Available blocks include weekday/time/date ranges, the application Night mode, outdoor/device/zone temperature, house/device/zone/group state, arbitrary Home Assistant state/numeric/attribute/availability checks, a constant diagnostic source, AND/OR/NOT, thermostat actions, direct GREE actions and group actions. The GREE action uses the existing `DeviceCommand` fields (power, mode, target, fan, swing, quiet, turbo, light, air, xfan, health and sleep). Multiple branches can feed an action; the compiled runtime preserves the graph logic.
Available blocks include weekday/time/date ranges, the application Night mode, outdoor/device/zone temperature, house/device/zone/group state, arbitrary Home Assistant state/numeric/attribute/availability checks, reusable Shared Inputs, a constant diagnostic source, AND/OR/NOT, thermostat actions, direct GREE actions and group actions. Flow exports keep the existing format version and carry definitions of referenced Shared Inputs, so import can create missing sources or safely remap ID collisions. The GREE action uses the existing `DeviceCommand` fields (power, mode, target, fan, swing, quiet, turbo, light, air, xfan, health and sleep). Multiple branches can feed an action; the compiled runtime preserves the graph logic. The editor keeps the Interpretation / Flow cycle details collapsed until requested.
Quick preset/setpoint overrides normally hand control back at the next schedule boundary. Temporary Quick Thermostat adds explicit start/finish rules such as duration, exact time, temperature reached/stable or next schedule boundary.
+15 -15
View File
@@ -1,13 +1,13 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
bc531fc3bd260aa91bc4b3f000768d740222f6f80cd780cf2bbd4522d82d8841 ./Cargo.lock
8555c5b50711059f76fdbaae495a6a56cae16813af2efd7c69a7fde34762949d ./Cargo.toml
ddda584a029e67d057e3afbe93246795e6579d7efd2f3e9ebe4cfd2bb164e907 ./Cargo.lock
d09e3c8438adcfac6aa9d16de9aade898a1fa6f64f7edf17bcd70afb714db12f ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
4d648f23faf4e57d2900f898ce5416dd0b2be9b842f18e67e8f6b31312c511dc ./README.md
5b5586d2e3e7b80c0ffe2d241517f8f7a8c4d7e80169a6cb3cf46221024f54e9 ./README.md
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
fe1bd7a84b16782f9ba2c3c301592b52b194698d7e71f0ba806a88ed3c5a9688 ./docs/API.md
a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md
f70388d4fd9a5b35c1e32c43a9a4b4599adc6bd458e388ac571684a9cb1dc20d ./docs/openapi.json
70a8217b46a12ae47e6b461f6f619fa19facb0b422fd8d850208b55237abc752 ./docs/API.md
c6b1aded8ef6f4c840a2b1f2ffa3f0b22eddc080a4750adb34ee950f12f64382 ./docs/FLOW.md
9debc0d7d3f663c2a46ab17fd385a37aa732757e6bb7c75d6d60281ea89092b5 ./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
396c1e2aa1dce367bd1b773ff2e9b80e09729ca58900dd5141085bf30a47f8f0 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
00fc79f5195f43cd8d076b3d9a7a782263f0bde2a6b58b310c2d7a8b307a9f88 ./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
861a1df8677b0539720978aa0e84e7ffc7dea96d37e6d99c6adca2ced0851a44 ./ha-addon/repository/gree-controller/CHANGELOG.md
2976822c9663e505d22297e2749192b4ca119bb4e46ccc1c29c813522c84b3c5 ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md
40d18f2415fb84fe83d79ec19a9c9ca20232d751e02e5713786bac4d38f715a1 ./ha-addon/repository/gree-controller/config.yaml
5f0bd0d1ea4aca47844f9e870e4ba1676ad8b90369577db75a1bb0d00228a9da ./ha-addon/repository/gree-controller/README.md
f43723e7e356c7cc637b36b5ade0e34cfb69c50c4cf653cca7fc013e926cd7c9 ./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
@@ -84,7 +84,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
9083c4a8c602698a53d926864f2fe7e8c9658379f3d82fcb4171f44ec946a7ad ./scripts/api_dev_test.py
3a3287a4ca516f91a1b1bf14257aeef7b1b3027d592200c0a6da1cc70afa9c32 ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -106,7 +106,7 @@ b53e1afab342634fedc218ea4517446f19a5ba7985dee599e191b37d9cc8058a ./src/api.rs
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs
ca5bc21e051222dcaf0c507e84cb3e60f52a0b612a6056847fa0aee3a4d1a672 ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs
@@ -128,7 +128,7 @@ a8fe3636ac79df191e2f2c9d8c1eec1e5181e048ef8c9036cfa18f84aef1a462 ./src/db/confi
4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs
7bc79d9d33726fbdd33b485c8c2e839cedbb8fe29fee872611da978ed149d6a0 ./src/db/energy_history.rs
96aac6510ddda8c18fd7e29802a937b96e8a74badf504980846c80cd7933a4c6 ./src/db/events_tokens.rs
bb5048ee60053f39ce3c1203dc1f24c7047310538d2357f105a052c016d21ec4 ./src/db/flows.rs
bba6e3fb9f4800ae06d0ca67772b06a8c389d308243306719442d0c1f2623420 ./src/db/flows.rs
5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs
4fc007c08304e96818ad1d78997a5b2458e31aa9c48ad68b5be2a007533924bb ./src/db/network_history.rs
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
@@ -198,7 +198,7 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
0b783d6cea599e53fed4ed82fa95e2048c493057c6d57b0dfa423e7cfa10a7ad ./web/css/styles.css
1043dbcfb24a474ce088acb6aa86479e84db74688fb4802febeb3f3f564ca65f ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
cc1509fd76f0b0bd4c069db63e1e104210c2bed05f32a3eebac7642f94ef8a99 ./web/index.html
@@ -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
8e0f294ceeec1d258edb56271469a8a05381e6443a475f76fbeded6637d17503 ./web/js-dynamic/flows.js
879a92e95575ace4775b2f9a7f189e78ad0082ca5c12d012ef456e6477010886 ./web/js-dynamic/flows.js
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
+5 -5
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.15.7**.
HTTP and WebSocket API for GREE Controller **0.15.8**.
[← Main documentation](../README.md)
@@ -241,7 +241,7 @@ Response:
{
"status": "ok",
"name": "gree-controller",
"version": "0.15.7",
"version": "0.15.8",
"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.7",
"version": "0.15.8",
"uptime_seconds": 1234,
"auth_required": false,
"control_ready": true,
@@ -1711,9 +1711,9 @@ Cancellation suppresses the same pending intent until a new explicit thermostat/
### Flow portability and diagnostics
`GET /api/flows/:id/export` returns a versioned `gree-controller-flow` JSON document containing only the source graph. Generated schedules/automations are not exported.
`GET /api/flows/:id/export` returns a versioned `gree-controller-flow` JSON document containing the source graph plus definitions of every referenced Shared Input. Generated schedules/automations are not exported. The existing envelope may include an optional top-level `shared_inputs` array while leaving the `flow` object unchanged.
`POST /api/flows/import` accepts either that envelope or a direct Flow source payload, creates a new Flow ID/revision, and preserves draft state. Executable imports are validated and recompiled; draft imports remain disabled with no generated outputs.
`POST /api/flows/import` accepts either that envelope or a direct Flow source payload, creates a new Flow ID/revision, and preserves draft state. Executable imports are validated and recompiled; draft imports remain disabled with no generated outputs. When the envelope contains `shared_inputs`, the importer creates missing Shared Inputs automatically. If an imported Shared Input ID collides with a different local definition, an equivalent local source is reused when available; otherwise a new `shared-<uuid>` ID is created and the imported Flow node is remapped. The Flow and newly created Shared Inputs are saved in one database transaction.
`POST /api/flows/simulate` accepts `{ flow, flow_id?, at?, overrides?, log? }`. `at` is RFC3339. `overrides` maps Flow node IDs to simulated values. The endpoint validates and compiles the graph, evaluates every action and returns per-node traces plus `matched`, `would_execute` and `blocked_reason` (for example disabled zone/device, manual/local/temporary thermostat ownership, thermostat-output conflict or disabled Flow). It never mutates thermostat/device/group/schedule/automation state. Home Assistant read/attribute/parse failures evaluate safely as false and are visible in the trace instead of accidentally satisfying `NOT`/`neq` logic.
+2 -2
View File
@@ -54,7 +54,7 @@ The Flow editor exposes:
- per-node trace,
- `would_execute` and ownership/block reason,
- Flow-scoped execution/dry-run logs,
- import/export of versioned `.flow.json` source graphs.
- import/export of versioned `.flow.json` source graphs. Flow exports include the definitions of referenced Shared Inputs; import creates missing inputs automatically and remaps ID collisions without overwriting a different local input.
Dry-run never mutates thermostats, devices, groups, schedules or automations. For an existing Flow it clones persisted stateful runtime data, so timers, last-observed values, rate-limit history and rolling/oscillation windows are evaluated against the same saved state without writing it back.
@@ -68,7 +68,7 @@ Actions include thermostat zone, full GREE device command, single GREE function,
The editor ships 37 editable presets grouped into seven categories: Comfort, Energy & cost, Protection & safety, Night, Reliability, Home Assistant / heat sources and Advanced. The library UI also provides search, Favorites, Recent, a mini graph preview and requirement checks before applying a preset. The library includes workday/weekend comfort, weather-aware demand, morning boost, presence and energy-price eco modes, peak-power limiting, open-window/humidity/overheat/frost guards, night profiles, sensor/device resilience, dual heating/cooling thresholds, multi-room group control, nested AND/OR/NOT Home Assistant scenarios, and gas-boiler/external-heat-source coordination including off, reduced-target, boost and fallback heating patterns.
Shared Flow inputs show the Flows that reference them and link directly to those editors. Home Assistant-backed shared inputs can be tested live in the input modal. Shared inputs report the current raw value/attribute. Each Flow reference defines its own comparison without changing the shared source. Home Assistant temperature entities remain normal HA entities: room-temperature sensors are configured per zone, while outdoor temperature has one global `outdoor_entity_id` plus an optional per-zone `ha_outdoor_entity_id` override. Their aliases are shared with the rest of the UI, outdoor overrides are recorded in sensor metrics/history, and all configured entities are offered as suggestions in Home Assistant Flow blocks; selecting a zone sensor does not create a separate shared input.
Shared Flow inputs show the Flows that reference them and link directly to those editors. The Flow editor keeps the natural-language Interpretation / Flow cycle panel collapsed by default on every viewport and exposes it through a disclosure button. Home Assistant-backed shared inputs can be tested live in the input modal. Shared inputs report the current raw value/attribute. Each Flow reference defines its own comparison without changing the shared source. Home Assistant temperature entities remain normal HA entities: room-temperature sensors are configured per zone, while outdoor temperature has one global `outdoor_entity_id` plus an optional per-zone `ha_outdoor_entity_id` override. Their aliases are shared with the rest of the UI, outdoor overrides are recorded in sensor metrics/history, and all configured entities are offered as suggestions in Home Assistant Flow blocks; selecting a zone sensor does not create a separate shared input.
Presets are data files, not JavaScript definitions. Every preset lives in `presets/<id>.json` and contains `id`, `category`, localized `name`, localized `description`, and the complete `flow` graph (`nodes` + `edges`). `build.rs` validates and embeds the directory, exposes `/presets/index.json` plus `/presets/<file>.json`, and the Flow editor loads that library dynamically. Device/zone/group placeholders such as `$zone1`, `$zone2`, `$device1` and `$group1` are resolved when a preset is applied. Adding a preset therefore does not require adding graph-building code to `web/js-dynamic/flows.js`.
+39 -4
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "GREE Controller API",
"version": "0.15.7",
"version": "0.15.8",
"summary": "Local HTTP/WebSocket API for GREE Controller",
"description": "Local API used by the GREE Controller Web UI and Home Assistant integration.",
"license": {
@@ -2635,7 +2635,7 @@
"Flows"
],
"summary": "Export Flow source",
"description": "Exports a versioned `gree-controller-flow` document containing only the source graph, never generated schedules/automations.",
"description": "Exports a versioned `gree-controller-flow` document containing the source graph plus definitions of referenced Shared Inputs, never generated schedules/automations.",
"operationId": "exportFlow",
"parameters": [
{
@@ -2686,7 +2686,7 @@
"Flows"
],
"summary": "Import Flow source",
"description": "Imports either a `gree-controller-flow` envelope or a direct Flow source payload. A new Flow ID/revision is created; executable imports are validated and recompiled, drafts remain disabled.",
"description": "Imports either a `gree-controller-flow` envelope or a direct Flow source payload. Flow envelopes may include Shared Input definitions; missing inputs are created automatically and conflicting IDs are safely remapped. A new Flow ID/revision is created; executable imports are validated and recompiled, drafts remain disabled.",
"operationId": "importFlow",
"requestBody": {
"required": true,
@@ -5967,7 +5967,7 @@
},
"version": {
"type": "string",
"example": "0.15.7"
"example": "0.15.8"
},
"uptime_seconds": {
"type": "integer",
@@ -8026,6 +8026,41 @@
"type": "string",
"format": "date-time"
},
"shared_inputs": {
"type": "array",
"description": "Portable definitions for Shared Inputs referenced by the Flow. Import creates missing inputs and safely remaps ID collisions.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "shared-7bdc11d1-2d98-48c2-ba9f-ed2f13cd867b"
},
"name": {
"type": "string",
"example": "Room humidity"
},
"kind": {
"type": "string",
"example": "ha_numeric"
},
"config": {
"type": "object",
"additionalProperties": true,
"example": {
"entity_id": "sensor.room_humidity"
}
}
},
"required": [
"id",
"name",
"kind",
"config"
],
"additionalProperties": false
}
},
"flow": {
"$ref": "#/components/schemas/FlowInput"
}
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.15.7",
"version": "0.15.8",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
@@ -1,5 +1,12 @@
# Changelog
## 0.15.8
- Visual Flow exports now include the Shared Input definitions referenced by the graph.
- Flow import creates missing Shared Inputs automatically, reuses equivalent local sources and safely remaps conflicting IDs without overwriting local definitions; Flow and imported inputs are persisted atomically without changing the Flow format version.
- Flow export/import remains on format version 1; exported graphs may carry the Shared Input definitions they reference.
- The Flow editor keeps the Interpretation / Flow cycle section collapsed by default on desktop, tablet and mobile, with details available through the existing disclosure button.
## 0.15.7
- Fixes mixed V1/V2 Local discovery: legacy/plain discovery envelopes are no longer treated as proof that a unit is V1.
@@ -2,6 +2,8 @@
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.8 makes Visual Flow exports portable with referenced Shared Inputs and keeps the Interpretation / Flow cycle details collapsed until opened.
![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)
For VLAN deployments, configure VLAN interfaces on the Home Assistant OS host. The add-on uses `host_network: true`; with multiple directly attached subnets, `gree_interface` may remain empty. Run broadcast discovery separately for each VLAN/subnet.
@@ -1,5 +1,5 @@
name: "GREE Controller"
version: "0.15.7"
version: "0.15.8"
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/"
+10 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Developer smoke/integration tests for GREE Controller API 0.15.7.
"""Developer smoke/integration tests for GREE Controller API 0.15.8.
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.7"
DEFAULT_EXPECTED_VERSION = "0.15.8"
SETTINGS_PATHS: dict[str, set[str]] = {
"/api/settings/application": {"simulator_enabled"},
@@ -534,7 +534,13 @@ def test_flow_read_subresources(client: ApiClient) -> str:
flow_id = urllib.parse.quote(item["id"], safe="")
export_resp = client.get(f"/api/flows/{flow_id}/export")
assert_status(export_resp, 200)
export_resp.json()
exported = assert_json_object(export_resp)
if exported.get("format") != "gree-controller-flow":
raise AssertionError(f"unexpected Flow export format: {exported.get('format')!r}")
if exported.get("version") != 1:
raise AssertionError(f"unexpected Flow export version: {exported.get('version')!r}")
if "shared_inputs" in exported and not isinstance(exported["shared_inputs"], list):
raise AssertionError("Flow export shared_inputs must be an array")
logs_resp = client.get(f"/api/flows/{flow_id}/logs")
assert_status(logs_resp, 200)
logs_resp.json()
@@ -593,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.7 contract", lambda: test_openapi(client, args.expected_version))
runner.run("OpenAPI 0.15.8 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))
+200 -28
View File
@@ -53,6 +53,109 @@ fn flow_u8(config: &Value, key: &str) -> Option<u8> {
fn flow_bool(config: &Value, key: &str) -> Option<bool> {
config.get(key).and_then(Value::as_bool)
}
fn flow_referenced_shared_input_ids(
nodes: &[crate::models::FlowNode],
) -> std::collections::HashSet<String> {
nodes
.iter()
.filter(|node| node.kind == "shared_input")
.filter_map(|node| flow_string(&node.config, "input_id"))
.collect()
}
fn same_shared_input_source(
left: &crate::models::FlowSharedInput,
right: &crate::models::FlowSharedInput,
) -> bool {
left.kind == right.kind && left.config == right.config
}
fn prepare_imported_shared_inputs(
input: &mut FlowInput,
imported: Vec<crate::models::FlowSharedInput>,
current: &[crate::models::FlowSharedInput],
) -> Result<(Vec<crate::models::FlowSharedInput>, bool), AppError> {
let mut imported_ids = std::collections::HashSet::new();
let mut normalized_imported = Vec::with_capacity(imported.len());
for mut item in imported {
item.id = item.id.trim().chars().take(120).collect();
item.name = item.name.trim().chars().take(100).collect();
item.kind = item.kind.trim().to_string();
if item.id.is_empty() || item.name.is_empty() {
return Err(AppError::BadRequest(
"imported shared Flow input requires id and name".into(),
));
}
if !imported_ids.insert(item.id.clone()) {
return Err(AppError::BadRequest(
"imported shared Flow input IDs must be unique".into(),
));
}
normalized_imported.push(item);
}
let referenced_import_ids = flow_referenced_shared_input_ids(&input.nodes);
let mut next = current.to_vec();
let mut remap = std::collections::HashMap::<String, String>::new();
let mut changed = false;
for item in normalized_imported
.into_iter()
.filter(|item| referenced_import_ids.contains(&item.id))
{
if let Some(existing) = next.iter().find(|existing| existing.id == item.id) {
if same_shared_input_source(existing, &item) {
remap.insert(item.id.clone(), existing.id.clone());
continue;
}
}
if let Some(existing) = next
.iter()
.find(|existing| same_shared_input_source(existing, &item))
{
remap.insert(item.id.clone(), existing.id.clone());
continue;
}
let original_id = item.id.clone();
let mut to_add = item;
if next.iter().any(|existing| existing.id == to_add.id) {
to_add.id = format!("shared-{}", Uuid::new_v4());
}
remap.insert(original_id, to_add.id.clone());
next.push(to_add);
changed = true;
}
for node in &mut input.nodes {
if node.kind != "shared_input" {
continue;
}
let Some(input_id) = flow_string(&node.config, "input_id") else {
continue;
};
if let Some(mapped) = remap.get(&input_id) {
if mapped != &input_id {
let config = node.config.as_object_mut().ok_or_else(|| {
AppError::BadRequest("shared Flow input block config must be an object".into())
})?;
config.insert("input_id".into(), Value::String(mapped.clone()));
}
}
}
for input_id in flow_referenced_shared_input_ids(&input.nodes) {
if !next.iter().any(|item| item.id == input_id) {
return Err(AppError::BadRequest(format!(
"shared Flow input block references missing input {input_id}; export the Flow again with shared input definitions or create the input first"
)));
}
}
Ok((next, changed))
}
fn flow_louver_position(config: &Value, key: &str) -> Option<u8> {
match config.get(key) {
Some(Value::Bool(value)) => Some(u8::from(*value)),
@@ -604,6 +707,7 @@ fn validate_text_comparison(config: &Value) -> Result<(), AppError> {
fn validate_condition(
condition: &crate::models::FlowCondition,
state: &AppState,
shared_inputs_override: Option<&[crate::models::FlowSharedInput]>,
) -> Result<(), AppError> {
match condition.kind.as_str() {
"weekday" => {
@@ -1032,20 +1136,25 @@ fn validate_condition(
let input_id = flow_string(&condition.config, "input_id").ok_or_else(|| {
AppError::BadRequest("shared Flow input block needs input_id".into())
})?;
let settings = state
.db
.load_runtime_settings()?
.ok_or_else(|| AppError::BadRequest("runtime settings are unavailable".into()))?;
let shared = settings
.home_assistant
.flow_inputs
.iter()
.find(|item| item.id == input_id)
.ok_or_else(|| {
AppError::BadRequest(
"shared Flow input block references a missing input".into(),
)
})?;
let shared = if let Some(inputs) = shared_inputs_override {
inputs.iter().find(|item| item.id == input_id).cloned()
} else {
state
.db
.load_runtime_settings()?
.and_then(|settings| {
settings
.home_assistant
.flow_inputs
.into_iter()
.find(|item| item.id == input_id)
})
}
.ok_or_else(|| {
AppError::BadRequest(format!(
"shared Flow input block references missing input {input_id}"
))
})?;
validate_shared_input_source(&shared.kind, &shared.config, state)?;
if shared_input_comparison_kind(&shared.kind) {
let operator = flow_string(&condition.config, "operator").ok_or_else(|| {
@@ -1072,7 +1181,7 @@ fn validate_condition(
config,
inputs: Vec::new(),
};
validate_condition(&resolved, state)?;
validate_condition(&resolved, state, shared_inputs_override)?;
} else if flow_string(&condition.config, "operator").is_some()
|| condition.config.get("value").is_some()
{
@@ -1104,6 +1213,7 @@ fn validate_flow_comparison(config: &Value) -> Result<(), AppError> {
fn compile_flow(
state: &AppState,
mut flow: crate::models::Flow,
shared_inputs_override: Option<&[crate::models::FlowSharedInput]>,
) -> Result<(crate::models::Flow, Vec<Schedule>, Vec<Automation>), AppError> {
let mut schedules = Vec::new();
let mut automations = Vec::new();
@@ -1137,7 +1247,7 @@ fn compile_flow(
.iter()
.filter(|condition| flow_condition_kind(&condition.kind))
{
validate_condition(condition, state)?;
validate_condition(condition, state, shared_inputs_override)?;
}
let schedule_leaves: Vec<_> = conditions
.iter()
@@ -1572,7 +1682,7 @@ async fn create_flow(
let (flow, schedules, automations) = if flow.draft {
(prepare_draft_flow(flow), vec![], vec![])
} else {
compile_flow(&state, flow)?
compile_flow(&state, flow, None)?
};
state
.db
@@ -1629,7 +1739,7 @@ async fn update_flow(
let (flow, schedules, automations) = if flow.draft {
(prepare_draft_flow(flow), vec![], vec![])
} else {
compile_flow(&state, flow)?
compile_flow(&state, flow, None)?
};
state
.db
@@ -1689,10 +1799,22 @@ async fn export_flow(
.db
.get_flow(&id)?
.ok_or_else(|| AppError::NotFound(format!("flow {id}")))?;
let referenced = flow_referenced_shared_input_ids(&flow.nodes);
let shared_inputs: Vec<_> = state
.settings
.read()
.await
.home_assistant
.flow_inputs
.iter()
.filter(|item| referenced.contains(&item.id))
.cloned()
.collect();
Ok(Json(json!({
"format": "gree-controller-flow",
"version": 1,
"exported_at": Utc::now(),
"shared_inputs": shared_inputs,
"flow": {
"name": flow.name,
"enabled": flow.enabled,
@@ -1715,28 +1837,74 @@ async fn import_flow(
));
}
}
if let Some(version) = document.get("version").and_then(Value::as_u64) {
if !matches!(version, 1 | 2) {
return Err(AppError::BadRequest(format!(
"unsupported Flow import version {version}"
)));
}
}
let imported_shared_inputs: Vec<crate::models::FlowSharedInput> = document
.get("shared_inputs")
.cloned()
.map(serde_json::from_value)
.transpose()
.map_err(|err| AppError::BadRequest(format!("invalid shared Flow inputs: {err}")))?
.unwrap_or_default();
let payload = document.get("flow").cloned().unwrap_or(document);
let mut input: FlowInput = serde_json::from_value(payload)
.map_err(|err| AppError::BadRequest(format!("invalid Flow import: {err}")))?;
input.expected_revision = None;
let _configuration_guard = state.lock_configuration_operation().await;
let _automation_guard = state.lock_automation_operation().await;
let _schedule_guard = state.lock_schedule_operation().await;
let _cycle_guard = state.lock_zone_control_cycle().await;
let mut next_settings = state.settings.read().await.clone();
let (flow_inputs, shared_inputs_changed) = prepare_imported_shared_inputs(
&mut input,
imported_shared_inputs,
&next_settings.home_assistant.flow_inputs,
)?;
next_settings.home_assistant.flow_inputs = flow_inputs;
validate_flow_shared_inputs(&mut next_settings.home_assistant, &state)?;
if input.draft {
validate_flow_draft_graph(&input)?;
} else {
validate_flow_graph(&input)?;
}
let _configuration_guard = state.lock_configuration_operation().await;
let _automation_guard = state.lock_automation_operation().await;
let _schedule_guard = state.lock_schedule_operation().await;
let _cycle_guard = state.lock_zone_control_cycle().await;
let flow = flow_from_input(Uuid::new_v4().to_string(), input, Utc::now(), 1);
let (flow, schedules, automations) = if flow.draft {
(prepare_draft_flow(flow), vec![], vec![])
} else {
compile_flow(&state, flow)?
compile_flow(
&state,
flow,
Some(&next_settings.home_assistant.flow_inputs),
)?
};
state
.db
.replace_flow_outputs(&flow, &schedules, &automations)?;
if shared_inputs_changed {
state.db.replace_flow_outputs_with_runtime_settings(
&flow,
&schedules,
&automations,
&next_settings,
)?;
*state.settings.write().await = next_settings.clone();
let payload = home_assistant_settings(&next_settings);
state.broadcast(
"settings.home_assistant.updated",
serde_json::to_value(&payload)?,
);
} else {
state
.db
.replace_flow_outputs(&flow, &schedules, &automations)?;
}
for zone_id in schedules
.iter()
.map(|s| s.zone_id.as_str())
@@ -1748,7 +1916,11 @@ async fn import_flow(
"info",
"flow.imported",
&format!("Imported Flow {}", flow.name),
json!({"flow_id": flow.id, "revision": flow.revision}),
json!({
"flow_id": flow.id,
"revision": flow.revision,
"shared_inputs_imported": shared_inputs_changed
}),
);
state.broadcast("flow.created", serde_json::to_value(&flow)?);
state.wake_zone_control();
@@ -1896,7 +2068,7 @@ async fn simulate_flow(
created_at: Utc::now(),
updated_at: Utc::now(),
};
let (compiled, schedules, automations) = compile_flow(&state, preview)?;
let (compiled, schedules, automations) = compile_flow(&state, preview, None)?;
let devices = state.db.list_devices()?;
let mut actions = Vec::new();
for action in input
+44
View File
@@ -45,6 +45,50 @@ impl Db {
Ok(())
}
pub fn replace_flow_outputs_with_runtime_settings(
&self,
flow: &Flow,
schedules: &[Schedule],
automations: &[Automation],
settings: &RuntimeSettings,
) -> Result<()> {
let mut conn = self.lock()?;
let tx = conn.transaction()?;
tx.execute(queries::DELETE_SCHEDULES_BY_FLOW_ID, [flow.id.as_str()])?;
tx.execute(queries::DELETE_AUTOMATIONS_BY_FLOW_ID, [flow.id.as_str()])?;
for schedule in schedules {
let payload = Self::to_json(schedule)?;
tx.execute(
queries::UPSERT_SCHEDULE,
params![
schedule.id,
schedule.zone_id,
payload,
schedule.updated_at.to_rfc3339()
],
)?;
}
for item in automations {
let payload = Self::to_json(item)?;
tx.execute(
queries::UPSERT_AUTOMATION,
params![item.id, payload, item.updated_at.to_rfc3339()],
)?;
}
let flow_payload = Self::to_json(flow)?;
tx.execute(
queries::UPSERT_FLOW,
params![flow.id, flow_payload, flow.updated_at.to_rfc3339()],
)?;
let settings_payload = Self::to_json(settings)?;
tx.execute(
queries::UPSERT_RUNTIME_SETTINGS,
params![settings_payload, Utc::now().to_rfc3339()],
)?;
tx.commit()?;
Ok(())
}
pub fn delete_flow(&self, id: &str) -> Result<bool> {
let mut conn = self.lock()?;
let tx = conn.transaction()?;
+65 -1
View File
@@ -10342,4 +10342,68 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr
.public-custom-chart-wrap {
min-height: 140px;
}
}
}
/* Keep Flow interpretation compact on every viewport. Details are opt-in. */
.flow-natural-preview {
display: block;
min-height: 0;
max-height: none;
padding: 0;
overflow: hidden;
}
.flow-preview-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 40px;
padding: 8px 14px;
border: 0;
border-radius: 0;
background: var(--surface);
color: var(--text);
text-align: left;
cursor: pointer;
}
.flow-preview-toggle strong {
font-size: .8rem;
}
.flow-preview-toggle-icon {
color: var(--muted);
font-size: 1rem;
transition: transform .15s ease;
}
.flow-preview-details {
display: none;
flex-direction: column;
align-items: stretch;
gap: 8px;
max-height: 34dvh;
padding: 9px 14px 11px;
overflow: auto;
}
.flow-natural-preview.is-expanded .flow-preview-details {
display: flex;
}
.flow-natural-preview.is-expanded .flow-preview-toggle {
border-bottom: 1px solid var(--line);
}
.flow-natural-preview.is-expanded .flow-preview-toggle-icon {
transform: rotate(180deg);
}
.flow-natural-preview .flow-preview-runtime {
justify-content: start;
text-align: left;
}
.flow-natural-preview .flow-preview-runtime span {
white-space: normal;
}
+12 -1
View File
@@ -731,6 +731,17 @@ function flowSourcePayload() {
};
}
function flowExportSharedInputs() {
if (!app.flowDraft) return [];
const ids = new Set((app.flowDraft.nodes || [])
.filter(node => node.kind === 'shared_input')
.map(node => node.config?.input_id)
.filter(Boolean));
return (app.flowSharedInputs || [])
.filter(item => ids.has(item.id))
.map(item => JSON.parse(JSON.stringify(item)));
}
function downloadJsonFile(filename, data) {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob); const a = document.createElement('a');
@@ -743,7 +754,7 @@ async function exportFlow() {
try {
const envelope = app.flowDraft.id && !app.flowDirty
? await api(`/api/flows/${encodeURIComponent(app.flowDraft.id)}/export`)
: { format: 'gree-controller-flow', version: 1, exported_at: new Date().toISOString(), flow: flowSourcePayload() };
: { format: 'gree-controller-flow', version: 1, exported_at: new Date().toISOString(), shared_inputs: flowExportSharedInputs(), flow: flowSourcePayload() };
const safe = (flowSourcePayload()?.name || 'flow').replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'flow';
downloadJsonFile(`${safe}.flow.json`, envelope); toast(tr('flow.exported'));
} catch (error) { toast(error.message, true); }