v0.10.0
This commit is contained in:
Generated
+1
-1
@@ -633,7 +633,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+20
-20
@@ -1,12 +1,12 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
18765f4d1c77c7c44ad4438aa3d31e90030577f5395190d0ec1450c0fadd5987 ./Cargo.lock
|
||||
d3e13a2ef72326abae6de8eb727d858d191bb2eeb1a1680e2c028c2b560f13ea ./Cargo.toml
|
||||
bf9a8e40ddb34e4ad07c8af2fb2701d80694ae735b2cbca1a9d227a3f1450ca1 ./Cargo.lock
|
||||
7142adffb9b6a110ea8aa6337aeb7b43b52a2a35024ef3bc9e9ff3c3b0437590 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
6955218013ff72cc2b92b930e664f0215d067c0ea88f928df09d244b69958ba9 ./README.md
|
||||
77904d7a2d5bf7206bd1d350f2a15e827b0f821e31da511a4afdd0ba403542c9 ./README.md
|
||||
41dfdc6d099b54f87d4dd51f696122b3c88d3bf3ddf420bea8f4bc621ba920b0 ./build.rs
|
||||
b232adc48c1737bd2a7d6ee47c97ff17750243027f6737ae8209f9111c8e819c ./docs/API.md
|
||||
5322117989c854763d87a4ea6ec5e957863d370ca4ebdc332e2c8c11f23338d1 ./docs/FLOW.md
|
||||
f2d6d4177a3628e0939ee691a3071c876e95c56d564fcd6e34aedbc3f25d1b0c ./docs/API.md
|
||||
bdbb1bf4f46fe4e4e99ef3a484a091442c082f57304880b4a1535da167a1d830 ./docs/FLOW.md
|
||||
a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./home-assistant/README.md
|
||||
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
|
||||
6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py
|
||||
@@ -17,7 +17,7 @@ ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assista
|
||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
|
||||
3593dc4b937182dcf7d372c8710f079d99f25304f6a17ab7d5b84508127047ce ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
ac0edf99b99e4764a6feed506795c8fb709d3ae73cf054e2a68bf7c8685ac115 ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
|
||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
|
||||
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./home-assistant/custom_components/gree_controller/sensor.py
|
||||
@@ -26,8 +26,8 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./home-assista
|
||||
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
|
||||
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
608a846712e3bccfade1b799843096ea9dfcbe6628804ac1a23e9ccc2cfec794 ./lang/en.json
|
||||
fece587ab929200753697419de702f3abf4661a419acfac9c89d6143b761c7fd ./lang/pl.json
|
||||
8c92d35110a2610a9693c45063b2996370ad9cd63db2c667f027fcfff23dcd5e ./lang/en.json
|
||||
b4e288e03408570f51a5bda2e9f5eaa3f64be3c134f805fc0b965a1eeda08f68 ./lang/pl.json
|
||||
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
||||
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
|
||||
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
|
||||
@@ -67,7 +67,7 @@ ae21459a261712bcb8d57594528b1648432e8d03a8a38502234829c0dbfef774 ./presets/week
|
||||
fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/window_available_guard.json
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
|
||||
6408d99fbb6154afa65a9bda016dab9c9236e57cf4ae4ccb89943c1a4c5a5a56 ./scripts/FILE_MANIFEST.sha256
|
||||
feb34a6d60da49a5135eae8de5028988cca1ea1dd0971c76aec848f965fdea67 ./scripts/FILE_MANIFEST.sha256
|
||||
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
@@ -78,7 +78,7 @@ bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./scripts/inst
|
||||
e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
8ccca8ed49576cf0b8e3453436ac3ff9c2a222cd41b27db98ceb62bd5f87aeef ./scripts/smoke.sh
|
||||
fec8b0362e763bbc115b5cc5c56f9ebe815730cee161632d81c5dc0ee1475f80 ./scripts/smoke.sh
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
da16e16dfb330d528d54d2138b07950da701edc46af1ff174a9f6a54090122b3 ./src/api.rs
|
||||
825370f409124a719043200742c01c3441bc65f483108106140d13fe7bbcba91 ./src/api/assets.rs
|
||||
@@ -87,7 +87,7 @@ da16e16dfb330d528d54d2138b07950da701edc46af1ff174a9f6a54090122b3 ./src/api.rs
|
||||
2317b7cd0c2437c128ac4982d64412c8184ecb5e774825761d61aab6e45fae4a ./src/api/debug_tokens.rs
|
||||
4f82206fb48a291bdc643c96d4d6c917d669c7bdf2a35312c0a056dddecc7062 ./src/api/devices.rs
|
||||
417940b467c6f00bbf01cc47eb4479880dee9b2f284a80e3662a6d433dc11b92 ./src/api/events.rs
|
||||
787fdee57945c8542b692c849160a3447a8089b03e322f1cc137aa50305e14aa ./src/api/flows.rs
|
||||
10dcc9330f4d0b1b88b4fd3e80dd85b67cb45c5205c3fe03bbadecf3003e9e99 ./src/api/flows.rs
|
||||
d096000e94997cf9aaf9d3cc567214a78b8b2da7da57a76e2b3519247af6e592 ./src/api/groups.rs
|
||||
a13d4e217fe3ddaa73873ba6e0d1bc93750cf61764d867ff7372274ed21d9599 ./src/api/history.rs
|
||||
b3ed82de9d885a6325647bff3d0ead6ac46fc74d0250d5e14d75319555ad88d7 ./src/api/house.rs
|
||||
@@ -95,7 +95,7 @@ b3ed82de9d885a6325647bff3d0ead6ac46fc74d0250d5e14d75319555ad88d7 ./src/api/hous
|
||||
bb2a746ecdcc2da5fe54e18b455c7bd19453486dd5c008e71951a2c81d0e7d64 ./src/api/middleware.rs
|
||||
50cfd47e44e22cc802f22f97267c157fa5a6d19b51be921a6ca6b1ea7c9799d1 ./src/api/public_settings.rs
|
||||
a1d1a4ac071493b052e6bf20b03be67cf75726de95649e9a73aeba8d22395a15 ./src/api/schedules.rs
|
||||
85d1843a830a079e9993dcc01329c0fea7e6cfe62f22b60617665e7300b405b0 ./src/api/settings.rs
|
||||
09ac8aae6e51f36379908e48b3b18d845d3c0b246277ed945c29e0c648197c4a ./src/api/settings.rs
|
||||
bc1614b9948b8904d0f678c79cc9fffecb75977fbec951aca1ba087527808836 ./src/api/system.rs
|
||||
681cccb8d09f4ad9c2e6467a4dbe2f9d991125775a5d2bfc9043daf940fecd29 ./src/api/websocket.rs
|
||||
923e7434e8f5789150eb34ffc4d18d25fa701f4a9040bf856d7926016f1936aa ./src/api/zones.rs
|
||||
@@ -112,7 +112,7 @@ f3bd91d0bdb7699f319b9341d928d3cc3b25f759653fcd2095a5c5f695d08fac ./src/db/event
|
||||
35cbb521a8ec456756496b8aee71f9dfebad528a2cf2653ba8b48f6035be0c01 ./src/db/tests.rs
|
||||
a384ea4042d1d11692b5f8693df1f54850dcc4e80726780b44b2b096bf667b78 ./src/db/zone_history.rs
|
||||
952ea5906d39ef917ad71edc345f76b29c07c63079644fd5699b12aa254cb35f ./src/engine.rs
|
||||
58a09f3980d6a054921b5e484fa3fd2327f8bd90a48ffa5751a60a80542ee47a ./src/engine/automations.rs
|
||||
db8a94f464e0e92f9b80ec4e9a1f2c46cabbf69e6540c3d3b2155f96ba6b55cb ./src/engine/automations.rs
|
||||
77931af524e9402fc826050b5fcec422d01b02b625c8ce7f2004f1f6520c9b4d ./src/engine/commands.rs
|
||||
ce7e809765c8ad7dc4819b7f775ba7a632031d4d0e68780cc6ea953f2fd890e7 ./src/engine/control_plan.rs
|
||||
38f42c86c31048780ca414f2c328f363ef803a0edbc1e1ed85503909a10b69ae ./src/engine/deadlines.rs
|
||||
@@ -143,7 +143,7 @@ a4f2d6d5a8d1ef58e45721e4640a63d7dd2e80b24accd22f32c74ce4a541ae21 ./src/models/a
|
||||
7cdc99a6f0920baf4ac88fbe7e6f1859e8598a00f00b8c3d84f20bc3a4de35f9 ./src/models/device.rs
|
||||
42f968d9a99ac154b0bd83e5baa498659b1378be4d47d2a05665be641a2fb938 ./src/models/flow.rs
|
||||
cfbaa853a5185c4be572588577b313a2d618837f7c07a3860d0e8d2cca04ee60 ./src/models/history.rs
|
||||
5d98d82af11090701d5e654a9c7733b995c571dbd0f7a1905d95ffb7b234b44f ./src/models/integrations.rs
|
||||
552dfc1717ea88f3a23b1fb7705f1dbb0df7792ca9a6ae7ad7bd275eacef27af ./src/models/integrations.rs
|
||||
268b4211df93f08423e39374ab9ffe93a7eba647c173168821736af299b36549 ./src/models/runtime.rs
|
||||
e77f8cdcb5fc0e51cdbf2527a69585a2f6f9089e159d1e45ec4a48735ab0e304 ./src/models/temporary_thermostat.rs
|
||||
5b5c80e25ae2aad1b511bd3f0a82b8fbae2440abf8c656ecb90814587d0be137 ./src/models/zone.rs
|
||||
@@ -171,24 +171,24 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
0c10450b9241a9b7a811fe8ad44b4413018e33fcd5b932f024a73e1ba6a3f946 ./web/index.html
|
||||
d6df434f3fbd33ba38dd244830c980e54433c6dff94dd39c716b377255bd2ebf ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
||||
21b662914590e720dafefe4dc3dccc91dbeb963cce9ae91f007c835d2d0c1fe5 ./web/js/bootstrap.js
|
||||
72fa280996ba331a05bdb29a1f49b4e61bdd8964953c8ce1db952156620f08fe ./web/js/charts.js
|
||||
7dd04683781137deedc5dfccafcb5a4d15954041f2e5207312816de216904513 ./web/js/core.js
|
||||
d34953aec21ade81ca9770c9c99873b0b2a4862a1bf671a6e13b507a5dd5b7ed ./web/js/dashboard.js
|
||||
dfc123885a9b6b5c1d07c12d2d2625ff15aa0844e5cd3736092ccd0a360da39c ./web/js/dashboard.js
|
||||
2887856e35e086bb575e9fc0d8b0ed1160b6912297a2957535d5416bda68b4b7 ./web/js/entities.js
|
||||
9a0e87911f78cd8a969f4c57ce74774f5fa829fdecd79494d9a7f9d5804f9792 ./web/js/events.js
|
||||
ee094f55b3f6d0c40fafaf422776ee87b17e0d83eb228ca6c197dfd3efd4c774 ./web/js/flows.js
|
||||
92674d277d12aa2bca0fbd7dffcd6e76f182a2601ae60dbdd33210e906a5fee2 ./web/js/flows.js
|
||||
f06557c6d338259059b990f8f79c91d68ac8ec1f245c302468f08929b5506901 ./web/js/forms.js
|
||||
dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js
|
||||
7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js
|
||||
7f23507b2f1f632d69b466688f68f3f0bf661dc6856a1ed14a462e063114a980 ./web/js/navigation.js
|
||||
c56059693d09e35261dc5dd1940289ffc67c82ea7c547750d57e6c79fe75b05b ./web/js/realtime.js
|
||||
c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/router.js
|
||||
3d44b5457200a71f66fbf7bde89c7b9adf416ca185ad0e3c61f2c4e551bceb1e ./web/js/settings-ui.js
|
||||
a06d718217dc6ffb6d4fb5b002fedac02ba7eea3a5b00066c2241df7c1957e51 ./web/js/settings.js
|
||||
34513d9ec2e45c60443dffe028eea74f415b8a29016df6af3377a80a4ac0df5b ./web/js/settings-ui.js
|
||||
aa6a2ba22648de44547efd09f41c4335ebe38ed564f4684e942d7827df40f1a2 ./web/js/settings.js
|
||||
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
|
||||
c8ca437e032fe81327f3bfe7457b5510f21bad21c021bfc83caf44527ce9633f ./web/styles.css
|
||||
cad5c59275ab306b0de88b76dcc7b667a3a83585bd9c64ba0221bde7744dd1a1 ./web/styles.css
|
||||
ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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.
|
||||
|
||||
**Current release: 0.9.14**
|
||||
**Current release: 0.10.0**
|
||||
|
||||
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
|
||||
|
||||
|
||||
+6
-4
@@ -1,6 +1,6 @@
|
||||
# GREE Controller API reference
|
||||
|
||||
HTTP and WebSocket API for GREE Controller **0.9.14**.
|
||||
HTTP and WebSocket API for GREE Controller **0.10.0**.
|
||||
|
||||
[← Main documentation](../README.md)
|
||||
|
||||
@@ -213,7 +213,7 @@ Response:
|
||||
{
|
||||
"status": "ok",
|
||||
"name": "gree-controller",
|
||||
"version": "0.9.14",
|
||||
"version": "0.10.0",
|
||||
"uptime_seconds": 1234,
|
||||
"control_ready": true,
|
||||
"time": "2026-08-30T06:54:00Z"
|
||||
@@ -237,7 +237,7 @@ Returns the initial Web UI snapshot:
|
||||
"settings": {},
|
||||
"outdoor_temperature": null,
|
||||
"system": {
|
||||
"version": "0.9.14",
|
||||
"version": "0.10.0",
|
||||
"uptime_seconds": 1234,
|
||||
"auth_required": false,
|
||||
"control_ready": true,
|
||||
@@ -1053,6 +1053,8 @@ Shape:
|
||||
}
|
||||
```
|
||||
|
||||
`default_entity_id` is primarily the entity used by the Home Assistant connection test when the request does not provide another entity. Thermostat zones use their own configured `ha_entity_id`; this setting does not automatically become a zone temperature source.
|
||||
|
||||
#### InfluxDB settings
|
||||
|
||||
```json
|
||||
@@ -1514,7 +1516,7 @@ Flow is the source-of-truth representation for visual schedule/automation logic.
|
||||
|
||||
Saving a Flow validates the DAG and compiles it atomically. A thermostat action driven only by one weekday block and one time-range block is emitted as a native schedule when its settings are schedule-compatible. More complex graphs are emitted as Flow-triggered automations. Generated schedules and automations expose `flow_id` / `flow_node_id`, use stable names in the form `flow-<stable-unique-id>`, and cannot be edited or deleted through their legacy endpoints; edit the owning Flow instead.
|
||||
|
||||
Condition blocks support weekday, time/date ranges, application Night mode, outdoor/device/zone temperatures, house/device/zone/group state, arbitrary Home Assistant state/numeric/attribute/availability sources, and `shared_input` references. Shared inputs are stored in `home_assistant.flow_inputs` and resolve dynamically at evaluation time, allowing the same condition definition to be reused across multiple Flows. Logic blocks support AND, OR and NOT. Action blocks target thermostat zones, GREE devices or climate groups. Direct GREE actions map to the existing `DeviceCommand` fields including fan, swing, quiet, turbo, light, air, xfan, health and sleep; ownership rules may suppress fields that would fight an active thermostat. Flow group actions use the existing group controller and support power, house/heat/cool mode, auto/comfort/sleep/away and the existing custom group target.
|
||||
Condition blocks support weekday, time/date ranges, application Night mode, outdoor/device/zone temperatures, house/device/zone/group state, arbitrary Home Assistant state/numeric/attribute/availability sources, and `shared_input` references. Shared inputs are stored in `home_assistant.flow_inputs` and resolve dynamically at evaluation time. Shared inputs store reusable value sources only. They never store a comparison operator or threshold. For comparison-capable source kinds, each `shared_input` Flow node defines its own `operator` and `value`. `PUT /api/settings` rejects shared-input configs that contain an `operator`, and rejects comparison `value` fields for comparison-capable source kinds. Logic blocks support AND, OR and NOT. Action blocks target thermostat zones, GREE devices or climate groups. Direct GREE actions map to the existing `DeviceCommand` fields including fan, swing, quiet, turbo, light, air, xfan, health and sleep; ownership rules may suppress fields that would fight an active thermostat. Flow group actions use the existing group controller and support power, house/heat/cool mode, auto/comfort/sleep/away and the existing custom group target.
|
||||
|
||||
## Compressor protection queue
|
||||
|
||||
|
||||
+2
-2
@@ -57,7 +57,7 @@ Dry-run never mutates thermostats, devices, groups, schedules or automations.
|
||||
|
||||
## Blocks
|
||||
|
||||
Condition/source blocks currently include weekday, time range, date range, application Night mode, outdoor/device/zone temperature, house mode, device state, thermostat/zone state, group state, Home Assistant state/numeric/attribute/availability, a diagnostic constant, and `shared_input`. Shared inputs are configured centrally in Home Assistant / Sensors and referenced by ID, so one source definition can be reused by multiple Flows. Logic blocks are AND, OR and NOT.
|
||||
Condition/source blocks currently include weekday, time range, date range, application Night mode, outdoor/device/zone temperature, house mode, device state, thermostat/zone state, group state, Home Assistant state/numeric/attribute/availability, a diagnostic constant, and `shared_input`. Shared inputs are configured centrally in Home Assistant / Sensors and referenced by ID. Shared inputs store reusable value sources only; operators and comparison values are configured exclusively in each Flow reference. Logic blocks are AND, OR and NOT.
|
||||
|
||||
Actions include thermostat zone, GREE device and climate group. Direct GREE actions expose power, HVAC mode, target temperature, fan speed, vertical/horizontal swing, quiet, turbo, light, air, xfan, health and sleep.
|
||||
|
||||
@@ -65,7 +65,7 @@ Actions include thermostat zone, GREE device and climate group. Direct GREE acti
|
||||
|
||||
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, including the current raw value/attribute and the resulting TRUE/FALSE condition.
|
||||
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.
|
||||
|
||||
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/flows.js`.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.9.14",
|
||||
"version": "0.10.0",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
+9
-7
@@ -148,7 +148,8 @@
|
||||
"settings.haTokenKeep": "Leave empty to keep the saved token",
|
||||
"settings.haTokenSaved": "Token saved — leave empty to keep it",
|
||||
"settings.haLongLivedToken": "Long-Lived Access Token",
|
||||
"settings.defaultEntity": "Default entity_id",
|
||||
"settings.defaultEntity": "Connection test entity_id",
|
||||
"settings.defaultEntityHint": "This entity is used by the Home Assistant connection test when no other entity is specified. It is not automatically used as a zone temperature source.",
|
||||
"settings.testHa": "Test HA",
|
||||
"settings.systemState": "System status",
|
||||
"settings.version": "Version",
|
||||
@@ -941,10 +942,12 @@
|
||||
"flow.sharedInputType": "Source type",
|
||||
"flow.sharedInputAdd": "Add input",
|
||||
"flow.sharedInputsTitle": "Shared Flow inputs",
|
||||
"flow.sharedInputsHint": "Define constants, states and sensors once, then reuse them in many Flows. Changes here apply to every Flow that references the input.",
|
||||
"flow.sharedInputsHint": "Define reusable value sources once, then use them in many Flows. Operators and comparison values are configured only in Flow blocks.",
|
||||
"flow.sharedInputsEmpty": "No shared inputs",
|
||||
"flow.sharedInputsEmptyHint": "Add a constant, Home Assistant state, or another shared source for multiple Flows.",
|
||||
"flow.sharedInputNodeHint": "This block uses the central shared-input configuration.",
|
||||
"flow.sharedInputNodeHint": "This block uses a central value source. The comparison below belongs only to this Flow.",
|
||||
"flow.sharedInputFlowComparisonHint": "The shared input provides only the value source. Define this Flow's comparison here.",
|
||||
"flow.selectOperator": "Select operator",
|
||||
"flow.sharedInputNodeEmptyHint": "Add a shared input in Home Assistant / Sensors first.",
|
||||
"flow.sharedInputNameRequired": "Enter a shared input name.",
|
||||
"flow.sharedInputDeleteConfirm": "Delete shared input “{name}”?",
|
||||
@@ -971,9 +974,7 @@
|
||||
"flow.sharedInputTestHint": "The test uses the saved Home Assistant connection and does not save this input.",
|
||||
"flow.sharedInputTestEntityRequired": "Enter entity_id before testing.",
|
||||
"flow.sharedInputTestCurrent": "Current value",
|
||||
"flow.sharedInputTestExpected": "Condition",
|
||||
"flow.sharedInputTestMatched": "TRUE — condition matched",
|
||||
"flow.sharedInputTestNotMatched": "FALSE — condition did not match",
|
||||
"flow.sharedInputTestValueRead": "Value read successfully",
|
||||
"flow.sharedInputTestAvailable": "Entity available",
|
||||
"flow.sharedInputTestUnavailable": "Entity unavailable",
|
||||
"flow.sharedInputUsedBy": "Used in {count} Flows",
|
||||
@@ -1042,6 +1043,7 @@
|
||||
"flow.logMessageBlockedThermostatOwner": "The Flow action was suppressed because the device is controlled by an enabled thermostat zone.",
|
||||
"flow.logMessageBlockedOwnership": "The Flow action was suppressed because control ownership changed during execution.",
|
||||
"flow.logMessageConflict": "The Flow action was skipped because another automation already claimed the same target.",
|
||||
"flow.nameAliasHint": "This is only the user-facing display name. The technical Flow ID and generated task names remain unchanged."
|
||||
"flow.nameAliasHint": "This is only the user-facing display name. The technical Flow ID and generated task names remain unchanged.",
|
||||
"flow.sharedInputSourceOnlyHint": "This shared input stores only the value source. Configure the comparison in the Flow block."
|
||||
}
|
||||
}
|
||||
|
||||
+9
-7
@@ -148,7 +148,8 @@
|
||||
"settings.haTokenKeep": "Pozostaw puste, aby zachować zapisany token",
|
||||
"settings.haTokenSaved": "Token zapisany — pozostaw puste",
|
||||
"settings.haLongLivedToken": "Długotrwały token dostępu",
|
||||
"settings.defaultEntity": "Domyślny entity_id",
|
||||
"settings.defaultEntity": "Entity_id do testu połączenia",
|
||||
"settings.defaultEntityHint": "Ta encja jest używana przez test połączenia z Home Assistant, gdy nie wskazano innej encji. Nie jest automatycznie źródłem temperatury strefy.",
|
||||
"settings.testHa": "Testuj HA",
|
||||
"settings.systemState": "Stan systemu",
|
||||
"settings.version": "Wersja",
|
||||
@@ -941,10 +942,12 @@
|
||||
"flow.sharedInputType": "Typ źródła",
|
||||
"flow.sharedInputAdd": "Dodaj wejście",
|
||||
"flow.sharedInputsTitle": "Wspólne wejścia Flow",
|
||||
"flow.sharedInputsHint": "Zdefiniuj stałe, stany i czujniki raz, a potem używaj ich w wielu Flow. Zmiana tutaj obowiązuje we wszystkich odwołujących się Flow.",
|
||||
"flow.sharedInputsHint": "Zdefiniuj wspólne źródła wartości raz i używaj ich w wielu Flow. Operatory i wartości porównań ustawia się wyłącznie w blokach Flow.",
|
||||
"flow.sharedInputsEmpty": "Brak wspólnych wejść",
|
||||
"flow.sharedInputsEmptyHint": "Dodaj stałą, stan Home Assistant albo inne współdzielone źródło dla wielu Flow.",
|
||||
"flow.sharedInputNodeHint": "Ten blok korzysta z centralnej konfiguracji wspólnego wejścia.",
|
||||
"flow.sharedInputNodeHint": "Ten blok korzysta z centralnego źródła wartości. Poniższe porównanie należy tylko do tego Flow.",
|
||||
"flow.sharedInputFlowComparisonHint": "Wspólne wejście dostarcza tylko źródło wartości. Warunek dla tego Flow ustaw tutaj.",
|
||||
"flow.selectOperator": "Wybierz operator",
|
||||
"flow.sharedInputNodeEmptyHint": "Najpierw dodaj wspólne wejście w Home Assistant / Czujniki.",
|
||||
"flow.sharedInputNameRequired": "Podaj nazwę wspólnego wejścia.",
|
||||
"flow.sharedInputDeleteConfirm": "Usunąć wspólne wejście „{name}”?",
|
||||
@@ -971,9 +974,7 @@
|
||||
"flow.sharedInputTestHint": "Test używa zapisanej konfiguracji połączenia Home Assistant i nie zapisuje tego wejścia.",
|
||||
"flow.sharedInputTestEntityRequired": "Podaj entity_id przed testem.",
|
||||
"flow.sharedInputTestCurrent": "Aktualna wartość",
|
||||
"flow.sharedInputTestExpected": "Warunek",
|
||||
"flow.sharedInputTestMatched": "TRUE — warunek spełniony",
|
||||
"flow.sharedInputTestNotMatched": "FALSE — warunek niespełniony",
|
||||
"flow.sharedInputTestValueRead": "Wartość odczytana poprawnie",
|
||||
"flow.sharedInputTestAvailable": "Encja dostępna",
|
||||
"flow.sharedInputTestUnavailable": "Encja niedostępna",
|
||||
"flow.sharedInputUsedBy": "Używane w {count} Flow",
|
||||
@@ -1042,6 +1043,7 @@
|
||||
"flow.logMessageBlockedThermostatOwner": "Akcja Flow została pominięta, ponieważ urządzeniem steruje aktywna strefa termostatu.",
|
||||
"flow.logMessageBlockedOwnership": "Akcja Flow została pominięta, ponieważ w trakcie wykonania zmienił się właściciel sterowania.",
|
||||
"flow.logMessageConflict": "Akcja Flow została pominięta z powodu konfliktu z inną automatyzacją dla tego samego celu.",
|
||||
"flow.nameAliasHint": "To tylko nazwa wyświetlana użytkownikowi. Techniczne ID Flow i nazwy wygenerowanych zadań pozostają bez zmian."
|
||||
"flow.nameAliasHint": "To tylko nazwa wyświetlana użytkownikowi. Techniczne ID Flow i nazwy wygenerowanych zadań pozostają bez zmian.",
|
||||
"flow.sharedInputSourceOnlyHint": "Wspólne wejście zapisuje tylko źródło wartości. Porównanie ustawiasz w bloku Flow."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@ bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./install.sh
|
||||
e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./network-debug.sh
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./service.sh
|
||||
8ccca8ed49576cf0b8e3453436ac3ff9c2a222cd41b27db98ceb62bd5f87aeef ./smoke.sh
|
||||
fec8b0362e763bbc115b5cc5c56f9ebe815730cee161632d81c5dc0ee1475f80 ./smoke.sh
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./update.sh
|
||||
|
||||
@@ -83,6 +83,49 @@ grep -q '"revision":2' "$TMP/flow-updated.json"
|
||||
STALE_STATUS="$(curl -sS -o /dev/null -w '%{http_code}' -X PUT -H 'Content-Type: application/json' --data-binary @"$TMP/flow-update.json" "http://127.0.0.1:$PORT/api/flows/$FLOW_ID")"
|
||||
[[ "$STALE_STATUS" == "409" ]]
|
||||
|
||||
# Shared Flow inputs are source-only; comparisons belong exclusively to Flow references.
|
||||
curl -fsS "http://127.0.0.1:$PORT/api/settings" >"$TMP/settings-shared-input.json"
|
||||
python3 - "$TMP/settings-shared-input.json" "$TMP/settings-shared-input-put.json" <<'PYFLOW'
|
||||
import json,sys
|
||||
settings=json.load(open(sys.argv[1]))
|
||||
items=settings.setdefault("home_assistant",{}).setdefault("flow_inputs",[])
|
||||
items=[item for item in items if item.get("id") != "smoke-shared-outdoor"]
|
||||
items.append({"id":"smoke-shared-outdoor","name":"Smoke outdoor value","kind":"outdoor_temperature","config":{}})
|
||||
settings["home_assistant"]["flow_inputs"]=items
|
||||
json.dump(settings,open(sys.argv[2],"w"))
|
||||
PYFLOW
|
||||
curl -fsS -X PUT -H 'Content-Type: application/json' --data-binary @"$TMP/settings-shared-input-put.json" \
|
||||
"http://127.0.0.1:$PORT/api/settings" >"$TMP/settings-shared-input-result.json"
|
||||
python3 - "$TMP/settings-shared-input-result.json" <<'PYFLOW'
|
||||
import json,sys
|
||||
settings=json.load(open(sys.argv[1]))
|
||||
item=next(item for item in settings["home_assistant"]["flow_inputs"] if item["id"] == "smoke-shared-outdoor")
|
||||
assert item["config"] == {}, item
|
||||
PYFLOW
|
||||
python3 - "$TMP/settings-shared-input-put.json" "$TMP/settings-shared-input-invalid.json" <<'PYFLOW'
|
||||
import json,sys
|
||||
settings=json.load(open(sys.argv[1]))
|
||||
item=next(item for item in settings["home_assistant"]["flow_inputs"] if item["id"] == "smoke-shared-outdoor")
|
||||
item["config"]={"operator":"lt","value":20}
|
||||
json.dump(settings,open(sys.argv[2],"w"))
|
||||
PYFLOW
|
||||
INVALID_SHARED_STATUS="$(curl -sS -o /dev/null -w '%{http_code}' -X PUT -H 'Content-Type: application/json' --data-binary @"$TMP/settings-shared-input-invalid.json" "http://127.0.0.1:$PORT/api/settings")"
|
||||
[[ "$INVALID_SHARED_STATUS" == "400" ]]
|
||||
cat >"$TMP/flow-shared-value.json" <<EOF
|
||||
{"flow":{"name":"Shared value comparisons","enabled":true,"nodes":[{"id":"low","kind":"shared_input","x":20,"y":20,"config":{"input_id":"smoke-shared-outdoor","operator":"lt","value":20}},{"id":"high","kind":"shared_input","x":20,"y":140,"config":{"input_id":"smoke-shared-outdoor","operator":"gt","value":30}},{"id":"either","kind":"logic_or","x":220,"y":80,"config":{}},{"id":"action","kind":"zone_thermostat","x":420,"y":80,"config":{"zone_id":"$ZONE_ID","preset":"comfort","mode":"auto","cooldown_seconds":60}}],"edges":[{"id":"e1","from":"low","to":"either"},{"id":"e2","from":"high","to":"either"},{"id":"e3","from":"either","to":"action"}]},"overrides":{"low":15,"high":15},"log":false}
|
||||
EOF
|
||||
curl -fsS -X POST -H 'Content-Type: application/json' --data-binary @"$TMP/flow-shared-value.json" \
|
||||
"http://127.0.0.1:$PORT/api/flows/simulate" >"$TMP/flow-shared-value-result.json"
|
||||
python3 - "$TMP/flow-shared-value-result.json" <<'PYFLOW'
|
||||
import json,sys
|
||||
result=json.load(open(sys.argv[1]))
|
||||
action=result["actions"][0]
|
||||
assert action["matched"] is True, result
|
||||
trace={item["node_id"]:item for item in action["trace"]}
|
||||
assert trace["low"]["matched"] is True and trace["low"]["expected"] == 20, trace
|
||||
assert trace["high"]["matched"] is False and trace["high"]["expected"] == 30, trace
|
||||
PYFLOW
|
||||
|
||||
# A schedule-compatible Flow must keep using the native schedule engine.
|
||||
cat >"$TMP/flow-schedule.json" <<EOF
|
||||
{"name":"Native schedule Flow","enabled":true,"nodes":[{"id":"days","kind":"weekday","x":20,"y":20,"config":{"days":[1,2,3,4,5,6,7]}},{"id":"time","kind":"time_range","x":180,"y":20,"config":{"start":"06:00","end":"08:00"}},{"id":"action","kind":"zone_thermostat","x":360,"y":20,"config":{"zone_id":"$ZONE_ID","preset":"comfort","mode":"auto","cooldown_seconds":60}}],"edges":[{"id":"e1","from":"days","to":"time"},{"id":"e2","from":"time","to":"action"}]}
|
||||
|
||||
+75
-1
@@ -51,6 +51,62 @@ fn flow_condition_kind(kind: &str) -> bool {
|
||||
fn flow_logic_kind(kind: &str) -> bool { matches!(kind, "logic_and" | "logic_or" | "logic_not") }
|
||||
fn flow_action_kind(kind: &str) -> bool { matches!(kind, "zone_thermostat" | "device_action" | "group_action") }
|
||||
|
||||
fn shared_input_comparison_kind(kind: &str) -> bool {
|
||||
matches!(kind,
|
||||
"outdoor_temperature" | "device_temperature" | "zone_temperature" |
|
||||
"ha_state" | "ha_numeric" | "ha_attribute" | "house_mode" |
|
||||
"device_state" | "zone_state" | "group_state"
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_shared_input_source(kind: &str, config: &Value, state: &AppState) -> Result<(), AppError> {
|
||||
match kind {
|
||||
"outdoor_temperature" | "house_mode" | "night_mode" => {}
|
||||
"device_temperature" => {
|
||||
let id = flow_string(config, "device_id").ok_or_else(|| AppError::BadRequest("shared device temperature input needs device".into()))?;
|
||||
if state.db.get_device(&id)?.is_none() { return Err(AppError::BadRequest("shared Flow input references a missing device".into())); }
|
||||
}
|
||||
"zone_temperature" => {
|
||||
let id = flow_string(config, "zone_id").ok_or_else(|| AppError::BadRequest("shared zone temperature input needs zone".into()))?;
|
||||
if state.db.get_zone(&id)?.is_none() { return Err(AppError::BadRequest("shared Flow input references a missing zone".into())); }
|
||||
}
|
||||
"ha_state" | "ha_numeric" | "ha_available" => {
|
||||
if flow_string(config, "entity_id").is_none() { return Err(AppError::BadRequest("shared Home Assistant input needs entity_id".into())); }
|
||||
}
|
||||
"ha_attribute" => {
|
||||
if flow_string(config, "entity_id").is_none() || flow_string(config, "attribute").is_none() {
|
||||
return Err(AppError::BadRequest("shared Home Assistant attribute input needs entity_id and attribute".into()));
|
||||
}
|
||||
}
|
||||
"device_state" => {
|
||||
let id = flow_string(config, "device_id").ok_or_else(|| AppError::BadRequest("shared device state input needs device".into()))?;
|
||||
if state.db.get_device(&id)?.is_none() { return Err(AppError::BadRequest("shared Flow input references a missing device".into())); }
|
||||
let field = flow_string(config, "field").ok_or_else(|| AppError::BadRequest("shared device state input needs a field".into()))?;
|
||||
if !matches!(field.as_str(), "enabled" | "online" | "power" | "mode" | "fan_speed" | "swing_vertical" | "swing_horizontal" | "quiet" | "turbo" | "light" | "air" | "xfan" | "health" | "sleep") {
|
||||
return Err(AppError::BadRequest("unsupported shared device state field".into()));
|
||||
}
|
||||
}
|
||||
"zone_state" => {
|
||||
let id = flow_string(config, "zone_id").ok_or_else(|| AppError::BadRequest("shared zone state input needs zone".into()))?;
|
||||
if state.db.get_zone(&id)?.is_none() { return Err(AppError::BadRequest("shared Flow input references a missing zone".into())); }
|
||||
let field = flow_string(config, "field").ok_or_else(|| AppError::BadRequest("shared zone state input needs a field".into()))?;
|
||||
if !matches!(field.as_str(), "enabled" | "mode" | "active_preset" | "demand" | "control_owner" | "device_manual_override" | "local_thermostat_power") {
|
||||
return Err(AppError::BadRequest("unsupported shared zone state field".into()));
|
||||
}
|
||||
}
|
||||
"group_state" => {
|
||||
let id = flow_string(config, "group_id").ok_or_else(|| AppError::BadRequest("shared group state input needs group".into()))?;
|
||||
if state.db.get_group(&id)?.is_none() { return Err(AppError::BadRequest("shared Flow input references a missing group".into())); }
|
||||
if flow_string(config, "field").as_deref() != Some("power_enabled") { return Err(AppError::BadRequest("unsupported shared group state field".into())); }
|
||||
}
|
||||
"constant" => {
|
||||
if config.get("value").and_then(Value::as_bool).is_none() { return Err(AppError::BadRequest("shared constant input needs a boolean value".into())); }
|
||||
}
|
||||
_ => return Err(AppError::BadRequest(format!("unsupported shared Flow input kind: {kind}"))),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_flow_graph(input: &FlowInput) -> Result<(), AppError> {
|
||||
if input.name.trim().is_empty() { return Err(AppError::BadRequest("flow name is required".into())); }
|
||||
if input.nodes.is_empty() { return Err(AppError::BadRequest("flow needs at least one block".into())); }
|
||||
@@ -221,7 +277,25 @@ fn validate_condition(condition: &crate::models::FlowCondition, state: &AppState
|
||||
if condition.config.get("value").and_then(Value::as_bool).is_none() { return Err(AppError::BadRequest("constant block needs a boolean value".into())); }
|
||||
}
|
||||
"shared_input" => {
|
||||
if flow_string(&condition.config, "input_id").is_none() { return Err(AppError::BadRequest("shared Flow input block needs input_id".into())); }
|
||||
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()))?;
|
||||
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(|| AppError::BadRequest("shared Flow value needs an operator in the Flow block".into()))?;
|
||||
let mut config = shared.config.clone();
|
||||
let map = config.as_object_mut().ok_or_else(|| AppError::BadRequest("shared Flow input config must be an object".into()))?;
|
||||
map.insert("operator".into(), Value::String(operator));
|
||||
map.insert("value".into(), condition.config.get("value").cloned().unwrap_or(Value::Null));
|
||||
let resolved = crate::models::FlowCondition {
|
||||
id: condition.id.clone(), kind: shared.kind.clone(), config, inputs: Vec::new(),
|
||||
};
|
||||
validate_condition(&resolved, state)?;
|
||||
} else if flow_string(&condition.config, "operator").is_some() || condition.config.get("value").is_some() {
|
||||
return Err(AppError::BadRequest("this shared Flow input is already boolean and does not accept a comparison".into()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
+10
-7
@@ -120,13 +120,16 @@ fn validate_flow_shared_inputs(settings: &mut RuntimeSettings, state: &AppState)
|
||||
"night_mode" | "constant") {
|
||||
return Err(AppError::BadRequest(format!("unsupported shared Flow input kind: {}", item.kind)));
|
||||
}
|
||||
let condition = crate::models::FlowCondition {
|
||||
id: item.id.clone(),
|
||||
kind: item.kind.clone(),
|
||||
config: item.config.clone(),
|
||||
inputs: Vec::new(),
|
||||
};
|
||||
validate_condition(&condition, state)?;
|
||||
if !item.config.is_object() {
|
||||
return Err(AppError::BadRequest("shared Flow input config must be an object".into()));
|
||||
}
|
||||
if item.config.get("operator").is_some() {
|
||||
return Err(AppError::BadRequest("shared Flow inputs are value sources; operator belongs to the Flow block".into()));
|
||||
}
|
||||
if shared_input_comparison_kind(&item.kind) && item.config.get("value").is_some() {
|
||||
return Err(AppError::BadRequest("shared Flow inputs are value sources; comparison value belongs to the Flow block".into()));
|
||||
}
|
||||
validate_shared_input_source(&item.kind, &item.config, state)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -286,16 +286,30 @@ async fn flow_leaf_observation(
|
||||
overrides: &HashMap<String, Value>,
|
||||
) -> Result<(bool, Value), AppError> {
|
||||
let mut resolved_kind = condition.kind.as_str();
|
||||
let mut resolved_config: Option<&Value> = None;
|
||||
let mut resolved_config: Option<Value> = None;
|
||||
if condition.kind == "shared_input" {
|
||||
let input_id = condition.config.get("input_id").and_then(Value::as_str).unwrap_or("");
|
||||
let Some(shared) = settings.home_assistant.flow_inputs.iter().find(|item| item.id == input_id) else {
|
||||
return Ok((false, json!({"error": "missing_shared_input", "input_id": input_id})));
|
||||
};
|
||||
resolved_kind = shared.kind.as_str();
|
||||
resolved_config = Some(&shared.config);
|
||||
let mut config = shared.config.clone();
|
||||
if matches!(shared.kind.as_str(),
|
||||
"outdoor_temperature" | "device_temperature" | "zone_temperature" |
|
||||
"ha_state" | "ha_numeric" | "ha_attribute" | "house_mode" |
|
||||
"device_state" | "zone_state" | "group_state") {
|
||||
let Some(operator) = condition.config.get("operator").and_then(Value::as_str).map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return Ok((false, json!({"error": "missing_shared_input_operator", "input_id": input_id})));
|
||||
};
|
||||
let Some(map) = config.as_object_mut() else {
|
||||
return Ok((false, json!({"error": "invalid_shared_input_config", "input_id": input_id})));
|
||||
};
|
||||
map.insert("operator".into(), Value::String(operator.to_string()));
|
||||
map.insert("value".into(), condition.config.get("value").cloned().unwrap_or(Value::Null));
|
||||
}
|
||||
resolved_config = Some(config);
|
||||
}
|
||||
let c = resolved_config.unwrap_or(&condition.config);
|
||||
let c = resolved_config.as_ref().unwrap_or(&condition.config);
|
||||
let override_value = overrides.get(&condition.id).cloned();
|
||||
let (matched, actual) = match resolved_kind {
|
||||
"weekday" => {
|
||||
@@ -497,12 +511,13 @@ pub async fn evaluate_flow_conditions_trace(
|
||||
};
|
||||
values.insert(condition.id.clone(), matched);
|
||||
final_id = Some(condition.id.clone());
|
||||
let expected = condition.config.get("value").cloned();
|
||||
trace.push(json!({
|
||||
"node_id": condition.id,
|
||||
"kind": condition.kind,
|
||||
"matched": matched,
|
||||
"actual": actual,
|
||||
"expected": condition.config.get("value"),
|
||||
"expected": expected,
|
||||
"inputs": condition.inputs
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ pub struct HomeAssistantSettings {
|
||||
/// Friendly labels used only by the controller UI/charts; entity_id remains the storage key.
|
||||
#[serde(default)]
|
||||
pub sensor_aliases: BTreeMap<String, String>,
|
||||
/// Reusable Flow condition inputs shared by any visual Flow.
|
||||
/// Reusable value sources shared by visual Flows. Comparisons belong to Flow nodes.
|
||||
#[serde(default)]
|
||||
pub flow_inputs: Vec<FlowSharedInput>,
|
||||
}
|
||||
|
||||
+3
-2
@@ -386,7 +386,8 @@
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.haThermostatSources">Źródła temperatury sterowania</h3><p data-i18n="settings.haThermostatSourcesHint">Te ustawienia należą do logiki termostatów i źródeł temperatury, nie do wspólnych wejść Flow.</p></div></div>
|
||||
<div class="settings-grid">
|
||||
<label class="wide"><span data-i18n="settings.defaultEntity">Default room entity_id</span><input name="ha_entity_id" placeholder="sensor.living_room_temperature"></label>
|
||||
<label class="wide"><span data-i18n="settings.defaultEntity">Connection test entity_id</span><input name="ha_entity_id" placeholder="sensor.living_room_temperature"></label>
|
||||
<p class="field-note wide" data-i18n="settings.defaultEntityHint">This entity is used by the Home Assistant connection test when no other entity is specified. It is not automatically used as a zone temperature source.</p>
|
||||
<label class="wide"><span data-i18n="settings.outdoorEntity">Outdoor temperature entity_id</span><input name="ha_outdoor_entity_id" placeholder="sensor.outdoor_temperature"></label>
|
||||
<label class="wide"><span data-i18n="settings.haSensorStaleAfterMinutes">Maximum HA sensor reading age (min)</span><input type="number" name="ha_sensor_stale_after_minutes" min="1" max="1440" step="1" value="5"></label>
|
||||
<p class="field-note wide" data-i18n="settings.haSensorStaleAfterHint">If the HA sensor is not updated within this time, the reading is treated as stale.</p>
|
||||
@@ -395,7 +396,7 @@
|
||||
</section>
|
||||
|
||||
<section class="panel settings-block flow-shared-settings" id="flowSharedInputsSettings">
|
||||
<div class="settings-block-head flow-shared-settings-head"><div><h3 data-i18n="flow.sharedInputsTitle">Wspólne wejścia Flow</h3><p data-i18n="flow.sharedInputsHint">Zdefiniuj stałe i warunki czujników raz, a potem używaj ich w wielu Flow.</p></div><button type="button" class="primary flow-shared-add-button" id="addFlowSharedInput" data-i18n="flow.sharedInputAdd">Dodaj wejście</button></div>
|
||||
<div class="settings-block-head flow-shared-settings-head"><div><h3 data-i18n="flow.sharedInputsTitle">Wspólne wejścia Flow</h3><p data-i18n="flow.sharedInputsHint">Zdefiniuj wartości, stany i czujniki raz, a potem używaj ich w wielu Flow. Operator jest opcjonalny.</p></div><button type="button" class="primary flow-shared-add-button" id="addFlowSharedInput" data-i18n="flow.sharedInputAdd">Dodaj wejście</button></div>
|
||||
<input type="hidden" id="flowSharedInputsRevision" name="flow_inputs_revision" value="[]"><div id="flowSharedInputList" class="flow-shared-input-list"></div>
|
||||
</section>
|
||||
|
||||
|
||||
+70
-3
@@ -29,6 +29,29 @@ function newFlowId(prefix = 'node') {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
function sharedFlowInputRequiresComparison(kind) {
|
||||
return ['outdoor_temperature','device_temperature','zone_temperature','ha_state','ha_numeric','ha_attribute','house_mode','device_state','zone_state','group_state'].includes(kind);
|
||||
}
|
||||
|
||||
function sharedFlowReferenceComparisonDefaults(item) {
|
||||
const kind = item?.kind || '';
|
||||
if (['outdoor_temperature','device_temperature','zone_temperature'].includes(kind)) return { operator:'lt', value:20 };
|
||||
if (kind === 'ha_numeric') return { operator:'lt', value:0 };
|
||||
if (kind === 'ha_state') return { operator:'eq', value:'on' };
|
||||
if (kind === 'ha_attribute') return { operator:'eq', value:'' };
|
||||
if (kind === 'house_mode') return { operator:'eq', value:'cool' };
|
||||
if (kind === 'device_state') return { operator:'eq', value:'true' };
|
||||
if (kind === 'zone_state') return { operator:'eq', value:'true' };
|
||||
if (kind === 'group_state') return { operator:'eq', value:'true' };
|
||||
return {};
|
||||
}
|
||||
|
||||
function sharedFlowReferenceDefaultConfig(item) {
|
||||
const config = { input_id:item?.id || '' };
|
||||
if (item && sharedFlowInputRequiresComparison(item.kind)) Object.assign(config, sharedFlowReferenceComparisonDefaults(item));
|
||||
return config;
|
||||
}
|
||||
|
||||
function flowDefaultConfig(kind) {
|
||||
if (kind === 'weekday') return { days: [1, 2, 3, 4, 5] };
|
||||
if (kind === 'time_range') return { start: '06:00', end: '08:00' };
|
||||
@@ -46,7 +69,7 @@ function flowDefaultConfig(kind) {
|
||||
if (kind === 'group_state') return { group_id: app.groups[0]?.id || '', field: 'power_enabled', operator: 'eq', value: 'true' };
|
||||
if (kind === 'night_mode') return {};
|
||||
if (kind === 'constant') return { value: true };
|
||||
if (kind === 'shared_input') return { input_id: app.flowSharedInputs?.[0]?.id || '' };
|
||||
if (kind === 'shared_input') return sharedFlowReferenceDefaultConfig(app.flowSharedInputs?.[0]);
|
||||
if (kind === 'zone_thermostat') return { zone_id: app.zones[0]?.id || '', preset: 'comfort', setpoint: 21, mode: 'auto', cooldown_seconds: 60 };
|
||||
if (kind === 'device_action') return { device_id: app.devices[0]?.id || '', power: true, mode: '', target_temperature: null, fan_speed: null, swing_vertical: null, swing_horizontal: null, quiet: null, turbo: null, light: null, air: null, xfan: null, health: null, sleep: null, cooldown_seconds: 60 };
|
||||
if (kind === 'group_action') return { group_id: app.groups[0]?.id || '', power: true, mode: '', preset: '', setpoint: 21, cooldown_seconds: 60 };
|
||||
@@ -109,7 +132,16 @@ function flowNodeSummary(node) {
|
||||
if (node.kind === 'group_state') return `${app.groups.find(g => g.id === c.group_id)?.name || tr('groups.group')} · ${c.field || 'power_enabled'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
|
||||
if (node.kind === 'night_mode') return tr('flow.nightModeActive');
|
||||
if (node.kind === 'constant') return c.value === false ? tr('common.no') : tr('common.yes');
|
||||
if (node.kind === 'shared_input') { const item = (app.flowSharedInputs || []).find(value => value.id === c.input_id); return item ? `${item.name} · ${sharedFlowInputSummary(item)}` : tr('flow.sharedInputMissing'); }
|
||||
if (node.kind === 'shared_input') {
|
||||
const item = (app.flowSharedInputs || []).find(value => value.id === c.input_id);
|
||||
if (!item) return tr('flow.sharedInputMissing');
|
||||
if (sharedFlowInputRequiresComparison(item.kind)) {
|
||||
return c.operator
|
||||
? `${item.name} · ${sharedFlowInputSourceSummary(item)} · ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`
|
||||
: `${item.name} · ${tr('flow.selectOperator')}`;
|
||||
}
|
||||
return `${item.name} · ${sharedFlowInputSourceSummary(item)}`;
|
||||
}
|
||||
if (node.kind === 'logic_and') return tr('flow.allConditions');
|
||||
if (node.kind === 'logic_or') return tr('flow.anyCondition');
|
||||
if (node.kind === 'logic_not') return tr('flow.invertCondition');
|
||||
@@ -161,6 +193,20 @@ function flowSelectOptions(items, selected, nameFn = item => item.name) {
|
||||
}
|
||||
function flowOperatorOptions(selected) { return [['lt','<'],['lte','≤'],['gt','>'],['gte','≥'],['eq','='],['neq','≠']].map(([v,l]) => `<option value="${v}" ${v === selected ? 'selected' : ''}>${l}</option>`).join(''); }
|
||||
|
||||
function sharedFlowReferenceComparisonFields(item, config) {
|
||||
if (!item || !sharedFlowInputRequiresComparison(item.kind)) return '';
|
||||
const defaults = sharedFlowReferenceComparisonDefaults(item);
|
||||
const selected = config.operator || '';
|
||||
const numeric = ['outdoor_temperature','device_temperature','zone_temperature','ha_numeric'].includes(item.kind);
|
||||
const fullOperators = numeric || item.kind === 'ha_attribute';
|
||||
const operatorOptions = `${!selected ? `<option value="" selected disabled>${esc(tr('flow.selectOperator'))}</option>` : ''}${fullOperators ? flowOperatorOptions(selected) : [['eq','='],['neq','≠']].map(([value,label]) => `<option value="${value}" ${value === selected ? 'selected' : ''}>${label}</option>`).join('')}`;
|
||||
const value = config.value ?? defaults.value ?? '';
|
||||
let valueField = `<input data-flow-config="value" value="${esc(value)}">`;
|
||||
if (numeric) valueField = `<input type="number" step="0.1" data-flow-config="value" value="${Number(value ?? 0)}">`;
|
||||
else if (item.kind === 'house_mode') valueField = `<select data-flow-config="value">${['cool','heat','off'].map(v => `<option value="${v}" ${String(value) === v ? 'selected' : ''}>${esc(v === 'off' ? tr('common.off') : tr(`mode.${v}`))}</option>`).join('')}</select>`;
|
||||
return `<p class="field-note">${esc(tr('flow.sharedInputFlowComparisonHint'))}</p><div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${operatorOptions}</select></label><label><span>${esc(tr('flow.value'))}</span>${valueField}</label></div>`;
|
||||
}
|
||||
|
||||
function renderFlowInspector() {
|
||||
const host = $('#flowInspector'), node = flowNodeById(app.flowSelectedNodeId); if (!host) return;
|
||||
if (!node) { host.innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.selectBlock'))}</strong><span>${esc(tr('flow.selectBlockHint'))}</span></div>`; return; }
|
||||
@@ -182,7 +228,12 @@ function renderFlowInspector() {
|
||||
else if (node.kind === 'group_state') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-flow-config="group_id">${flowSelectOptions(app.groups, c.group_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-flow-config="field"><option value="power_enabled" selected>power_enabled</option></select></label>${flowTextComparisonFields(c)}`;
|
||||
else if (node.kind === 'night_mode') fields = `<p class="field-note">${esc(tr('flow.nightModeHint'))}</p>`;
|
||||
else if (node.kind === 'constant') fields = `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value"><option value="true" ${c.value !== false ? 'selected' : ''}>${esc(tr('common.yes'))}</option><option value="false" ${c.value === false ? 'selected' : ''}>${esc(tr('common.no'))}</option></select></label>`;
|
||||
else if (node.kind === 'shared_input') fields = (app.flowSharedInputs || []).length ? `<label><span>${esc(tr('flow.sharedInputTitle'))}</span><select data-flow-config="input_id">${flowSelectOptions(app.flowSharedInputs || [], c.input_id)}</select></label><p class="field-note">${esc(tr('flow.sharedInputNodeHint'))}</p>` : `<div class="empty compact"><strong>${esc(tr('flow.sharedInputsEmpty'))}</strong><span>${esc(tr('flow.sharedInputNodeEmptyHint'))}</span></div>`;
|
||||
else if (node.kind === 'shared_input') {
|
||||
const item = (app.flowSharedInputs || []).find(value => value.id === c.input_id) || app.flowSharedInputs?.[0];
|
||||
fields = (app.flowSharedInputs || []).length
|
||||
? `<label><span>${esc(tr('flow.sharedInputTitle'))}</span><select data-flow-config="input_id">${flowSelectOptions(app.flowSharedInputs || [], c.input_id)}</select></label><p class="field-note">${esc(tr('flow.sharedInputNodeHint'))}</p>${sharedFlowReferenceComparisonFields(item, c)}`
|
||||
: `<div class="empty compact"><strong>${esc(tr('flow.sharedInputsEmpty'))}</strong><span>${esc(tr('flow.sharedInputNodeEmptyHint'))}</span></div>`;
|
||||
}
|
||||
else if (node.kind === 'logic_and') fields = `<p class="field-note">${esc(tr('flow.andHint'))}</p>`;
|
||||
else if (node.kind === 'logic_or') fields = `<p class="field-note">${esc(tr('flow.orHint'))}</p>`;
|
||||
else if (node.kind === 'logic_not') fields = `<p class="field-note">${esc(tr('flow.notHint'))}</p>`;
|
||||
@@ -698,6 +749,22 @@ function updateFlowConfig(input) {
|
||||
const node = flowNodeById(app.flowSelectedNodeId); if (!node) return;
|
||||
const key = input.dataset.flowConfig; if (!key) return;
|
||||
if (key === 'days') node.config.days = $$('[data-flow-config="days"]', $('#flowInspector')).filter(el => el.checked).map(el => Number(el.value));
|
||||
else if (node.kind === 'shared_input' && key === 'input_id') {
|
||||
const item = (app.flowSharedInputs || []).find(value => value.id === input.value);
|
||||
node.config = sharedFlowReferenceDefaultConfig(item);
|
||||
}
|
||||
else if (node.kind === 'shared_input' && key === 'operator') {
|
||||
node.config.operator = input.value;
|
||||
if (node.config.value == null) {
|
||||
const item = (app.flowSharedInputs || []).find(value => value.id === node.config.input_id);
|
||||
node.config.value = sharedFlowReferenceComparisonDefaults(item).value ?? '';
|
||||
}
|
||||
}
|
||||
else if (node.kind === 'shared_input' && key === 'value') {
|
||||
const item = (app.flowSharedInputs || []).find(value => value.id === node.config.input_id);
|
||||
const numeric = ['outdoor_temperature','device_temperature','zone_temperature','ha_numeric'].includes(item?.kind);
|
||||
node.config.value = numeric ? (input.value === '' ? null : Number(input.value)) : input.value;
|
||||
}
|
||||
else if (['power','swing_vertical','swing_horizontal','quiet','turbo','light','air','xfan','health','sleep'].includes(key)) node.config[key] = input.value === '' ? null : input.value === 'true';
|
||||
else if (key === 'value' && node.kind === 'constant') node.config[key] = input.value === 'true';
|
||||
else if (['value','setpoint','target_temperature','cooldown_seconds','fan_speed'].includes(key) && ['outdoor_temperature','device_temperature','zone_temperature','ha_numeric','zone_thermostat','device_action','group_action'].includes(node.kind)) node.config[key] = input.value === '' ? null : Number(input.value);
|
||||
|
||||
+36
-21
@@ -36,9 +36,31 @@ function flowSharedInputKinds() {
|
||||
];
|
||||
}
|
||||
|
||||
function sharedFlowInputSummary(item) {
|
||||
function flowSharedInputDefaultConfig(kind) {
|
||||
if (kind === 'device_temperature') return { device_id: app.devices[0]?.id || '' };
|
||||
if (kind === 'zone_temperature') return { zone_id: app.zones[0]?.id || '' };
|
||||
if (kind === 'ha_state' || kind === 'ha_numeric' || kind === 'ha_available') return { entity_id: '' };
|
||||
if (kind === 'ha_attribute') return { entity_id: '', attribute: '' };
|
||||
if (kind === 'device_state') return { device_id: app.devices[0]?.id || '', field: 'online' };
|
||||
if (kind === 'zone_state') return { zone_id: app.zones[0]?.id || '', field: 'demand' };
|
||||
if (kind === 'group_state') return { group_id: app.groups[0]?.id || '', field: 'power_enabled' };
|
||||
if (kind === 'constant') return { value: true };
|
||||
return {};
|
||||
}
|
||||
|
||||
function sharedFlowInputSourceSummary(item) {
|
||||
if (!item) return '—';
|
||||
try { return flowNodeSummary({ kind: item.kind, config: item.config || {} }); } catch (_) { return item.kind || '—'; }
|
||||
const c = item.config || {};
|
||||
if (item.kind === 'outdoor_temperature') return tr('flow.node.outdoorTemperature');
|
||||
if (item.kind === 'device_temperature') return app.devices.find(value => value.id === c.device_id)?.name || tr('common.noDevice');
|
||||
if (item.kind === 'zone_temperature') return app.zones.find(value => value.id === c.zone_id)?.name || tr('common.noZone');
|
||||
if (item.kind === 'ha_state' || item.kind === 'ha_numeric' || item.kind === 'ha_available') return c.entity_id || 'entity_id';
|
||||
if (item.kind === 'ha_attribute') return `${c.entity_id || 'entity_id'}.${c.attribute || 'attribute'}`;
|
||||
if (item.kind === 'house_mode') return tr('flow.houseMode');
|
||||
if (item.kind === 'device_state') return `${app.devices.find(value => value.id === c.device_id)?.name || tr('common.noDevice')} · ${c.field || 'state'}`;
|
||||
if (item.kind === 'zone_state') return `${app.zones.find(value => value.id === c.zone_id)?.name || tr('common.noZone')} · ${c.field || 'state'}`;
|
||||
if (item.kind === 'group_state') return `${app.groups.find(value => value.id === c.group_id)?.name || tr('groups.group')} · ${c.field || 'power_enabled'}`;
|
||||
return flowNodeSummary({ kind:item.kind, config:c });
|
||||
}
|
||||
|
||||
function isHaSharedInputKind(kind) { return ['ha_state','ha_numeric','ha_attribute','ha_available'].includes(kind); }
|
||||
@@ -56,7 +78,7 @@ function renderFlowSharedInputs() {
|
||||
? `<div class="flow-shared-usage"><small>${esc(tr('flow.sharedInputUsedBy', { count:usages.length }))}</small><div>${usages.slice(0, 4).map(flow => `<button type="button" class="link-button" data-open-shared-flow="${esc(flow.id)}" title="${esc(tr('flow.openReferencedFlow', { name:flow.name }))}">${esc(flow.name)}</button>`).join('')}${usages.length > 4 ? `<span class="muted">+${usages.length - 4}</span>` : ''}</div></div>`
|
||||
: `<small class="flow-shared-unused">${esc(tr('flow.sharedInputUnused'))}</small>`;
|
||||
return `<div class="flow-shared-input-row">
|
||||
<div><strong>${esc(item.name)}</strong><small>${esc(flowNodeTitle(FLOW_NODE_META[item.kind] || { title:item.kind }))} · ${esc(sharedFlowInputSummary(item))}</small><code>${esc(item.id)}</code>${usageMarkup}</div>
|
||||
<div><strong>${esc(item.name)}</strong><small>${esc(flowNodeTitle(FLOW_NODE_META[item.kind] || { title:item.kind }))} · ${esc(sharedFlowInputSourceSummary(item))}</small><code>${esc(item.id)}</code>${usageMarkup}</div>
|
||||
<div class="flow-shared-input-actions"><button type="button" class="secondary" data-flow-shared-edit="${esc(item.id)}">${esc(tr('actions.edit'))}</button><button type="button" class="danger" data-flow-shared-delete="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div>
|
||||
</div>`;
|
||||
}).join('') : `<div class="empty compact"><strong>${esc(tr('flow.sharedInputsEmpty'))}</strong><span>${esc(tr('flow.sharedInputsEmptyHint'))}</span></div>`;
|
||||
@@ -68,28 +90,22 @@ function sharedFlowOptions(items, selected, label = item => item.name) {
|
||||
return items.map(item => `<option value="${esc(item.id)}" ${item.id === selected ? 'selected' : ''}>${esc(label(item))}</option>`).join('');
|
||||
}
|
||||
|
||||
function sharedFlowOperatorOptions(selected = 'eq') {
|
||||
return [['lt','<'],['lte','≤'],['gt','>'],['gte','≥'],['eq','='],['neq','≠']].map(([value,label]) => `<option value="${value}" ${value === selected ? 'selected' : ''}>${label}</option>`).join('');
|
||||
}
|
||||
|
||||
function renderFlowSharedInputFields(kind, config = {}) {
|
||||
const host = $('#flowSharedInputFields'); if (!host) return;
|
||||
const c = config || {};
|
||||
const comparison = (temperature = false) => `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-shared-config="operator">${sharedFlowOperatorOptions(c.operator || 'lt')}</select></label><label><span>${esc(temperature ? tr('automations.thresholdC') : tr('flow.value'))}</span><input type="number" step="0.1" data-shared-config="value" value="${Number(c.value ?? 0)}"></label></div>`;
|
||||
const textComparison = () => `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-shared-config="operator"><option value="eq" ${c.operator !== 'neq' ? 'selected' : ''}>=</option><option value="neq" ${c.operator === 'neq' ? 'selected' : ''}>≠</option></select></label><label><span>${esc(tr('flow.value'))}</span><input data-shared-config="value" value="${esc(c.value ?? '')}"></label></div>`;
|
||||
let fields = '';
|
||||
if (kind === 'constant') fields = `<label><span>${esc(tr('flow.value'))}</span><select data-shared-config="value"><option value="true" ${c.value !== false ? 'selected' : ''}>${esc(tr('common.yes'))}</option><option value="false" ${c.value === false ? 'selected' : ''}>${esc(tr('common.no'))}</option></select></label>`;
|
||||
else if (kind === 'ha_state') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label>${textComparison()}`;
|
||||
else if (kind === 'ha_numeric') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="sensor.energy_price"></label>${comparison(false)}`;
|
||||
else if (kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="climate.living_room"></label><label><span>${esc(tr('flow.attribute'))}</span><input data-shared-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label><div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-shared-config="operator">${sharedFlowOperatorOptions(c.operator || 'eq')}</select></label><label><span>${esc(tr('flow.value'))}</span><input data-shared-config="value" value="${esc(c.value ?? '')}"></label></div>`;
|
||||
else if (kind === 'ha_state') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label>`;
|
||||
else if (kind === 'ha_numeric') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="sensor.energy_price"></label>`;
|
||||
else if (kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="climate.living_room"></label><label><span>${esc(tr('flow.attribute'))}</span><input data-shared-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label>`;
|
||||
else if (kind === 'ha_available') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label><p class="field-note">${esc(tr('flow.haAvailableHint'))}</p>`;
|
||||
else if (kind === 'outdoor_temperature') fields = comparison(true);
|
||||
else if (kind === 'device_temperature') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label>${comparison(true)}`;
|
||||
else if (kind === 'zone_temperature') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label>${comparison(true)}`;
|
||||
else if (kind === 'house_mode') fields = `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-shared-config="operator"><option value="eq" ${c.operator !== 'neq' ? 'selected' : ''}>=</option><option value="neq" ${c.operator === 'neq' ? 'selected' : ''}>≠</option></select></label><label><span>${esc(tr('flow.houseMode'))}</span><select data-shared-config="value">${['cool','heat','off'].map(v => `<option value="${v}" ${c.value === v ? 'selected' : ''}>${esc(v === 'off' ? tr('common.off') : tr(`mode.${v}`))}</option>`).join('')}</select></label></div>`;
|
||||
else if (kind === 'device_state') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-config="field">${['enabled','online','power','mode','fan_speed','swing_vertical','swing_horizontal','quiet','turbo','light','air','xfan','health','sleep'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(v)}</option>`).join('')}</select></label>${textComparison()}`;
|
||||
else if (kind === 'zone_state') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-config="field">${['enabled','mode','active_preset','demand','control_owner','device_manual_override','local_thermostat_power'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(v)}</option>`).join('')}</select></label>${textComparison()}`;
|
||||
else if (kind === 'group_state') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-shared-config="group_id">${sharedFlowOptions(app.groups, c.group_id)}</select></label><input type="hidden" data-shared-config="field" value="power_enabled">${textComparison()}`;
|
||||
else if (kind === 'outdoor_temperature') fields = `<p class="field-note">${esc(tr('flow.sharedInputSourceOnlyHint'))}</p>`;
|
||||
else if (kind === 'device_temperature') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label>`;
|
||||
else if (kind === 'zone_temperature') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label>`;
|
||||
else if (kind === 'house_mode') fields = `<p class="field-note">${esc(tr('flow.sharedInputSourceOnlyHint'))}</p>`;
|
||||
else if (kind === 'device_state') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-config="field">${['enabled','online','power','mode','fan_speed','swing_vertical','swing_horizontal','quiet','turbo','light','air','xfan','health','sleep'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(v)}</option>`).join('')}</select></label>`;
|
||||
else if (kind === 'zone_state') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-config="field">${['enabled','mode','active_preset','demand','control_owner','device_manual_override','local_thermostat_power'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(v)}</option>`).join('')}</select></label>`;
|
||||
else if (kind === 'group_state') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-shared-config="group_id">${sharedFlowOptions(app.groups, c.group_id)}</select></label><input type="hidden" data-shared-config="field" value="power_enabled">`;
|
||||
else if (kind === 'night_mode') fields = `<p class="field-note">${esc(tr('flow.nightModeHint'))}</p>`;
|
||||
host.innerHTML = fields;
|
||||
const testPanel = $('#flowSharedInputTestPanel'), testResult = $('#flowSharedInputTestResult');
|
||||
@@ -106,7 +122,7 @@ function openFlowSharedInputEditor(id = '') {
|
||||
form.name.value = item?.name || '';
|
||||
form.kind.value = item?.kind || 'constant';
|
||||
form.dataset.editingId = item?.id || '';
|
||||
renderFlowSharedInputFields(form.kind.value, item?.config || flowDefaultConfig(form.kind.value));
|
||||
renderFlowSharedInputFields(form.kind.value, item?.config || flowSharedInputDefaultConfig(form.kind.value));
|
||||
dialog.showModal();
|
||||
setTimeout(() => form.name.focus(), 0);
|
||||
}
|
||||
@@ -117,7 +133,6 @@ function collectFlowSharedInputConfig(kind) {
|
||||
const key = field.dataset.sharedConfig;
|
||||
let value = field.value;
|
||||
if (kind === 'constant' && key === 'value') value = value === 'true';
|
||||
else if (field.type === 'number') value = Number(value);
|
||||
config[key] = value;
|
||||
});
|
||||
return config;
|
||||
|
||||
+15
-24
@@ -293,28 +293,19 @@ document.addEventListener('change', event => {
|
||||
|
||||
$('#addFlowSharedInput')?.addEventListener('click', () => openFlowSharedInputEditor());
|
||||
$('#flowSharedInputKind')?.addEventListener('change', event => {
|
||||
renderFlowSharedInputFields(event.target.value, flowDefaultConfig(event.target.value));
|
||||
renderFlowSharedInputFields(event.target.value, flowSharedInputDefaultConfig(event.target.value));
|
||||
});
|
||||
|
||||
function compareFlowSharedHaValue(actual, operator, expected) {
|
||||
if (['lt','lte','gt','gte'].includes(operator)) {
|
||||
const a = Number(actual), b = Number(expected);
|
||||
if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
|
||||
if (operator === 'lt') return a < b;
|
||||
if (operator === 'lte') return a <= b;
|
||||
if (operator === 'gt') return a > b;
|
||||
return a >= b;
|
||||
}
|
||||
const left = actual == null ? '' : String(actual);
|
||||
const right = expected == null ? '' : String(expected);
|
||||
return operator === 'neq' ? left !== right : left === right;
|
||||
}
|
||||
|
||||
function evaluateFlowSharedHaTest(kind, config, result) {
|
||||
if (kind === 'ha_available') return { actual:result.state, matched:result.available === true };
|
||||
let actual = result.state;
|
||||
if (kind === 'ha_attribute') actual = result.attributes?.[config.attribute];
|
||||
return { actual, matched:compareFlowSharedHaValue(actual, config.operator || 'eq', config.value) };
|
||||
if (kind === 'ha_available') return { actual:result.available === true, valid:true };
|
||||
if (kind === 'ha_attribute') {
|
||||
const actual = result.attributes?.[config.attribute];
|
||||
return { actual, valid:actual !== undefined };
|
||||
}
|
||||
if (kind === 'ha_numeric') {
|
||||
const actual = Number(result.state);
|
||||
return { actual, valid:Number.isFinite(actual) };
|
||||
}
|
||||
return { actual:result.state, valid:true };
|
||||
}
|
||||
|
||||
$('#flowSharedInputTest')?.addEventListener('click', async event => {
|
||||
@@ -336,10 +327,10 @@ $('#flowSharedInputTest')?.addEventListener('click', async event => {
|
||||
const result = await api('/api/integrations/home-assistant/entity', { method:'POST', body:{ entity_id:entityId } });
|
||||
const evaluation = evaluateFlowSharedHaTest(kind, config, result);
|
||||
const actual = evaluation.actual == null ? 'null' : (typeof evaluation.actual === 'object' ? JSON.stringify(evaluation.actual) : String(evaluation.actual));
|
||||
const expectation = kind === 'ha_available' ? tr('flow.sharedInputTestAvailable') : `${config.operator || 'eq'} ${config.value ?? ''}`;
|
||||
resultHost.classList.toggle('pass', evaluation.matched);
|
||||
resultHost.classList.toggle('fail', !evaluation.matched);
|
||||
resultHost.innerHTML = `<div><strong>${esc(evaluation.matched ? tr('flow.sharedInputTestMatched') : tr('flow.sharedInputTestNotMatched'))}</strong><span class="badge ${evaluation.matched ? 'active' : ''}">${esc(result.available ? tr('flow.sharedInputTestAvailable') : tr('flow.sharedInputTestUnavailable'))}</span></div><dl><div><dt>${esc(tr('flow.sharedInputTestCurrent'))}</dt><dd><code>${esc(actual)}</code></dd></div><div><dt>${esc(tr('flow.sharedInputTestExpected'))}</dt><dd><code>${esc(expectation)}</code></dd></div></dl><small>${esc(result.entity_id)}${result.last_updated ? ` · ${esc(dateTime(result.last_updated))}` : ''}</small>`;
|
||||
const success = evaluation.valid;
|
||||
resultHost.classList.toggle('pass', success);
|
||||
resultHost.classList.toggle('fail', !success);
|
||||
resultHost.innerHTML = `<div><strong>${esc(success ? tr('flow.sharedInputTestValueRead') : tr('flow.sharedInputTestUnavailable'))}</strong><span class="badge ${success ? 'active' : ''}">${esc(result.available ? tr('flow.sharedInputTestAvailable') : tr('flow.sharedInputTestUnavailable'))}</span></div><dl><div><dt>${esc(tr('flow.sharedInputTestCurrent'))}</dt><dd><code>${esc(actual)}</code></dd></div></dl><small>${esc(result.entity_id)}${result.last_updated ? ` · ${esc(dateTime(result.last_updated))}` : ''}</small>`;
|
||||
} catch (error) {
|
||||
resultHost.classList.remove('pass'); resultHost.classList.add('fail');
|
||||
resultHost.innerHTML = `<strong>${esc(tr('flow.sharedInputTestUnavailable'))}</strong><span>${esc(error.message)}</span>`;
|
||||
|
||||
Reference in New Issue
Block a user