diff --git a/Cargo.lock b/Cargo.lock index 9bb284e..d5ed69e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -719,7 +719,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.15.7" +version = "0.15.8" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 8b7b4f1..a45e964 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index 18af4e9..3092a84 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -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 diff --git a/README.md b/README.md index f123a91..609f1c3 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API. -**Version: 0.15.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. diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256 index 8b738f4..a7bd486 100644 --- a/SOURCE_MANIFEST.sha256 +++ b/SOURCE_MANIFEST.sha256 @@ -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 diff --git a/docs/API.md b/docs/API.md index 89732ee..1c6905b 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # GREE Controller API reference -HTTP and WebSocket API for GREE Controller **0.15.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-` 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. diff --git a/docs/FLOW.md b/docs/FLOW.md index a800ea6..f9b0941 100644 --- a/docs/FLOW.md +++ b/docs/FLOW.md @@ -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/.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/.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`. diff --git a/docs/openapi.json b/docs/openapi.json index 79617c3..df7c4a7 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "GREE Controller API", - "version": "0.15.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" } diff --git a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json index be3fca1..2deec23 100644 --- a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json +++ b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "gree_controller", "name": "GREE Controller", - "version": "0.15.7", + "version": "0.15.8", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/ha-addon/repository/gree-controller/CHANGELOG.md b/ha-addon/repository/gree-controller/CHANGELOG.md index 8ba1998..e3c99e5 100644 --- a/ha-addon/repository/gree-controller/CHANGELOG.md +++ b/ha-addon/repository/gree-controller/CHANGELOG.md @@ -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. diff --git a/ha-addon/repository/gree-controller/README.md b/ha-addon/repository/gree-controller/README.md index 80a1425..fbd5548 100644 --- a/ha-addon/repository/gree-controller/README.md +++ b/ha-addon/repository/gree-controller/README.md @@ -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. diff --git a/ha-addon/repository/gree-controller/config.yaml b/ha-addon/repository/gree-controller/config.yaml index 400c777..c0fe536 100644 --- a/ha-addon/repository/gree-controller/config.yaml +++ b/ha-addon/repository/gree-controller/config.yaml @@ -1,5 +1,5 @@ name: "GREE Controller" -version: "0.15.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/" diff --git a/scripts/api_dev_test.py b/scripts/api_dev_test.py index fe9e9b7..9967f27 100644 --- a/scripts/api_dev_test.py +++ b/scripts/api_dev_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Developer smoke/integration tests for GREE Controller API 0.15.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)) diff --git a/src/api/flows.rs b/src/api/flows.rs index 172d339..6593de3 100644 --- a/src/api/flows.rs +++ b/src/api/flows.rs @@ -53,6 +53,109 @@ fn flow_u8(config: &Value, key: &str) -> Option { fn flow_bool(config: &Value, key: &str) -> Option { config.get(key).and_then(Value::as_bool) } + +fn flow_referenced_shared_input_ids( + nodes: &[crate::models::FlowNode], +) -> std::collections::HashSet { + 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, + current: &[crate::models::FlowSharedInput], +) -> Result<(Vec, 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::::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 { 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, Vec), 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 = 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 diff --git a/src/db/flows.rs b/src/db/flows.rs index b7471d1..1122227 100644 --- a/src/db/flows.rs +++ b/src/db/flows.rs @@ -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 { let mut conn = self.lock()?; let tx = conn.transaction()?; diff --git a/web/css/styles.css b/web/css/styles.css index 5026dd4..84eaef3 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -10342,4 +10342,68 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr .public-custom-chart-wrap { min-height: 140px; } -} \ No newline at end of file +} +/* 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; +} diff --git a/web/js-dynamic/flows.js b/web/js-dynamic/flows.js index 43be3b3..0075500 100644 --- a/web/js-dynamic/flows.js +++ b/web/js-dynamic/flows.js @@ -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); }