Compare commits
117
Commits
2383526a83
...
master
+36
-15
@@ -1,22 +1,43 @@
|
||||
# Serwer HTTP
|
||||
GREE_BIND=0.0.0.0:8787
|
||||
GREE_DATABASE=./data/gree-controller.db
|
||||
# HTTP / WebSocket
|
||||
GREE_CONTROLLER_BIND=0.0.0.0:8787
|
||||
GREE_CONTROLLER_DATABASE=./data/gree-controller.db
|
||||
GREE_CONTROLLER_BASE_PATH=
|
||||
RUST_LOG=gree_controller=info,tower_http=info
|
||||
|
||||
# Pusty token = dostęp bez logowania w zaufanej sieci LAN.
|
||||
# Ustaw długi losowy token, gdy interfejs jest dostępny poza zaufanym VLAN-em.
|
||||
GREE_APP_TOKEN=
|
||||
# Authentication
|
||||
# Empty = no login in standalone mode (trusted LAN only).
|
||||
# Set a long random token when the UI/API is reachable outside a trusted LAN/VLAN.
|
||||
GREE_CONTROLLER_APP_TOKEN=
|
||||
|
||||
# Tryb symulacji pozwala uruchomić aplikację bez klimatyzatora.
|
||||
GREE_SIMULATE=true
|
||||
GREE_AUTO_SEED=true
|
||||
GREE_POLL_INTERVAL_SECONDS=15
|
||||
GREE_ZONE_INTERVAL_SECONDS=5
|
||||
GREE_DISCOVERY_TIMEOUT_MS=3000
|
||||
GREE_DISCOVERY_BROADCAST=255.255.255.255:7000
|
||||
# Public Custom Chart links
|
||||
# Leave empty to use the current standalone origin/base path.
|
||||
# Example behind a reverse proxy:
|
||||
# GREE_CONTROLLER_PUBLIC_CHART_BASE_URL=https://gree.example.com
|
||||
GREE_CONTROLLER_PUBLIC_CHART_BASE_URL=
|
||||
|
||||
# Controller / discovery
|
||||
GREE_CONTROLLER_ID=gree-controller
|
||||
GREE_CONTROLLER_SIMULATE=false
|
||||
GREE_CONTROLLER_AUTO_SEED=false
|
||||
GREE_CONTROLLER_POLL_INTERVAL_SECONDS=15
|
||||
GREE_CONTROLLER_ZONE_INTERVAL_SECONDS=5
|
||||
GREE_CONTROLLER_DISCOVERY_TIMEOUT_MS=3000
|
||||
GREE_CONTROLLER_DISCOVERY_BROADCAST=255.255.255.255:7000
|
||||
# Optional interface name or local IP used only for GREE UDP traffic.
|
||||
GREE_CONTROLLER_GREE_INTERFACE=
|
||||
|
||||
# Opcjonalny sensor temperatury z Home Assistant.
|
||||
# Compressor protection
|
||||
GREE_CONTROLLER_COMPRESSOR_PROTECTION_ENABLED=true
|
||||
GREE_CONTROLLER_COMPRESSOR_PROTECTION_SECONDS=180
|
||||
|
||||
# Optional Home Assistant connection for standalone installations.
|
||||
# Not required inside the Home Assistant add-on; the add-on uses Supervisor API.
|
||||
HA_URL=
|
||||
HA_TOKEN=
|
||||
HA_ENTITY_ID=
|
||||
HA_OUTDOOR_ENTITY_ID=
|
||||
HA_SENSOR_STALE_AFTER_SECONDS=300
|
||||
HA_ALLOW_INVALID_TLS=false
|
||||
|
||||
# Do not set these manually in standalone mode:
|
||||
# GREE_CONTROLLER_HA_AUTH=supervisor
|
||||
# SUPERVISOR_TOKEN=...
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# GREE Controller v0.8.10 - dashboard and notification-filter report
|
||||
|
||||
Source baseline: GREE Controller v0.8.9.
|
||||
|
||||
## Changes
|
||||
|
||||
- added fine-grained notification categories with a dedicated stale Home Assistant sensor switch;
|
||||
- split stale HA reads into the `ha.sensor_stale` event kind while retaining other HA failures as `ha.sensor_error`;
|
||||
- rebuilt the Dashboard as horizontal **Main / Thermostats / Manual control** tabs with one vertically scrolling panel at a time;
|
||||
- moved LAN discovery from the global toolbar to Devices;
|
||||
- added centered `active/total running` unit status to the top toolbar;
|
||||
- changed control-owner metadata to show date and time instead of time only;
|
||||
- removed visible "Quick" wording from the Dashboard thermostat/manual-control section titles;
|
||||
- rotated the PWA cache key for the changed frontend.
|
||||
|
||||
## Validation
|
||||
|
||||
The Rust toolchain is not installed in this build environment, so `cargo test`/`cargo check` cannot be run here. JavaScript syntax checks, JSON parsing, HTML ID checks, shell syntax checks, manifest regeneration and ZIP integrity verification are performed before packaging. The target-host updater should run the full Rust test suite before replacing the service.
|
||||
Generated
+654
-279
File diff suppressed because it is too large
Load Diff
+29
-26
@@ -1,41 +1,44 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.8.10"
|
||||
version = "0.15.17"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8"
|
||||
aes-gcm = "0.10"
|
||||
anyhow = "1"
|
||||
axum = { version = "0.7", features = ["macros", "ws"] }
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4", features = ["serde", "clock"] }
|
||||
clap = { version = "4", features = ["derive", "env"] }
|
||||
dotenvy = "0.15"
|
||||
futures-util = "0.3"
|
||||
libc = "0.2"
|
||||
rand = "0.8"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower-http = { version = "0.6", features = ["compression-gzip", "cors", "trace"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
aes = "=0.9.3"
|
||||
aes-gcm = "=0.11.1"
|
||||
anyhow = "=1.0.104"
|
||||
axum = { version = "=0.8.9", features = ["macros", "ws"] }
|
||||
base64 = "=0.23.1"
|
||||
chrono = { version = "=0.4.45", features = ["serde", "clock"] }
|
||||
clap = { version = "=4.6.7", features = ["derive", "env"] }
|
||||
dotenvy = "=0.15.7"
|
||||
futures-util = "=0.3.34"
|
||||
libc = "=0.2.189"
|
||||
rand = "=0.10.2"
|
||||
reqwest = { version = "=0.13.5", default-features = false, features = ["json", "query", "form", "rustls-no-provider"] }
|
||||
rusqlite = { version = "=0.40.2", features = ["bundled"] }
|
||||
webpki-roots = "=1.0.9"
|
||||
tokio-rustls = { version = "=0.26.5", default-features = false, features = ["ring", "tls12", "logging"] }
|
||||
serde = { version = "=1.0.229", features = ["derive"] }
|
||||
serde_json = "=1.0.151"
|
||||
sha2 = "=0.11.0"
|
||||
thiserror = "=2.0.20"
|
||||
tokio = { version = "=1.53.1", features = ["full"] }
|
||||
tower-http = { version = "=0.7.1", features = ["compression-gzip", "cors", "trace"] }
|
||||
tracing = "=0.1.44"
|
||||
tracing-subscriber = { version = "=0.3.23", features = ["env-filter", "fmt"] }
|
||||
utoipa-swagger-ui = { version = "=9.0.2", default-features = false, features = ["axum", "debug-embed", "vendored"] }
|
||||
url = "=2.5.8"
|
||||
uuid = { version = "=1.26.1", features = ["v4", "serde"] }
|
||||
|
||||
[build-dependencies]
|
||||
serde_json = "1"
|
||||
serde_json = "=1.0.151"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
tempfile = "=3.27.0"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
+215
-60
@@ -1,72 +1,227 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
875d85d3388362890ef0fa28b7192634856b8c095f5fbdacc75d60e014f692cc ./BUILD_REPORT.md
|
||||
c91ff48b9278d8394e79e734b11cc49d0c092881500edc11121b6baca23d2e67 ./Cargo.lock
|
||||
43bf5b7dc87db920fdd5a7157c50c92aef7053a293c51c86c5b5fa76c761f9d7 ./Cargo.toml
|
||||
4051d2de7fa9858545582dc6bb5a411c980a282d0b25aa3766df88597442f94a ./Cargo.lock
|
||||
4bf5b286ffb52b22a32ef3d2d5e4dfaae600c1be01c1cac0b87c7138ecb4a129 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
21dd3dc409d51f29890ee303f97eaa4e68362c100e499c48bfc1ef8e57f525fd ./README.md
|
||||
91c826e2f8c974bdb7b15e01e9b20487f06ff887247ecd9600f1263c11a61b96 ./build.rs
|
||||
6e2953807ea0d1f346f99adb527391806cb088afdbc62e721193be7253a3bc9c ./docs/API.md
|
||||
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
|
||||
7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md
|
||||
10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md
|
||||
2ef5cb04cc74be3aab77a9d117b9040f186c73a7dcf24375f1cca2d73e428449 ./docs/PROJECT_SPEC.md
|
||||
7748b1230029439a405139750b2e0607b8e463bfd56ad86982394105069697c5 ./docs/REVERSE_PROXY.md
|
||||
28f1403d84b01b37bebb72548aa39a75bb14bdbb611a43bfa761319957cd6f39 ./docs/SECURITY.md
|
||||
c3f3f12206c552d3fb919022d64b3fa60342fc1031081e893b7efd743961c402 ./future.md
|
||||
95e3c8c99ba439a86a2fb05cfd8ef00c75cb959a024a349a082da935b726f500 ./home-assistant/README.md
|
||||
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
|
||||
6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py
|
||||
71c8058f85fd9ce9f063b7a84dcf2956f23cdbbd02bd4bd99d2fbb9d177c72ad ./home-assistant/custom_components/gree_controller/climate.py
|
||||
5e4aef2143e81bedb5a15dd4be5c71b64a3ab448ec6d33a20851edd098e5f529 ./home-assistant/custom_components/gree_controller/config_flow.py
|
||||
b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./home-assistant/custom_components/gree_controller/const.py
|
||||
ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assistant/custom_components/gree_controller/coordinator.py
|
||||
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
|
||||
98f494432dbc861b5b4b8928715845590189008482733006af0b0d9277d193a8 ./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
|
||||
1cae12876eb81085910907cf1fcf340a18dcba4f8c967ef222c9496325057849 ./home-assistant/custom_components/gree_controller/sensor.py
|
||||
229ef9b3de8001bdedea42015a219c7f7c17d50f90b10705508494d434aeb549 ./home-assistant/custom_components/gree_controller/switch.py
|
||||
6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./home-assistant/custom_components/gree_controller/translations/en.json
|
||||
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
|
||||
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
|
||||
9a429e8b766c66061e85ccd535cc1429fe1284d14fcf75daa362fea724e0bf85 ./README.md
|
||||
03d73e4b4c0036d2cc07d8057f25ae8c9d89451eebfb43715ee51aa06edd1c93 ./SOURCE_MANIFEST.sha256
|
||||
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
|
||||
3c1fce0f3097e7860f8ccf6a79749af4e7e4bd43e612c6419d91c0e620480506 ./docs/API.md
|
||||
43af7941ec4d427f9db7ab3cc6baf3d182a72dba7a6791f14094fee138324d8d ./docs/FLOW.md
|
||||
3ef573d60de5dcba3fdb4004df53ea170440cbeae2e244a1f6acfcb3255b7c78 ./docs/openapi.json
|
||||
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
|
||||
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
|
||||
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
|
||||
a4596a70e50acfe698a8ae1dcb9bebd04adba95b69781218fca7add82c17703e ./ha-addon/build.sh
|
||||
7dd2534a6f8405e640e28b7cb0e94270cc5f01fad8a972193ec254befe8a96b9 ./ha-addon/home-assistant/README.md
|
||||
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./ha-addon/home-assistant/custom_components/gree_controller/__init__.py
|
||||
3f6ef15ef58456376ac53fde7cace1ef359d6a6f7a64c5b575ad77ea6e55ccc4 ./ha-addon/home-assistant/custom_components/gree_controller/api.py
|
||||
da2a0a9390ab75cef150cd281bc329fa409d0d28f51382b36bd8de3a73325c16 ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
|
||||
5e4aef2143e81bedb5a15dd4be5c71b64a3ab448ec6d33a20851edd098e5f529 ./ha-addon/home-assistant/custom_components/gree_controller/config_flow.py
|
||||
b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/home-assistant/custom_components/gree_controller/const.py
|
||||
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
|
||||
5aea34a2adb03ed46a93199e943d7820e34825658fefc3d7c3eb5267ac9066b0 ./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
|
||||
338a42662e77f91215150851e55bbd39a5c77f5612cc77f3e037c06a6e6bdadb ./ha-addon/home-assistant/custom_components/gree_controller/switch.py
|
||||
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
|
||||
69d6cf26824f8851cdf076f87c7270d6f87717a2820b68b57a82beed53d2c736 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
d3d0e54672751a3400021ffd1fdcc83a9a903be72cff32cdc3607a61aec0a31e ./ha-addon/repository/gree-controller/README.md
|
||||
c591ab21837150e313407a34a5a3758302aaafd91cd5fcebe18bb5358b702433 ./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
|
||||
e6b93d3701d83a31506617a0e6f44ff32d8c24a52be038c8540c862b13546b50 ./ha-addon/repository/gree-controller/translations/pl.yaml
|
||||
2153b230357aab4766398577e23a7a9ff83bb135ddc86d1b3f2cbfc852efe0a1 ./ha-addon/repository/repository.yaml
|
||||
cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run.sh
|
||||
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
|
||||
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
29cc08123a581474a4445d8bf98dcf22f17cdd2e0e7f1358158e588e1252eec5 ./lang/en.json
|
||||
166d7daf829d8b3f9127a90f2da8c3db0ee2d8b490a083d1c278e7ccbde48a27 ./lang/pl.json
|
||||
028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py
|
||||
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
|
||||
859875af2aad9929fe6ecfb80244c8d09182345edd99663e54b04910268e902e ./lang/en.json
|
||||
7139d82eb65341e72ad978be3f57efbe04228b2101b19f792d665d146697d24f ./lang/pl.json
|
||||
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
||||
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
|
||||
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
|
||||
c5276fecc6a33c5de912e5a73b0a829d4e18c18d57b9c0ee49179ef96f133fc7 ./presets/dual_threshold_control.json
|
||||
aa190f51cb20bd0f3749c080e39b4182d8d843db51d9bf7c2315528f67b45a7e ./presets/energy_price_eco.json
|
||||
19d001dc0703dccf9c9ba3c78d866afafae94591ddb6d0d9e2d8c7703eabeea2 ./presets/frost_guard.json
|
||||
a86718e22f79769b40b363f32ec669bf77974e6a6ade081ed4b90792d5803ef7 ./presets/ha_attribute_mode_guard.json
|
||||
cc0d35fb5ad4bd52f1a8a122745bbda19922119cec48338f391f8c0619dab896 ./presets/ha_boiler_supply_boost.json
|
||||
515b2580221fb38975c7f1ac46f267af7d8ac418efd52d30b40d5ce3e4fae1d2 ./presets/ha_external_heat_source_assist.json
|
||||
07907f47c6c5c8dbd142b161135eae123889fb8845cd846fa7d1b05eda7b0740 ./presets/ha_external_heat_source_off.json
|
||||
349df3ee451da70958529bf6c8e8c09e717a26f69fb56e9d6b73831087055966 ./presets/ha_gas_backup_heat.json
|
||||
6fddede93fcb97913139791e6a75fb62322f425c59365b48bd8bb32a48c4ddee ./presets/ha_gas_heating_boost.json
|
||||
40e6bb0a22e29cc9e32844b90eafffb753df4c368ab55b5112c5bf25593491da ./presets/ha_gas_heating_off.json
|
||||
b6dce178fb517509f31e3ce58f6fa512d038ffcea98fae10dd41844b1d954144 ./presets/ha_gas_heating_reduce.json
|
||||
eb95423754fc798b0b5db01757f0fbe97f07bae9f16a95f5ca51ce379809ab7a ./presets/ha_heating_demand_follow.json
|
||||
0e34f595cebea8662646293b75e7422ac2b01c92fe75f152d71b13f6c8fb53d1 ./presets/ha_thermostat_idle_fallback.json
|
||||
e8946176bf86975c04ef40e82565d7f7a4c171bcf8b245c001121ed8aae938c9 ./presets/ha_window_guard.json
|
||||
a87a919d73003f774becc2d48aab646c21af43e50ceb54577cd37f26606a1674 ./presets/humidity_guard.json
|
||||
e78a5a9c02bc47fa6530aad3803581b10b3a0676705cb41f9d003b4da455aa14 ./presets/mild_weather_eco.json
|
||||
c3c10990e992948c2792bb71fa6606e616abb2a5455e1f11d9209c2f05132b86 ./presets/morning_boost.json
|
||||
664afa80d27130799899e54f5a2152a27955e6e2d8c035c2a594c8da6bd902a9 ./presets/multi_room_group_guard.json
|
||||
7518e0c694208c6ff69209edf8eec605fcf9e9abe8130d092c8c89524d7a24b3 ./presets/nested_guard.json
|
||||
863e557fe9c798a5aaa175f29d71752e148bb8d49910485d738bba00671d1198 ./presets/night_group.json
|
||||
c675cf84430f3c2d058c69dbfea1b5c6fcd82e927d53a301b3e517409a324f9a ./presets/night_quiet.json
|
||||
3b87def6c5fdd7f50cd9e6003bd8e0628b1365e1de8bbc414d5a65d1d7d76298 ./presets/occupancy_weather_matrix.json
|
||||
2360e1939cb46352042bd914a1a125fc98252ea80b68c89cc6ce3f5f197cc464 ./presets/offline_safe_off.json
|
||||
0f00f10c2d11984827738cd2910aa24910acbc8f36e754627a76fd5a0aeef7eb ./presets/overheat_guard.json
|
||||
f118ab957389bc4a47fe8c36b2911fb717a23970ec97d14c37a65f65eb479fc5 ./presets/peak_power_guard.json
|
||||
dfb2f5f382a7f83b7a6fe475a26bae73a4510a46e1e7949a99ac752e39f58b4b ./presets/presence_eco.json
|
||||
8e40d01a07911455c3c6607ba3435488fd6e520bebfc84cbf5eaad481883a4ed ./presets/sensor_availability_guard.json
|
||||
45183d75ce254315cd4032acd42fbb0d505df0e7ab1fefecad931a74db2f43c4 ./presets/sleep_temperature_guard.json
|
||||
7e819f2976ff48d89ccb636d08439d6e9736e7e95837c150dca157fb5bc78cb1 ./presets/smart_demand.json
|
||||
6d16f0a35b023afccd0f2011056ed8b15b5e5d2a3be6a52d122733aac862e85d ./presets/thermostat_enabled_guard.json
|
||||
fffa18bd989a0dcc1c5458975522517d40af4af40cc1ca30640e79d3db711053 ./presets/unoccupied_shutdown.json
|
||||
993b8e8151647cbda6e62312266d37d89178b3a4bb13f7df0950840e1ce647a8 ./presets/weather_comfort.json
|
||||
ae21459a261712bcb8d57594528b1648432e8d03a8a38502234829c0dbfef774 ./presets/weekend_comfort.json
|
||||
fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/window_available_guard.json
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
|
||||
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
|
||||
5969f2442a8d61ac5b1303f4cd6d8dcfe80596ef5427a513ae3addb126bf0273 ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
dbb92ddc27b8724faf709983e4feecd3f188052b2cba39daba96d4bc16914df5 ./scripts/dev.sh
|
||||
14076104c042fba1284ebb07531a6c3ff972df1f9f5b18f70da18ab774efed27 ./scripts/generate_ha_migration.py
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
3fe88e64e43d380c56955f577992ea5b0252c05280f6e9dc3cd0bbcc0bf00896 ./scripts/generate_ha_migration.py
|
||||
e4849261fd9ed1f01df96c0637c439c0c4eff8fa317b2918026167bba343af79 ./scripts/install-lxc.sh
|
||||
bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./scripts/install.sh
|
||||
50cc08e2aeff052a47c68212524f4584e9201cfc535d71803f276cdc223d5216 ./scripts/live_realtime_test.js
|
||||
e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
|
||||
bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
8295771c6907bcbc4e57d3c4572c120b72501167d02706f1fade7c1c09d6a6bf ./src/api.rs
|
||||
9040e8cb6647c76a875148b7591abcccfe4e2d4708ef462a1e5d25dd4ccac911 ./src/config.rs
|
||||
5dfda2f4dc540c502885b0cd7017dc77768684588acf528f01d1fd88f1af4aec ./src/db.rs
|
||||
15d07a2c0ad8ec6530897980787e6e9a94103bda849f07c233f6a37796d1f0b1 ./src/engine.rs
|
||||
4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs
|
||||
c6ff66da9ad08506f839ec56a869ead3abc824b5311e0ea9244517d11f7f4207 ./src/home_assistant.rs
|
||||
190b0a33431539676e5dd7796698077f16c179d42eae4501ca96a91bf797cbf8 ./src/influxdb.rs
|
||||
c67212da6a2bd5c2933ece31586ca4048b484fda3105f23a6e06e881b079d129 ./src/main.rs
|
||||
ef841f3ab27da395465ef94256dba74b6f04a24f65b248d80eb560f542ee968e ./src/models.rs
|
||||
9c735d7c475f21f2f388c21be784376f7184f0bf18af1886544ef51585a3b290 ./src/notifications.rs
|
||||
7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs
|
||||
bc03d88e5476386747ff5e32f1788cdc1fcd4bef238ab66df2dada282c348c8f ./src/protocol/gree.rs
|
||||
a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol/mod.rs
|
||||
6a1c0cab3eab80ecd254c5e486b1fba068523edb8d699c64054c17f67f4a31d0 ./src/queries.rs
|
||||
2d69811db832c90ce06035ee29665205a04e4c514a21ecae9623991a9b14d825 ./src/state.rs
|
||||
ea84f5230da97447c60dfb4f8dadd0747c92fd1f7a74d5a65d6fdb6f23cf4b6d ./src/api.rs
|
||||
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
|
||||
1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs
|
||||
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
|
||||
fffad7874465739cd6b508e9d7791387b864fa88420ce0f7e2fc933d129a2751 ./src/api/configuration.rs
|
||||
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
|
||||
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
|
||||
8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs
|
||||
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
|
||||
ca5bc21e051222dcaf0c507e84cb3e60f52a0b612a6056847fa0aee3a4d1a672 ./src/api/flows.rs
|
||||
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs
|
||||
655bc8135fbe0b8fa491666942e80817a0eabc7dea694c68c2e5ebb30c8ecf75 ./src/api/house.rs
|
||||
4ed7835ed622806124a805f54ffde7c39854f2447de0141d756d96f22446fb90 ./src/api/integrations.rs
|
||||
7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs
|
||||
9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs
|
||||
b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs
|
||||
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
|
||||
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
|
||||
7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs
|
||||
3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs
|
||||
ddd1bfc8625b66f6e5f3d24b3122ac86c25f89b0af527e3dfce9102551a8f142 ./src/api/zones.rs
|
||||
32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs
|
||||
de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs
|
||||
87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs
|
||||
a8fe3636ac79df191e2f2c9d8c1eec1e5181e048ef8c9036cfa18f84aef1a462 ./src/db/configuration.rs
|
||||
5143803ab470f287a587171f1aab96f60653e84cade523cfe104220079727ffc ./src/db/core_devices.rs
|
||||
4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs
|
||||
7bc79d9d33726fbdd33b485c8c2e839cedbb8fe29fee872611da978ed149d6a0 ./src/db/energy_history.rs
|
||||
96aac6510ddda8c18fd7e29802a937b96e8a74badf504980846c80cd7933a4c6 ./src/db/events_tokens.rs
|
||||
bba6e3fb9f4800ae06d0ca67772b06a8c389d308243306719442d0c1f2623420 ./src/db/flows.rs
|
||||
5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs
|
||||
4fc007c08304e96818ad1d78997a5b2458e31aa9c48ad68b5be2a007533924bb ./src/db/network_history.rs
|
||||
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
|
||||
28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs
|
||||
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
|
||||
0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs
|
||||
f3cc7d2cbf44180372d987d7502409547eadb1c725bcce6eeb5a26cc4277b83f ./src/engine/automations.rs
|
||||
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
|
||||
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
|
||||
315116a0dbeeba1af2f72513ea5fcc238da1b5ce9501b039c5188e44976978dc ./src/engine/control_plan.rs
|
||||
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
|
||||
519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs
|
||||
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
|
||||
4ec11426ef860811348f2bf1b458bc5713fe8a7794fb4c8045dd637652e8e696 ./src/engine/history.rs
|
||||
41b7045877771b7c352328998cb45ce2433e7f9631fad3eb1a882ab4897dc31f ./src/engine/local_thermostat.rs
|
||||
f684e6755d023478882d1689833f307f3b4cd2c66abf45f8af8315cee94d36ef ./src/engine/ownership.rs
|
||||
76c1594f31af64c9a34e2555ac015e8719d42273c687b4c85b9c8f306b3c69a2 ./src/engine/polling.rs
|
||||
c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/runtime.rs
|
||||
48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs
|
||||
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
|
||||
682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs
|
||||
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
|
||||
bf1a3840135a71ce64148559cce0fbd570ff99d31c8972cdea7447a49f6868f7 ./src/engine/tests.rs
|
||||
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
|
||||
87ab87d434a6462a2c3f25249b42c59ecb2bca6ed700ec73e95e4613349cf16f ./src/engine/zone_control.rs
|
||||
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
|
||||
d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs
|
||||
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
|
||||
da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs
|
||||
2d36405912646b30b1a1a37a7a4e18c2104ab22e865b593316a609dda884aec2 ./src/influxdb/query.rs
|
||||
67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs
|
||||
62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs
|
||||
23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs
|
||||
e7b6e49d369aaf665a8aea2464950fd795957de0ad7528a002e11c55c7983f35 ./src/models/automation.rs
|
||||
a7a35e60318a620d411ec16ab8faecd34410ccba3310f612e89c06904a16539b ./src/models/control_plan.rs
|
||||
06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs
|
||||
698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs
|
||||
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
|
||||
6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs
|
||||
1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs
|
||||
17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs
|
||||
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
|
||||
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
|
||||
bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs
|
||||
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
|
||||
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
|
||||
578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
|
||||
2029bad472fdf9f73637be6b7168e9ac79f6bdb55d2bf440d4aa254f3db9659b ./src/protocol/gree/binding.rs
|
||||
91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs
|
||||
4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs
|
||||
f4e07ca05e037e29d08abb1635f2e6d475522ce517647c422e05b749cd648ad9 ./src/protocol/gree/discovery.rs
|
||||
ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs
|
||||
1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.rs
|
||||
61d588dc055ac82f0f06fceac5236d5b4b7a0c0d2939144957a174bcbf2fa83c ./src/protocol/gree/tests.rs
|
||||
3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
|
||||
b4e285f499dd787b75259777a14e3a51a1a071c7d176bcf30c1de0933920e0dc ./src/protocol/gree_cloud.rs
|
||||
b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs
|
||||
5f70211b73215d9ae7af8fc306d15dc0b6cfbf0df9f79e34f1c785957fdf8bd4 ./src/protocol/mod.rs
|
||||
a677ce61d3d0f4cf358055ddbc343e73b452c68daed80798499147370819615e ./src/provider.rs
|
||||
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
|
||||
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
|
||||
580917b91441fe9b3276cf0cfc31536ba7954b6a86f4080347b93f9367a5b1ac ./src/queries/entities.rs
|
||||
2c5fba462b72158b04dbaaa53c8536263f8d72a1338a6c4ff06cb28a36f6ae27 ./src/queries/ha_history.rs
|
||||
7e12e8704faa7ac15a312bcc1d714ee1c6f27c1dce0972d4ccd6e5a40c69c884 ./src/queries/maintenance.rs
|
||||
7da8b73a1c8c30e2a12071062d36f5393938fde0b5ffebbe4af1891da40f3233 ./src/queries/schema.rs
|
||||
f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs
|
||||
d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
22c7882d8830101f21435069489dc976acb31fe027be8182c5bc444d3367c451 ./web/app.js
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
1d6fa9e291ebbae40a7a8b06a7cef9dbd0ee06d88ec6b664361c37cb6c1aa281 ./web/css/styles.css
|
||||
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
5900f4f892c1515178c26a8a243476010513970185009a0b3d44889e0c4c9681 ./web/index.html
|
||||
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
|
||||
db9319c0a0c6927a4f3e2b0a2a83927dd61ad6a456e9ca8fad9f3b48d1987778 ./web/styles.css
|
||||
2da305f344ba9cf2d56975fdc4c446ef47b65a117daf13e0689091bd31303f32 ./web/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||
43f33fc21bb11df0ea149fe7fbca722a57f93890e4d9bb542ced703c1311207e ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
|
||||
e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js
|
||||
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
|
||||
07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js
|
||||
5e681095a559fafbeca067024afb3705ba114f01fa562d487a5717d12dac0123 ./web/js-dynamic/dashboard.js
|
||||
930e67432f55e8476513e808edfa9a0809a8da7479307284fcc136082a4129a4 ./web/js-dynamic/entities.js
|
||||
4612c958d7b9ccf89d6d45a11c3bc7e864bbd2ceaccc16fd7d76811ba4c74196 ./web/js-dynamic/events.js
|
||||
43c78a3871cfdb0382081ec30b8d48650450c71b44d006414b161349ac1fa659 ./web/js-dynamic/flows.js
|
||||
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
|
||||
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
|
||||
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
|
||||
30cbaebb817d5d416e1a372595b647aae42c14b9cc406c4751d0ced563243b69 ./web/js-dynamic/navigation.js
|
||||
3ff9c027beb8fb033330bf3cfb3496e06fb38c42041f1ffe522c6c7af18e9e30 ./web/js-dynamic/realtime.js
|
||||
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
|
||||
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
|
||||
bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js
|
||||
672e404470bc6331aa6d658523b221659ec64222af9cc9c17ced978f725790da ./web/js-dynamic/settings.js
|
||||
489ab0208c86f7a2bb0d721106f01fb5d4f39f41e94749b440a6c58e13c4de07 ./web/js/custom-chart.js
|
||||
6ae536b18db756bf5eda9adb1b8a550d5a02bfc80f0b7da27fa146039f22e884 ./web/js/lang-init.js
|
||||
e3fc113d4c349f33f3faa32e7a46c96a85c71643f43f2e488065dc9715e27333 ./web/js/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/js/theme-init.js
|
||||
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
4051d2de7fa9858545582dc6bb5a411c980a282d0b25aa3766df88597442f94a ./Cargo.lock
|
||||
4bf5b286ffb52b22a32ef3d2d5e4dfaae600c1be01c1cac0b87c7138ecb4a129 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
9a429e8b766c66061e85ccd535cc1429fe1284d14fcf75daa362fea724e0bf85 ./README.md
|
||||
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
|
||||
3c1fce0f3097e7860f8ccf6a79749af4e7e4bd43e612c6419d91c0e620480506 ./docs/API.md
|
||||
43af7941ec4d427f9db7ab3cc6baf3d182a72dba7a6791f14094fee138324d8d ./docs/FLOW.md
|
||||
3ef573d60de5dcba3fdb4004df53ea170440cbeae2e244a1f6acfcb3255b7c78 ./docs/openapi.json
|
||||
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
|
||||
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
|
||||
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
|
||||
a4596a70e50acfe698a8ae1dcb9bebd04adba95b69781218fca7add82c17703e ./ha-addon/build.sh
|
||||
7dd2534a6f8405e640e28b7cb0e94270cc5f01fad8a972193ec254befe8a96b9 ./ha-addon/home-assistant/README.md
|
||||
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./ha-addon/home-assistant/custom_components/gree_controller/__init__.py
|
||||
3f6ef15ef58456376ac53fde7cace1ef359d6a6f7a64c5b575ad77ea6e55ccc4 ./ha-addon/home-assistant/custom_components/gree_controller/api.py
|
||||
da2a0a9390ab75cef150cd281bc329fa409d0d28f51382b36bd8de3a73325c16 ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
|
||||
5e4aef2143e81bedb5a15dd4be5c71b64a3ab448ec6d33a20851edd098e5f529 ./ha-addon/home-assistant/custom_components/gree_controller/config_flow.py
|
||||
b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/home-assistant/custom_components/gree_controller/const.py
|
||||
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
|
||||
5aea34a2adb03ed46a93199e943d7820e34825658fefc3d7c3eb5267ac9066b0 ./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
|
||||
338a42662e77f91215150851e55bbd39a5c77f5612cc77f3e037c06a6e6bdadb ./ha-addon/home-assistant/custom_components/gree_controller/switch.py
|
||||
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
|
||||
69d6cf26824f8851cdf076f87c7270d6f87717a2820b68b57a82beed53d2c736 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
d3d0e54672751a3400021ffd1fdcc83a9a903be72cff32cdc3607a61aec0a31e ./ha-addon/repository/gree-controller/README.md
|
||||
c591ab21837150e313407a34a5a3758302aaafd91cd5fcebe18bb5358b702433 ./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
|
||||
e6b93d3701d83a31506617a0e6f44ff32d8c24a52be038c8540c862b13546b50 ./ha-addon/repository/gree-controller/translations/pl.yaml
|
||||
2153b230357aab4766398577e23a7a9ff83bb135ddc86d1b3f2cbfc852efe0a1 ./ha-addon/repository/repository.yaml
|
||||
cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run.sh
|
||||
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
|
||||
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
859875af2aad9929fe6ecfb80244c8d09182345edd99663e54b04910268e902e ./lang/en.json
|
||||
7139d82eb65341e72ad978be3f57efbe04228b2101b19f792d665d146697d24f ./lang/pl.json
|
||||
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
||||
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
|
||||
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
|
||||
c5276fecc6a33c5de912e5a73b0a829d4e18c18d57b9c0ee49179ef96f133fc7 ./presets/dual_threshold_control.json
|
||||
aa190f51cb20bd0f3749c080e39b4182d8d843db51d9bf7c2315528f67b45a7e ./presets/energy_price_eco.json
|
||||
19d001dc0703dccf9c9ba3c78d866afafae94591ddb6d0d9e2d8c7703eabeea2 ./presets/frost_guard.json
|
||||
a86718e22f79769b40b363f32ec669bf77974e6a6ade081ed4b90792d5803ef7 ./presets/ha_attribute_mode_guard.json
|
||||
cc0d35fb5ad4bd52f1a8a122745bbda19922119cec48338f391f8c0619dab896 ./presets/ha_boiler_supply_boost.json
|
||||
515b2580221fb38975c7f1ac46f267af7d8ac418efd52d30b40d5ce3e4fae1d2 ./presets/ha_external_heat_source_assist.json
|
||||
07907f47c6c5c8dbd142b161135eae123889fb8845cd846fa7d1b05eda7b0740 ./presets/ha_external_heat_source_off.json
|
||||
349df3ee451da70958529bf6c8e8c09e717a26f69fb56e9d6b73831087055966 ./presets/ha_gas_backup_heat.json
|
||||
6fddede93fcb97913139791e6a75fb62322f425c59365b48bd8bb32a48c4ddee ./presets/ha_gas_heating_boost.json
|
||||
40e6bb0a22e29cc9e32844b90eafffb753df4c368ab55b5112c5bf25593491da ./presets/ha_gas_heating_off.json
|
||||
b6dce178fb517509f31e3ce58f6fa512d038ffcea98fae10dd41844b1d954144 ./presets/ha_gas_heating_reduce.json
|
||||
eb95423754fc798b0b5db01757f0fbe97f07bae9f16a95f5ca51ce379809ab7a ./presets/ha_heating_demand_follow.json
|
||||
0e34f595cebea8662646293b75e7422ac2b01c92fe75f152d71b13f6c8fb53d1 ./presets/ha_thermostat_idle_fallback.json
|
||||
e8946176bf86975c04ef40e82565d7f7a4c171bcf8b245c001121ed8aae938c9 ./presets/ha_window_guard.json
|
||||
a87a919d73003f774becc2d48aab646c21af43e50ceb54577cd37f26606a1674 ./presets/humidity_guard.json
|
||||
e78a5a9c02bc47fa6530aad3803581b10b3a0676705cb41f9d003b4da455aa14 ./presets/mild_weather_eco.json
|
||||
c3c10990e992948c2792bb71fa6606e616abb2a5455e1f11d9209c2f05132b86 ./presets/morning_boost.json
|
||||
664afa80d27130799899e54f5a2152a27955e6e2d8c035c2a594c8da6bd902a9 ./presets/multi_room_group_guard.json
|
||||
7518e0c694208c6ff69209edf8eec605fcf9e9abe8130d092c8c89524d7a24b3 ./presets/nested_guard.json
|
||||
863e557fe9c798a5aaa175f29d71752e148bb8d49910485d738bba00671d1198 ./presets/night_group.json
|
||||
c675cf84430f3c2d058c69dbfea1b5c6fcd82e927d53a301b3e517409a324f9a ./presets/night_quiet.json
|
||||
3b87def6c5fdd7f50cd9e6003bd8e0628b1365e1de8bbc414d5a65d1d7d76298 ./presets/occupancy_weather_matrix.json
|
||||
2360e1939cb46352042bd914a1a125fc98252ea80b68c89cc6ce3f5f197cc464 ./presets/offline_safe_off.json
|
||||
0f00f10c2d11984827738cd2910aa24910acbc8f36e754627a76fd5a0aeef7eb ./presets/overheat_guard.json
|
||||
f118ab957389bc4a47fe8c36b2911fb717a23970ec97d14c37a65f65eb479fc5 ./presets/peak_power_guard.json
|
||||
dfb2f5f382a7f83b7a6fe475a26bae73a4510a46e1e7949a99ac752e39f58b4b ./presets/presence_eco.json
|
||||
8e40d01a07911455c3c6607ba3435488fd6e520bebfc84cbf5eaad481883a4ed ./presets/sensor_availability_guard.json
|
||||
45183d75ce254315cd4032acd42fbb0d505df0e7ab1fefecad931a74db2f43c4 ./presets/sleep_temperature_guard.json
|
||||
7e819f2976ff48d89ccb636d08439d6e9736e7e95837c150dca157fb5bc78cb1 ./presets/smart_demand.json
|
||||
6d16f0a35b023afccd0f2011056ed8b15b5e5d2a3be6a52d122733aac862e85d ./presets/thermostat_enabled_guard.json
|
||||
fffa18bd989a0dcc1c5458975522517d40af4af40cc1ca30640e79d3db711053 ./presets/unoccupied_shutdown.json
|
||||
993b8e8151647cbda6e62312266d37d89178b3a4bb13f7df0950840e1ce647a8 ./presets/weather_comfort.json
|
||||
ae21459a261712bcb8d57594528b1648432e8d03a8a38502234829c0dbfef774 ./presets/weekend_comfort.json
|
||||
fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/window_available_guard.json
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
|
||||
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
|
||||
5969f2442a8d61ac5b1303f4cd6d8dcfe80596ef5427a513ae3addb126bf0273 ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
3fe88e64e43d380c56955f577992ea5b0252c05280f6e9dc3cd0bbcc0bf00896 ./scripts/generate_ha_migration.py
|
||||
e4849261fd9ed1f01df96c0637c439c0c4eff8fa317b2918026167bba343af79 ./scripts/install-lxc.sh
|
||||
bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./scripts/install.sh
|
||||
50cc08e2aeff052a47c68212524f4584e9201cfc535d71803f276cdc223d5216 ./scripts/live_realtime_test.js
|
||||
e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
ea84f5230da97447c60dfb4f8dadd0747c92fd1f7a74d5a65d6fdb6f23cf4b6d ./src/api.rs
|
||||
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
|
||||
1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs
|
||||
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
|
||||
fffad7874465739cd6b508e9d7791387b864fa88420ce0f7e2fc933d129a2751 ./src/api/configuration.rs
|
||||
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
|
||||
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
|
||||
8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs
|
||||
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
|
||||
ca5bc21e051222dcaf0c507e84cb3e60f52a0b612a6056847fa0aee3a4d1a672 ./src/api/flows.rs
|
||||
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs
|
||||
655bc8135fbe0b8fa491666942e80817a0eabc7dea694c68c2e5ebb30c8ecf75 ./src/api/house.rs
|
||||
4ed7835ed622806124a805f54ffde7c39854f2447de0141d756d96f22446fb90 ./src/api/integrations.rs
|
||||
7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs
|
||||
9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs
|
||||
b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs
|
||||
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
|
||||
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
|
||||
7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs
|
||||
3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs
|
||||
ddd1bfc8625b66f6e5f3d24b3122ac86c25f89b0af527e3dfce9102551a8f142 ./src/api/zones.rs
|
||||
32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs
|
||||
de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs
|
||||
87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs
|
||||
a8fe3636ac79df191e2f2c9d8c1eec1e5181e048ef8c9036cfa18f84aef1a462 ./src/db/configuration.rs
|
||||
5143803ab470f287a587171f1aab96f60653e84cade523cfe104220079727ffc ./src/db/core_devices.rs
|
||||
4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs
|
||||
7bc79d9d33726fbdd33b485c8c2e839cedbb8fe29fee872611da978ed149d6a0 ./src/db/energy_history.rs
|
||||
96aac6510ddda8c18fd7e29802a937b96e8a74badf504980846c80cd7933a4c6 ./src/db/events_tokens.rs
|
||||
bba6e3fb9f4800ae06d0ca67772b06a8c389d308243306719442d0c1f2623420 ./src/db/flows.rs
|
||||
5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs
|
||||
4fc007c08304e96818ad1d78997a5b2458e31aa9c48ad68b5be2a007533924bb ./src/db/network_history.rs
|
||||
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
|
||||
28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs
|
||||
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
|
||||
0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs
|
||||
f3cc7d2cbf44180372d987d7502409547eadb1c725bcce6eeb5a26cc4277b83f ./src/engine/automations.rs
|
||||
1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs
|
||||
357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs
|
||||
315116a0dbeeba1af2f72513ea5fcc238da1b5ce9501b039c5188e44976978dc ./src/engine/control_plan.rs
|
||||
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
|
||||
519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs
|
||||
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
|
||||
4ec11426ef860811348f2bf1b458bc5713fe8a7794fb4c8045dd637652e8e696 ./src/engine/history.rs
|
||||
41b7045877771b7c352328998cb45ce2433e7f9631fad3eb1a882ab4897dc31f ./src/engine/local_thermostat.rs
|
||||
f684e6755d023478882d1689833f307f3b4cd2c66abf45f8af8315cee94d36ef ./src/engine/ownership.rs
|
||||
76c1594f31af64c9a34e2555ac015e8719d42273c687b4c85b9c8f306b3c69a2 ./src/engine/polling.rs
|
||||
c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/runtime.rs
|
||||
48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs
|
||||
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
|
||||
682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs
|
||||
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
|
||||
bf1a3840135a71ce64148559cce0fbd570ff99d31c8972cdea7447a49f6868f7 ./src/engine/tests.rs
|
||||
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
|
||||
87ab87d434a6462a2c3f25249b42c59ecb2bca6ed700ec73e95e4613349cf16f ./src/engine/zone_control.rs
|
||||
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
|
||||
d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs
|
||||
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
|
||||
da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs
|
||||
2d36405912646b30b1a1a37a7a4e18c2104ab22e865b593316a609dda884aec2 ./src/influxdb/query.rs
|
||||
67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs
|
||||
62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs
|
||||
23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs
|
||||
e7b6e49d369aaf665a8aea2464950fd795957de0ad7528a002e11c55c7983f35 ./src/models/automation.rs
|
||||
a7a35e60318a620d411ec16ab8faecd34410ccba3310f612e89c06904a16539b ./src/models/control_plan.rs
|
||||
06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs
|
||||
698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs
|
||||
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
|
||||
6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs
|
||||
1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs
|
||||
17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs
|
||||
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
|
||||
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
|
||||
bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs
|
||||
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
|
||||
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
|
||||
578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
|
||||
2029bad472fdf9f73637be6b7168e9ac79f6bdb55d2bf440d4aa254f3db9659b ./src/protocol/gree/binding.rs
|
||||
91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs
|
||||
4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs
|
||||
f4e07ca05e037e29d08abb1635f2e6d475522ce517647c422e05b749cd648ad9 ./src/protocol/gree/discovery.rs
|
||||
ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs
|
||||
1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.rs
|
||||
61d588dc055ac82f0f06fceac5236d5b4b7a0c0d2939144957a174bcbf2fa83c ./src/protocol/gree/tests.rs
|
||||
3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
|
||||
b4e285f499dd787b75259777a14e3a51a1a071c7d176bcf30c1de0933920e0dc ./src/protocol/gree_cloud.rs
|
||||
b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs
|
||||
5f70211b73215d9ae7af8fc306d15dc0b6cfbf0df9f79e34f1c785957fdf8bd4 ./src/protocol/mod.rs
|
||||
a677ce61d3d0f4cf358055ddbc343e73b452c68daed80798499147370819615e ./src/provider.rs
|
||||
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
|
||||
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
|
||||
580917b91441fe9b3276cf0cfc31536ba7954b6a86f4080347b93f9367a5b1ac ./src/queries/entities.rs
|
||||
2c5fba462b72158b04dbaaa53c8536263f8d72a1338a6c4ff06cb28a36f6ae27 ./src/queries/ha_history.rs
|
||||
7e12e8704faa7ac15a312bcc1d714ee1c6f27c1dce0972d4ccd6e5a40c69c884 ./src/queries/maintenance.rs
|
||||
7da8b73a1c8c30e2a12071062d36f5393938fde0b5ffebbe4af1891da40f3233 ./src/queries/schema.rs
|
||||
f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs
|
||||
d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
1d6fa9e291ebbae40a7a8b06a7cef9dbd0ee06d88ec6b664361c37cb6c1aa281 ./web/css/styles.css
|
||||
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
43f33fc21bb11df0ea149fe7fbca722a57f93890e4d9bb542ced703c1311207e ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
|
||||
e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js
|
||||
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
|
||||
07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js
|
||||
5e681095a559fafbeca067024afb3705ba114f01fa562d487a5717d12dac0123 ./web/js-dynamic/dashboard.js
|
||||
930e67432f55e8476513e808edfa9a0809a8da7479307284fcc136082a4129a4 ./web/js-dynamic/entities.js
|
||||
4612c958d7b9ccf89d6d45a11c3bc7e864bbd2ceaccc16fd7d76811ba4c74196 ./web/js-dynamic/events.js
|
||||
43c78a3871cfdb0382081ec30b8d48650450c71b44d006414b161349ac1fa659 ./web/js-dynamic/flows.js
|
||||
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
|
||||
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
|
||||
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
|
||||
30cbaebb817d5d416e1a372595b647aae42c14b9cc406c4751d0ced563243b69 ./web/js-dynamic/navigation.js
|
||||
3ff9c027beb8fb033330bf3cfb3496e06fb38c42041f1ffe522c6c7af18e9e30 ./web/js-dynamic/realtime.js
|
||||
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
|
||||
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
|
||||
bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js
|
||||
672e404470bc6331aa6d658523b221659ec64222af9cc9c17ced978f725790da ./web/js-dynamic/settings.js
|
||||
489ab0208c86f7a2bb0d721106f01fb5d4f39f41e94749b440a6c58e13c4de07 ./web/js/custom-chart.js
|
||||
6ae536b18db756bf5eda9adb1b8a550d5a02bfc80f0b7da27fa146039f22e884 ./web/js/lang-init.js
|
||||
e3fc113d4c349f33f3faa32e7a46c96a85c71643f43f2e488065dc9715e27333 ./web/js/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/js/theme-init.js
|
||||
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
|
||||
@@ -1,5 +1,8 @@
|
||||
use serde_json::{json, Value};
|
||||
use std::{env, fs, path::PathBuf};
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
fn required_string<'a>(meta: &'a Value, key: &str, file: &str) -> &'a str {
|
||||
meta.get(key)
|
||||
@@ -8,10 +11,125 @@ fn required_string<'a>(meta: &'a Value, key: &str, file: &str) -> &'a str {
|
||||
.unwrap_or_else(|| panic!("{file}: meta.{key} must be a non-empty string"))
|
||||
}
|
||||
|
||||
fn has_text(value: Option<&Value>) -> bool {
|
||||
match value {
|
||||
Some(Value::String(text)) => !text.trim().is_empty(),
|
||||
Some(Value::Object(items)) => items
|
||||
.values()
|
||||
.any(|item| item.as_str().is_some_and(|text| !text.trim().is_empty())),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn fnv1a_update(mut hash: u64, bytes: &[u8]) -> u64 {
|
||||
for byte in bytes {
|
||||
hash ^= u64::from(*byte);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
fn content_hash(bytes: &[u8]) -> String {
|
||||
format!("{:016x}", fnv1a_update(0xcbf29ce484222325, bytes))
|
||||
}
|
||||
|
||||
const APP_JS_MODULES: &[&str] = &[
|
||||
"core.js",
|
||||
"select-ui.js",
|
||||
"forms.js",
|
||||
"bootstrap.js",
|
||||
"dashboard.js",
|
||||
"entities.js",
|
||||
"flows.js",
|
||||
"settings-ui.js",
|
||||
"router.js",
|
||||
"navigation.js",
|
||||
"charts.js",
|
||||
"history.js",
|
||||
"realtime.js",
|
||||
"events.js",
|
||||
"settings.js",
|
||||
"main.js",
|
||||
];
|
||||
|
||||
fn bundle_app_js(web_dir: &Path) -> String {
|
||||
let js_dir = web_dir.join("js-dynamic");
|
||||
println!("cargo:rerun-if-changed={}", js_dir.display());
|
||||
|
||||
let mut bundle = String::new();
|
||||
for &filename in APP_JS_MODULES {
|
||||
let path = js_dir.join(filename);
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
let source = fs::read_to_string(&path)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
|
||||
bundle.push_str(&source);
|
||||
if !source.ends_with('\n') {
|
||||
bundle.push('\n');
|
||||
}
|
||||
}
|
||||
bundle
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
|
||||
let lang_dir = manifest_dir.join("lang");
|
||||
let preset_dir = manifest_dir.join("presets");
|
||||
let web_dir = manifest_dir.join("web");
|
||||
println!("cargo:rerun-if-changed={}", lang_dir.display());
|
||||
println!("cargo:rerun-if-changed={}", preset_dir.display());
|
||||
|
||||
let theme_init_path = web_dir.join("js/theme-init.js");
|
||||
let lang_init_path = web_dir.join("js/lang-init.js");
|
||||
let styles_path = web_dir.join("css/styles.css");
|
||||
let index_path = web_dir.join("index.html");
|
||||
let not_found_path = web_dir.join("404.html");
|
||||
let sw_path = web_dir.join("js/sw.js");
|
||||
let manifest_path = web_dir.join("manifest.webmanifest");
|
||||
let favicon_path = web_dir.join("favicon.svg");
|
||||
for path in [
|
||||
&theme_init_path,
|
||||
&lang_init_path,
|
||||
&styles_path,
|
||||
&index_path,
|
||||
¬_found_path,
|
||||
&sw_path,
|
||||
&manifest_path,
|
||||
&favicon_path,
|
||||
] {
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
}
|
||||
|
||||
let app_js = bundle_app_js(&web_dir);
|
||||
let app_js_hash = content_hash(app_js.as_bytes());
|
||||
let theme_init_bytes = fs::read(&theme_init_path)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", theme_init_path.display()));
|
||||
let lang_init_bytes = fs::read(&lang_init_path)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", lang_init_path.display()));
|
||||
let styles_bytes = fs::read(&styles_path)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", styles_path.display()));
|
||||
let theme_init_hash = content_hash(&theme_init_bytes);
|
||||
let lang_init_hash = content_hash(&lang_init_bytes);
|
||||
let styles_hash = content_hash(&styles_bytes);
|
||||
let app_js_asset_path = format!("/app-{}.js", &app_js_hash[..12]);
|
||||
let theme_init_asset_path = format!("/theme-init-{}.js", &theme_init_hash[..12]);
|
||||
let lang_init_asset_path = format!("/lang-init-{}.js", &lang_init_hash[..12]);
|
||||
let styles_asset_path = format!("/styles-{}.css", &styles_hash[..12]);
|
||||
|
||||
let mut build_hash = fnv1a_update(0xcbf29ce484222325u64, app_js.as_bytes());
|
||||
for path in [
|
||||
&theme_init_path,
|
||||
&lang_init_path,
|
||||
&styles_path,
|
||||
&index_path,
|
||||
¬_found_path,
|
||||
&sw_path,
|
||||
&manifest_path,
|
||||
&favicon_path,
|
||||
] {
|
||||
let bytes = fs::read(path)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
|
||||
build_hash = fnv1a_update(build_hash, &bytes);
|
||||
}
|
||||
|
||||
let mut files: Vec<PathBuf> = fs::read_dir(&lang_dir)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", lang_dir.display()))
|
||||
@@ -25,31 +143,47 @@ fn main() {
|
||||
panic!("no language files found in {}", lang_dir.display());
|
||||
}
|
||||
|
||||
let default_language = "en";
|
||||
let mut assets = Vec::new();
|
||||
let mut manifest_languages = Vec::new();
|
||||
let mut has_english = false;
|
||||
let mut has_default_language = false;
|
||||
|
||||
for path in files {
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
let filename = path.file_name().and_then(|v| v.to_str()).expect("UTF-8 language filename");
|
||||
let stem = path.file_stem().and_then(|v| v.to_str()).expect("UTF-8 language code");
|
||||
if !stem.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_') {
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|v| v.to_str())
|
||||
.expect("UTF-8 language filename");
|
||||
let stem = path
|
||||
.file_stem()
|
||||
.and_then(|v| v.to_str())
|
||||
.expect("UTF-8 language code");
|
||||
if !stem
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
|
||||
{
|
||||
panic!("{filename}: filename may only contain ASCII letters, digits, '-' and '_'");
|
||||
}
|
||||
|
||||
let source = fs::read_to_string(&path)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
|
||||
build_hash = fnv1a_update(build_hash, source.as_bytes());
|
||||
let document: Value = serde_json::from_str(&source)
|
||||
.unwrap_or_else(|error| panic!("{filename}: invalid JSON: {error}"));
|
||||
let root_object = document.as_object().unwrap_or_else(|| panic!("{filename}: language pack root must be an object"));
|
||||
let unexpected: Vec<&str> = root_object.keys()
|
||||
let root_object = document
|
||||
.as_object()
|
||||
.unwrap_or_else(|| panic!("{filename}: language pack root must be an object"));
|
||||
let unexpected: Vec<&str> = root_object
|
||||
.keys()
|
||||
.filter(|key| key.as_str() != "meta" && key.as_str() != "translations")
|
||||
.map(|key| key.as_str())
|
||||
.collect();
|
||||
if !unexpected.is_empty() {
|
||||
panic!("{filename}: unexpected top-level keys {:?}; translation keys must be inside 'translations'", unexpected);
|
||||
}
|
||||
let meta = document.get("meta").unwrap_or_else(|| panic!("{filename}: missing meta object"));
|
||||
let meta = document
|
||||
.get("meta")
|
||||
.unwrap_or_else(|| panic!("{filename}: missing meta object"));
|
||||
let code = required_string(meta, "code", filename);
|
||||
let name = required_string(meta, "name", filename);
|
||||
let native_name = required_string(meta, "native_name", filename);
|
||||
@@ -60,32 +194,140 @@ fn main() {
|
||||
if !document.get("translations").is_some_and(Value::is_object) {
|
||||
panic!("{filename}: translations must be a JSON object");
|
||||
}
|
||||
if code == "en" {
|
||||
has_english = true;
|
||||
if code == default_language {
|
||||
has_default_language = true;
|
||||
}
|
||||
|
||||
let language_hash = content_hash(source.as_bytes());
|
||||
manifest_languages.push(json!({
|
||||
"code": code,
|
||||
"name": name,
|
||||
"native_name": native_name,
|
||||
"locale": locale,
|
||||
"path": format!("lang/{code}.json")
|
||||
"path": format!("lang/{code}.json?v={}", &language_hash[..12])
|
||||
}));
|
||||
assets.push((code.to_owned(), path));
|
||||
}
|
||||
|
||||
if !has_english {
|
||||
panic!("lang/en.json is required as the default fallback language");
|
||||
if !has_default_language {
|
||||
panic!("lang/{default_language}.json is required as the default fallback language");
|
||||
}
|
||||
|
||||
let mut preset_files: Vec<PathBuf> = fs::read_dir(&preset_dir)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", preset_dir.display()))
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("json"))
|
||||
.collect();
|
||||
preset_files.sort();
|
||||
if preset_files.is_empty() {
|
||||
panic!("no flow preset files found in {}", preset_dir.display());
|
||||
}
|
||||
|
||||
let mut preset_assets = Vec::new();
|
||||
let mut preset_manifest = Vec::new();
|
||||
for path in preset_files {
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.expect("UTF-8 preset filename");
|
||||
let stem = path
|
||||
.file_stem()
|
||||
.and_then(|value| value.to_str())
|
||||
.expect("UTF-8 preset id");
|
||||
if !stem
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
|
||||
{
|
||||
panic!("{filename}: filename may only contain ASCII letters, digits, '-' and '_'");
|
||||
}
|
||||
let source = fs::read_to_string(&path)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
|
||||
build_hash = fnv1a_update(build_hash, source.as_bytes());
|
||||
let document: Value = serde_json::from_str(&source)
|
||||
.unwrap_or_else(|error| panic!("{filename}: invalid JSON: {error}"));
|
||||
let object = document
|
||||
.as_object()
|
||||
.unwrap_or_else(|| panic!("{filename}: preset root must be an object"));
|
||||
let id = object
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| panic!("{filename}: id must be a non-empty string"));
|
||||
if id != stem {
|
||||
panic!("{filename}: id '{id}' must match filename '{stem}.json'");
|
||||
}
|
||||
let category = object
|
||||
.get("category")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| panic!("{filename}: category must be a non-empty string"));
|
||||
if !has_text(object.get("name")) {
|
||||
panic!("{filename}: name must be a string or localized object with text");
|
||||
}
|
||||
if !has_text(object.get("description")) {
|
||||
panic!("{filename}: description must be a string or localized object with text");
|
||||
}
|
||||
let flow = object
|
||||
.get("flow")
|
||||
.and_then(Value::as_object)
|
||||
.unwrap_or_else(|| panic!("{filename}: flow must be an object"));
|
||||
if !flow.get("nodes").is_some_and(Value::is_array)
|
||||
|| !flow.get("edges").is_some_and(Value::is_array)
|
||||
{
|
||||
panic!("{filename}: flow.nodes and flow.edges must be arrays");
|
||||
}
|
||||
preset_manifest.push(json!({
|
||||
"id": id,
|
||||
"category": category,
|
||||
"file": filename,
|
||||
}));
|
||||
preset_assets.push((filename.to_owned(), path));
|
||||
}
|
||||
|
||||
let manifest_json = serde_json::to_string(&json!({
|
||||
"default": "en",
|
||||
"default": default_language,
|
||||
"languages": manifest_languages
|
||||
})).expect("serialize language manifest");
|
||||
}))
|
||||
.expect("serialize language manifest");
|
||||
|
||||
let preset_manifest_json = serde_json::to_string(&json!({
|
||||
"version": 1,
|
||||
"presets": preset_manifest,
|
||||
}))
|
||||
.expect("serialize preset manifest");
|
||||
|
||||
let mut generated = String::new();
|
||||
generated.push_str("// @generated by build.rs - do not edit.\n");
|
||||
generated.push_str(&format!("pub const LANGUAGE_MANIFEST_JSON: &str = {:?};\n", manifest_json));
|
||||
generated.push_str(&format!(
|
||||
"pub const APP_JS_ASSET_PATH: &str = {:?};\n",
|
||||
app_js_asset_path
|
||||
));
|
||||
generated.push_str(&format!(
|
||||
"pub const THEME_INIT_ASSET_PATH: &str = {:?};\n",
|
||||
theme_init_asset_path
|
||||
));
|
||||
generated.push_str(&format!(
|
||||
"pub const LANG_INIT_ASSET_PATH: &str = {:?};\n",
|
||||
lang_init_asset_path
|
||||
));
|
||||
generated.push_str(&format!(
|
||||
"pub const STYLES_CSS_ASSET_PATH: &str = {:?};\n",
|
||||
styles_asset_path
|
||||
));
|
||||
generated.push_str(&format!(
|
||||
"pub const ASSET_BUILD_ID: &str = \"{:016x}\";\n",
|
||||
build_hash
|
||||
));
|
||||
generated.push_str(&format!(
|
||||
"pub const LANGUAGE_MANIFEST_JSON: &str = {:?};\n",
|
||||
manifest_json
|
||||
));
|
||||
generated.push_str(&format!(
|
||||
"pub const DEFAULT_LANGUAGE_CODE: &str = {:?};\n",
|
||||
default_language
|
||||
));
|
||||
generated.push_str("pub const LANGUAGE_ASSETS: &[(&str, &str)] = &[\n");
|
||||
for (code, path) in assets {
|
||||
generated.push_str(&format!(
|
||||
@@ -95,7 +337,21 @@ fn main() {
|
||||
));
|
||||
}
|
||||
generated.push_str("];\n");
|
||||
generated.push_str(&format!(
|
||||
"pub const PRESET_MANIFEST_JSON: &str = {:?};\n",
|
||||
preset_manifest_json
|
||||
));
|
||||
generated.push_str("pub const PRESET_ASSETS: &[(&str, &str)] = &[\n");
|
||||
for (filename, path) in preset_assets {
|
||||
generated.push_str(&format!(
|
||||
" ({:?}, include_str!({:?})),\n",
|
||||
filename,
|
||||
path.to_string_lossy()
|
||||
));
|
||||
}
|
||||
generated.push_str("];\n");
|
||||
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
|
||||
fs::write(out_dir.join("app.bundle.js"), app_js).expect("write generated app.bundle.js");
|
||||
fs::write(out_dir.join("languages.rs"), generated).expect("write generated languages.rs");
|
||||
}
|
||||
|
||||
+1685
-211
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
# Visual Flow architecture
|
||||
|
||||
Flow is an authoring/orchestration layer. It does not replace the thermostat, schedule, group or device engines.
|
||||
|
||||
## Compilation
|
||||
|
||||
- `weekday + time_range -> zone_thermostat` with schedule-compatible settings compiles to a native `Schedule`; selecting a vertical or horizontal louver position makes the action compile as an `Automation` so the louver command is not lost.
|
||||
- Richer DAGs compile to native `Automation` rows with `trigger_kind = "flow"`.
|
||||
- Generated rows keep `flow_id` and `flow_node_id`, use stable `flow-<stable-unique-id>` names, and are read-only in the legacy editors.
|
||||
- The Flow source graph plus all of its generated schedule/automation rows are replaced in one SQLite transaction.
|
||||
- Flow updates require `expected_revision`; stale editor tabs receive HTTP 409.
|
||||
- Work-in-progress graphs may be persisted with `draft=true`. Drafts are forced disabled, compile to zero schedules/automations, and can contain unfinished semantic wiring such as a graph without an action. Missing node references and duplicate/empty IDs are still rejected so the editor document remains structurally safe.
|
||||
- Saving a previously executable Flow as a draft atomically deletes its generated outputs, so no stale task can keep running. Completing and saving it normally clears draft status and recompiles it.
|
||||
|
||||
## Existing control domains remain authoritative
|
||||
|
||||
- `zone_thermostat` writes the existing persistent Zone intent and wakes the normal thermostat cycle. It can also issue one-shot granular vertical/horizontal louver commands to the assigned unit; these auxiliary fields do not take over thermostat ownership.
|
||||
- `group_action` calls the existing `control_group` path, including custom group targets.
|
||||
- `device_action` uses the existing `DeviceCommand` / automatic-device path.
|
||||
- `device_feature_action` uses the same path but compiles exactly one selected `DeviceCommand` field, leaving all other unit settings untouched.
|
||||
- Thermostat hysteresis, compressor protection, schedule hand-back, manual/local ownership and Temporary Quick Thermostat keep their existing priority.
|
||||
- GREE fan/quiet/sleep and dry/fan HVAC modes are suppressed on thermostat-assigned devices when they would fight the regulator. Device-only features such as light/turbo/louver/air/xfan/health remain available through the normal command path.
|
||||
|
||||
## Concurrency invariants
|
||||
|
||||
Structural Flow writes use the global order:
|
||||
|
||||
`configuration -> automation -> schedule -> thermostat-cycle`
|
||||
|
||||
Thermostat/device actions then use:
|
||||
|
||||
`automation -> schedule -> thermostat-cycle -> zone -> device`
|
||||
|
||||
Group actions use the existing group order:
|
||||
|
||||
`automation -> house -> thermostat-cycle -> group -> zone(s) -> device`
|
||||
|
||||
The control loop runs thermostat arbitration and automation arbitration sequentially. Automation execution reloads the row after acquiring the automation lock and skips it when `updated_at` changed. Same-cycle due automations deterministically claim target devices and Home Assistant entity IDs so two rules cannot issue conflicting commands to the same target in one pass. Polling and physical commands share per-zone/per-device locks.
|
||||
|
||||
Holding the automation lock across the physical action is deliberate: Flow cannot be deleted/recompiled while an already-selected action is in flight. Do not shorten this ownership window without adding an explicit execution-generation/token mechanism.
|
||||
|
||||
External sensor values can naturally change between condition sampling and action execution. This is normal sampled-control semantics, not a persistent-state race; safety/ownership is rechecked at the action boundary.
|
||||
|
||||
## Home Assistant safety
|
||||
|
||||
`ha_state`, `ha_numeric`, `ha_attribute` and `ha_available` fail closed. Connection failures, missing attributes and non-numeric numeric states evaluate to `false` and appear as an error object in dry-run traces. This prevents `NOT` / `neq` branches from becoming true merely because Home Assistant is unavailable.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
The Flow editor exposes:
|
||||
|
||||
- dry-run at an arbitrary date/time,
|
||||
- per-source overrides,
|
||||
- per-node trace,
|
||||
- `would_execute` and ownership/block reason,
|
||||
- Flow-scoped execution/dry-run logs,
|
||||
- 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.
|
||||
|
||||
## Blocks
|
||||
|
||||
Condition/source blocks include weekday, time range, date range, optional 5-field CRON triggers, application Night mode, outdoor/device/zone temperature, house mode, device state, thermostat/zone state, group state, Home Assistant state/numeric/attribute/availability, rolling mean/median windows, numeric oscillation detection, a diagnostic constant, and `shared_input`. Stateful gates include `stable_for`, bounded `state_duration`, `on_change` (condition-result or raw-value edge), `rate_limit` (maximum successful executions in a rolling period), and `delay`. Their runtime state, together with rolling statistics and oscillation samples, is persisted in the generated automation payload so restarts do not silently reset timing/history. Editing a stateful block resets only that block's incompatible runtime state. Flow state continues to be observed while the action itself is in cooldown. `on_change` latches a detected event while its upstream condition remains true and clears it only after a successful downstream action, so cooldown, ownership suppression or same-cycle target arbitration cannot silently consume the edge. Rolling mean/median blocks wait for one complete configured time window before matching and restart that warm-up after their source becomes unavailable. A `rate_limit` block must be placed directly before an action and consumes quota only after a successful execution; suppressed or failed actions do not consume it. 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, full GREE device command, single GREE function, climate group, and generic Home Assistant service calls (`domain`, `service`, optional `entity_id`, JSON service data). Direct GREE actions expose power, HVAC mode, target temperature, fan speed, vertical/horizontal louver position, quiet, turbo, light, air, xfan, health and sleep. The single-function block writes exactly one of those fields.
|
||||
|
||||
## Built-in presets
|
||||
|
||||
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. The Flow editor keeps the natural-language Interpretation / Flow cycle panel collapsed by default on every viewport and exposes it through a disclosure button. Home Assistant-backed shared inputs can be tested live in the input modal. Shared inputs report the current raw value/attribute. Each Flow reference defines its own comparison without changing the shared source. Home Assistant temperature entities remain normal HA entities: room-temperature sensors are configured per zone, while outdoor temperature has one global `outdoor_entity_id` plus an optional per-zone `ha_outdoor_entity_id` override. Their aliases are shared with the rest of the UI, outdoor overrides are recorded in sensor metrics/history, and all configured entities are offered as suggestions in Home Assistant Flow blocks; selecting a zone sensor does not create a separate shared input.
|
||||
|
||||
Presets are data files, not JavaScript definitions. Every preset lives in `presets/<id>.json` and contains `id`, `category`, localized `name`, localized `description`, and the complete `flow` graph (`nodes` + `edges`). `build.rs` validates and embeds the directory, exposes `/presets/index.json` plus `/presets/<file>.json`, and the Flow editor loads that library dynamically. Device/zone/group placeholders such as `$zone1`, `$zone2`, `$device1` and `$group1` are resolved when a preset is applied. Adding a preset therefore does not require adding graph-building code to `web/js-dynamic/flows.js`.
|
||||
|
||||
The canvas supports drag-box multi-selection, additive Shift/Ctrl/Cmd selection, `Ctrl/Cmd+A`, Select all / Clear selection toolbar actions, and dragging the whole selected group while preserving relative positions. Selected blocks can be duplicated from the toolbar or with `Ctrl/Cmd+D`; `Ctrl/Cmd+C`, `Ctrl/Cmd+X` and `Ctrl/Cmd+V` copy/cut/paste the selected subgraph and preserve connections whose endpoints are both selected. Keyboard-first authoring also supports `A`, `Insert` or `Ctrl/Cmd+K` to open block search, `Enter` to insert the first matching block, `Ctrl/Cmd+S` to save, `Ctrl/Cmd+Z` to undo graph/setting edits, Delete/Backspace to remove selected blocks, Escape to clear selection/cancel a pending connection, arrow keys to move the selection (Shift for 1 px precision), `Ctrl/Cmd+0` to fit, `Ctrl/Cmd++/-` to zoom and `?` to open the built-in shortcut reference. Shortcut success messages are shown as a small status inside the Flow editor instead of global toasts. Right-click opens a context menu: blocks expose copy/cut/duplicate/delete plus paste/add, blank canvas exposes undo/add/paste/select-all/fit, and connections can be removed directly. Context-menu paste on blank canvas places the copied subgraph at the clicked position.
|
||||
|
||||
## Good next stateful blocks
|
||||
|
||||
Stateful timing now has persisted runtime support for `stable_for`, bounded state duration, change detection, rolling execution limits, delay-before-action, rolling mean/median samples and oscillation detection. Useful next additions on the same runtime-state foundation are:
|
||||
|
||||
- debounce with configurable dead time,
|
||||
- a dedicated external-entity thermostat/hysteresis action (rather than duplicating the built-in HVAC thermostat),
|
||||
- explicit rising/falling edge modes,
|
||||
- retry/backoff for external service failures,
|
||||
- variables/counters,
|
||||
- reusable subflows/macros,
|
||||
- explicit priority/mutex groups for actions,
|
||||
- trace replay from historical sensor samples.
|
||||
|
||||
These need durable state and restart semantics before they should be exposed in the editor.
|
||||
|
||||
### Display name / alias
|
||||
|
||||
The Flow name edited in `/flows` is a presentation alias. Renaming it changes labels shown in the UI (including the dashboard) but does not change the stable Flow ID or generated `flow-<unique-id>` schedule/automation names.
|
||||
@@ -1,67 +0,0 @@
|
||||
# Home Assistant entity-ID migration
|
||||
|
||||
## Goal
|
||||
|
||||
Replace an existing GREE climate entity while preserving its `entity_id`.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Before: climate.klima_salon -> default GREE integration -> AC
|
||||
After: climate.klima_salon -> GREE Controller integration -> Rust controller -> AC
|
||||
```
|
||||
|
||||
Keeping the same entity ID allows existing dashboards, scripts, scenes and automations that reference the entity by ID to continue working.
|
||||
|
||||
## Why a takeover step is required
|
||||
|
||||
Home Assistant's entity registry reserves entity IDs. A second integration cannot create another active `climate.klima_salon` while the original entity still exists. The new integration therefore checks for conflicts and stops setup rather than allowing HA to generate a suffixed name such as `climate.klima_salon_2`.
|
||||
|
||||
## Generate the mapping
|
||||
|
||||
For one entity:
|
||||
|
||||
```bash
|
||||
./scripts/generate_ha_migration.py \
|
||||
--entity climate.klima_salon \
|
||||
--device gree-aabbccddeeff
|
||||
```
|
||||
|
||||
For several:
|
||||
|
||||
```bash
|
||||
./scripts/generate_ha_migration.py \
|
||||
--map climate.klima_salon=gree-aabbccddeeff \
|
||||
--map climate.klima_sypialnia=gree-112233445566
|
||||
```
|
||||
|
||||
Generated structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"entities": [
|
||||
{
|
||||
"entity_id": "climate.klima_salon",
|
||||
"device_id": "gree-aabbccddeeff"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Copy it to `/config/gree_controller_entities.json` on the Home Assistant host.
|
||||
|
||||
## Migration sequence
|
||||
|
||||
1. Add the physical AC to the standalone Rust application.
|
||||
2. Test power, mode and target temperature from its web UI.
|
||||
3. Generate the HA entity mapping.
|
||||
4. Copy the custom component to `/config/custom_components/gree_controller/`.
|
||||
5. Disable/remove the previous GREE integration in HA so it no longer controls or publishes the old climate entity.
|
||||
6. Remove any stale entity registry entry only after the old integration is unloaded.
|
||||
7. Add the **GREE Controller** integration and provide its URL/token.
|
||||
8. Confirm the exact old entity ID is present again.
|
||||
9. Test `climate.set_temperature`, HVAC modes and power from HA.
|
||||
10. Verify automations and dashboards that use the preserved entity ID.
|
||||
|
||||
The standalone controller remains available during the HA migration, so the AC can still be controlled from its own web UI if HA is restarting.
|
||||
@@ -1,71 +0,0 @@
|
||||
# Localization
|
||||
|
||||
The web interface uses JSON language packs from `lang/`. Language files are discovered at Rust build time and embedded in the application binary.
|
||||
|
||||
English (`lang/en.json`) is required and is always the fallback language. Polish (`lang/pl.json`) is included by default.
|
||||
|
||||
## Add a language
|
||||
|
||||
1. Copy `lang/en.json` to a file named with the new language code, for example `lang/de.json`.
|
||||
2. Update the `meta` object.
|
||||
3. Translate values inside `translations`. Do not rename translation keys.
|
||||
4. Run `./scripts/dev.sh --check` or build the project again.
|
||||
5. Start the rebuilt binary. The new language appears automatically in the language selector.
|
||||
|
||||
Example structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"meta": {
|
||||
"code": "de",
|
||||
"name": "German",
|
||||
"native_name": "Deutsch",
|
||||
"locale": "de-DE"
|
||||
},
|
||||
"translations": {
|
||||
"controls.language": "Sprache",
|
||||
"controls.theme": "Darstellung"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The filename and `meta.code` must match (`de.json` -> `"code": "de"`). File names may contain ASCII letters, digits, `-` and `_` only.
|
||||
|
||||
## Fallback behavior
|
||||
|
||||
A language pack does not have to duplicate every English key while it is being developed. If a key is missing from the selected language, the UI uses the value from `en.json`. If a key is also missing from English, the translation key itself is shown, which makes incomplete strings visible during development.
|
||||
|
||||
## Build-time validation
|
||||
|
||||
`build.rs` checks that:
|
||||
|
||||
- at least one JSON language file exists,
|
||||
- `en.json` exists,
|
||||
- every language file contains valid JSON,
|
||||
- every file has `meta.code`, `meta.name`, `meta.native_name` and `meta.locale`,
|
||||
- `meta.code` matches the filename,
|
||||
- `translations` is a JSON object.
|
||||
|
||||
A malformed language pack fails the Rust build instead of producing a broken selector at runtime.
|
||||
|
||||
## Runtime endpoints
|
||||
|
||||
The embedded language catalog is available at:
|
||||
|
||||
```text
|
||||
GET /lang/index.json
|
||||
```
|
||||
|
||||
Individual embedded packs are available at:
|
||||
|
||||
```text
|
||||
GET /lang/en.json
|
||||
GET /lang/pl.json
|
||||
GET /lang/<code>.json
|
||||
```
|
||||
|
||||
These endpoints are intentionally public so that localization also works before API authentication is completed.
|
||||
|
||||
## Browser preference
|
||||
|
||||
The selected language code is stored for one year in the `gree_controller_language` cookie. If the stored language is no longer present in a later build, the UI falls back to English.
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
# LXC installation and update
|
||||
|
||||
This document describes the supported Debian/Ubuntu systemd deployment used for LXC testing.
|
||||
|
||||
## First installation
|
||||
|
||||
Unpack the source archive inside the container and run:
|
||||
|
||||
```bash
|
||||
cd gree-controller
|
||||
chmod +x scripts/*.sh
|
||||
sudo ./scripts/install.sh
|
||||
```
|
||||
|
||||
The installer:
|
||||
|
||||
1. installs required build packages when missing,
|
||||
2. installs stable Rust with rustup when Cargo is unavailable,
|
||||
3. runs `cargo test --all-targets`,
|
||||
4. builds `target/release/gree-controller`,
|
||||
5. creates the `gree-controller` system user/group,
|
||||
6. creates `/var/lib/gree-controller`, `/opt/gree-controller` and `/var/backups/gree-controller`,
|
||||
7. installs the systemd unit,
|
||||
8. creates `/etc/gree-controller.env` only when it does not already exist,
|
||||
9. enables/restarts the service,
|
||||
10. verifies `GET /api/health`.
|
||||
|
||||
Installed state is intentionally outside the extracted source directory:
|
||||
|
||||
```text
|
||||
/opt/gree-controller/gree-controller installed binary
|
||||
/etc/gree-controller.env persistent configuration/secrets
|
||||
/var/lib/gree-controller/gree-controller.db persistent SQLite database
|
||||
/var/backups/gree-controller/ update backups
|
||||
/etc/systemd/system/gree-controller.service systemd service
|
||||
```
|
||||
|
||||
The default installation starts in simulator mode. Change `GREE_CONTROLLER_SIMULATE=false` and `GREE_CONTROLLER_AUTO_SEED=false` in `/etc/gree-controller.env` when moving to physical units, then restart the service.
|
||||
|
||||
## Update
|
||||
|
||||
Unpack a newer source archive and run from that new directory:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/update.sh
|
||||
```
|
||||
|
||||
The update workflow is designed to minimize downtime and preserve the database:
|
||||
|
||||
1. build dependencies/Rust are checked,
|
||||
2. tests run while the currently installed controller stays online,
|
||||
3. the new release binary is built while the old service stays online,
|
||||
4. the service is stopped,
|
||||
5. the installed binary, service unit, environment file and SQLite database are copied to `/var/backups/gree-controller/<UTC timestamp>/`,
|
||||
6. the new binary/unit is installed,
|
||||
7. systemd starts the new version,
|
||||
8. `/api/health` is checked,
|
||||
9. on failure the old binary, unit and database are restored automatically.
|
||||
|
||||
The updater does not overwrite `/etc/gree-controller.env` during a successful update.
|
||||
|
||||
Use `--skip-tests` only for deliberate fast testing:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/update.sh --skip-tests
|
||||
```
|
||||
|
||||
## Service helper
|
||||
|
||||
```bash
|
||||
./scripts/service.sh status
|
||||
./scripts/service.sh health
|
||||
./scripts/service.sh logs
|
||||
sudo ./scripts/service.sh restart
|
||||
sudo ./scripts/service.sh stop
|
||||
sudo ./scripts/service.sh start
|
||||
```
|
||||
|
||||
Equivalent native commands remain available:
|
||||
|
||||
```bash
|
||||
systemctl status gree-controller
|
||||
journalctl -u gree-controller -f
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit:
|
||||
|
||||
```text
|
||||
/etc/gree-controller.env
|
||||
```
|
||||
|
||||
and restart:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/service.sh restart
|
||||
```
|
||||
|
||||
Do not store controller or Home Assistant secrets in the source tree.
|
||||
|
||||
## Database and SQL layout
|
||||
|
||||
Runtime persistence uses SQLite. All schema definitions and SQL statements in the Rust application are centralized in:
|
||||
|
||||
```text
|
||||
src/queries.rs
|
||||
```
|
||||
|
||||
`src/db.rs` contains connection/transaction logic and maps database rows to Rust domain models, but it does not embed SQL statements.
|
||||
|
||||
|
||||
## Two network interfaces
|
||||
|
||||
When the LXC has a management interface and a dedicated GREE network, configure the service environment explicitly. Example for `eth1` on `10.87.65.0/25`:
|
||||
|
||||
```env
|
||||
GREE_CONTROLLER_GREE_INTERFACE=eth1
|
||||
GREE_CONTROLLER_DISCOVERY_BROADCAST=auto
|
||||
|
||||
This explicit interface configuration is recommended for predictable LXC deployments. Version 0.3.7 also automatically selects the directly connected local IPv4 address for a GREE device when the interface variable is omitted; for example, a target in `10.87.65.0/25` selects the local address on that subnet.
|
||||
GREE_CONTROLLER_SIMULATE=false
|
||||
GREE_CONTROLLER_AUTO_SEED=false
|
||||
```
|
||||
|
||||
Restart the service after editing `/etc/gree-controller.env`:
|
||||
|
||||
```bash
|
||||
systemctl restart gree-controller
|
||||
journalctl -u gree-controller -n 100 --no-pager
|
||||
```
|
||||
|
||||
During discovery the log should contain a line similar to:
|
||||
|
||||
```text
|
||||
Starting GREE discovery target=10.87.65.127:7000 local=10.87.65.27:<ephemeral> interface=eth1
|
||||
```
|
||||
|
||||
Use `scripts/network-debug.sh` for routing diagnostics.
|
||||
|
||||
|
||||
## Mixed GREE model generations
|
||||
|
||||
Version 0.3.7 can discover both AES-ECB and AES-GCM modules. In the Web UI choose **Discover -> Auto (V1 + V2)** and use 3-5 scan passes. If a family is still missing, repeat with V1-only and V2-only to see which protocol its Wi-Fi module answers with.
|
||||
|
||||
A single command/status timeout no longer immediately flips a device offline; offline requires three consecutive communication failures.
|
||||
|
||||
## Multi-NIC LXC and AF_NETLINK
|
||||
|
||||
When `GREE_CONTROLLER_GREE_INTERFACE` is set, the controller enumerates IPv4 addresses with Linux `getifaddrs()`. On Linux this requires a Netlink socket. The systemd sandbox therefore allows `AF_NETLINK` in addition to `AF_UNIX`, `AF_INET`, and `AF_INET6`.
|
||||
|
||||
If an older unit reports:
|
||||
|
||||
```text
|
||||
getifaddrs failed
|
||||
Address family not supported by protocol (os error 97)
|
||||
```
|
||||
|
||||
update the systemd unit or add `AF_NETLINK` to `RestrictAddressFamilies`, then run:
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl restart gree-controller
|
||||
```
|
||||
|
||||
For a dedicated GREE NIC such as `eth1`, the expected startup/bind log should identify the IPv4 address of that interface instead of `0.0.0.0`.
|
||||
|
||||
### Legacy V1 devices discovered but not binding
|
||||
|
||||
Version 0.3.8 sends the GREE protocol `tcid` and inner `mac` identifiers in canonical lowercase hexadecimal. If an older `502cc6...` device is discovered on UDP/7000 but stays offline after bind timeouts, update to v0.3.8 before changing routing or firewall settings. With debug logging, `Sending GREE request` should show `wire_mac=502cc6...` in lowercase.
|
||||
|
||||
|
||||
## Home Assistant HTTPS with a self-signed certificate
|
||||
|
||||
The optional outbound HA sensor client validates TLS certificates by default. For a trusted local endpoint such as `https://10.87.65.2` that uses a self-signed, expired or hostname-mismatched certificate, enable **Settings -> Allow invalid/self-signed HTTPS certificate** and save. The same initial setting can be supplied as `HA_ALLOW_INVALID_TLS=true`.
|
||||
|
||||
This opt-in affects only controller -> Home Assistant sensor requests. It does not change the GREE UDP transport and it does not add HTTPS termination to the controller itself.
|
||||
@@ -1,196 +0,0 @@
|
||||
# GREE Controller project specification
|
||||
|
||||
## Product goal
|
||||
|
||||
Build an autonomous local GREE HVAC controller running primarily as a Rust service in a dedicated Linux/LXC environment. Home Assistant is optional: it can provide external sensor data and can consume controller devices through a thin custom integration, but it must not contain the core GREE protocol or heating logic.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
Home Assistant / Web UI / REST clients
|
||||
|
|
||||
REST + WebSocket
|
||||
|
|
||||
GREE Controller (Rust)
|
||||
+--------------------------------+
|
||||
| Device/state manager |
|
||||
| GREE protocol UDP/AES |
|
||||
| Zone/heating engine |
|
||||
| Scheduler |
|
||||
| Automation engine |
|
||||
| SQLite |
|
||||
| Mobile-first web UI |
|
||||
+--------------------------------+
|
||||
|
|
||||
UDP/7000
|
||||
|
|
||||
GREE AC
|
||||
```
|
||||
|
||||
## Independence requirements
|
||||
|
||||
- GREE control must work when Home Assistant is stopped.
|
||||
- Local schedules and automations must continue without HA.
|
||||
- HA sensor failures must never remove basic access to the AC.
|
||||
- Device state is persisted by the controller and can be restored to clients after reconnect/restart.
|
||||
|
||||
## GREE protocol layer
|
||||
|
||||
The protocol implementation is isolated from application logic and covers:
|
||||
|
||||
- discovery,
|
||||
- packet encoding/decoding,
|
||||
- AES-128-ECB support,
|
||||
- AES-128-GCM envelope support,
|
||||
- bind/key acquisition,
|
||||
- status polling,
|
||||
- command transport,
|
||||
- reconnect/offline handling.
|
||||
|
||||
The rest of the application works with generic device state and commands rather than raw protocol packets.
|
||||
|
||||
## Device model
|
||||
|
||||
Each device stores:
|
||||
|
||||
- stable controller ID,
|
||||
- MAC/CID,
|
||||
- name,
|
||||
- IP/port,
|
||||
- protocol version,
|
||||
- model/firmware when available,
|
||||
- encryption key when bound,
|
||||
- enabled/simulated flags,
|
||||
- power and HVAC mode,
|
||||
- target/current/outdoor temperature,
|
||||
- fan, swing, quiet, turbo, light,
|
||||
- online/last-seen/error state.
|
||||
|
||||
## Smart thermostat, zones and schedules
|
||||
|
||||
The primary user model is a fast thermostat, not raw device automation. A global climate mode selects the season (`cool`, `heat`, `off`). Zones follow it by default and can optionally override Heat/Cool. The UI labels this as **Mode policy**: keep **Follow global mode** when the zone should inherit the mode selected globally in GREE Controller; the fixed Cooling/Heating policies are explicit overrides. House `off` is a **do not control** state for inherited zones: manual device operation is left untouched, while a zone explicitly switched to Heat/Cool can operate independently. Whole-house power on/off is a separate master state. Master Off is authoritative for zones and controller automations but does not modify the selected house thermostat mode.
|
||||
|
||||
During normal heating/cooling the controller minimizes unit power cycling. It keeps the indoor unit powered and uses **setpoint modulation**:
|
||||
|
||||
- when the zone requires conditioning, use the active target (optionally assisted slightly by outdoor weather),
|
||||
- when the zone is satisfied, move the device setpoint to the non-demand side of the room target so the inverter/compressor can stop naturally,
|
||||
- use hysteresis and a minimum adjustment interval to avoid command chatter,
|
||||
- optionally adjust fan speed based on room error and outdoor extremes; with Smart fan enabled, a satisfied zone uses Low fan together with the standby setpoint and requests Quiet mode in the same frame when the unit supports it. Unsupported Quiet commands automatically fall back to Low fan without breaking thermostat control.
|
||||
|
||||
Each zone has separate seasonal profile temperatures:
|
||||
|
||||
- Cooling: Comfort / Sleep / Away,
|
||||
- Heating: Comfort / Sleep / Away.
|
||||
|
||||
The zone can use the GREE internal sensor, its own Home Assistant room sensor, or a weighted combination. External room sensors are configured per zone; loss/discrepancy falls back safely to the GREE sensor.
|
||||
|
||||
Weekly schedules reference profiles instead of duplicating temperatures. Ready-made Family, Child room, Bedroom, Workday and Always-comfort templates create ordinary schedule rows that remain fully editable. A manual **Sleep**, Comfort, Away or custom-temperature override ends automatically at the next schedule boundary; if no future transition exists, it remains active until explicitly cleared. Whole-house preset actions apply the same temporary policy to every zone.
|
||||
|
||||
Automations remain available for advanced exceptions. Daily comfort should be implemented with zones/profiles/schedules so direct-device automation commands do not compete with the thermostat engine.
|
||||
|
||||
Climate groups are named collections of thermostat zones (for example Upstairs/Downstairs). Group commands can independently set power, Heat/Cool/house-follow mode and Auto/Comfort/Sleep/Away profile for only their member zones. Automation actions may target either a direct device or a climate group.
|
||||
|
||||
### Outdoor temperature assist
|
||||
|
||||
A configured Home Assistant outdoor-temperature entity is optional auxiliary context. It never replaces the room-control temperature. Under strong heat/cold the engine may slightly bias the active AC target and increase Smart Fan airflow. If HA is unavailable, the smart thermostat continues without outdoor assistance.
|
||||
|
||||
## Scheduler and automations
|
||||
|
||||
Schedules are weekly time windows and may cross midnight. Equal start/end times mean a 24-hour block for each selected weekday. Enabled schedules for one zone may not overlap. Schedules can select Comfort/Sleep/Away or a custom setpoint. Automations support time or temperature triggers and device commands with cooldown protection; a failed command also observes the cooldown instead of retrying every control cycle. They are intended for exceptions rather than the normal daily thermostat cycle.
|
||||
|
||||
The automation engine runs in Rust and does not require Home Assistant YAML automation logic.
|
||||
|
||||
## API
|
||||
|
||||
The controller exposes REST for configuration/commands and WebSocket for live state updates. The HA integration and web UI use the same controller API.
|
||||
|
||||
## Home Assistant direction 1: external sensor input
|
||||
|
||||
The Rust application can read a specifically configured HA entity using a Long-Lived Access Token. This input is optional and is not a prerequisite for GREE control.
|
||||
|
||||
## Home Assistant direction 2: native climate entities
|
||||
|
||||
A custom integration creates HA climate entities and translates HA service calls to controller API commands. It does not communicate with GREE directly.
|
||||
|
||||
For migrations from the default GREE integration, a mapping file can request an existing entity ID such as `climate.klima_salon`. The previous integration must release that ID before takeover.
|
||||
|
||||
## Web UI
|
||||
|
||||
Primary use is from a phone. Requirements:
|
||||
|
||||
- responsive mobile-first layout,
|
||||
- touch-friendly controls,
|
||||
- live state updates,
|
||||
- device, zone, schedule, automation, history and diagnostics screens,
|
||||
- JSON-based language packs from `lang/*.json`, with English as the required default/fallback and Polish included,
|
||||
- automatic language discovery at build time, so a new valid `<code>.json` file adds a language without JavaScript changes,
|
||||
- language stored in the `gree_controller_language` cookie,
|
||||
- system/light/dark appearance modes,
|
||||
- appearance stored in a cookie,
|
||||
- flat visual design without decorative shadows,
|
||||
- no letter-logo badge next to the application name.
|
||||
|
||||
## Storage
|
||||
|
||||
SQLite stores configuration, state, readings and events. PostgreSQL or other external database services are not required for the single-node deployment target.
|
||||
|
||||
## Deployment
|
||||
|
||||
Primary target:
|
||||
|
||||
```text
|
||||
LXC / Debian or Ubuntu
|
||||
/opt/gree-controller/gree-controller
|
||||
/etc/gree-controller.env
|
||||
/var/lib/gree-controller/gree-controller.db
|
||||
systemd: gree-controller.service
|
||||
```
|
||||
|
||||
A development script prepares dependencies/build/runtime configuration, and a separate installer creates the systemd deployment.
|
||||
|
||||
## Security
|
||||
|
||||
- optional administrator Bearer token for the controller Web/API,
|
||||
- separately generated, revocable Home Assistant client tokens stored only as SHA-256 hashes,
|
||||
- Home Assistant client tokens are restricted to device read/control endpoints,
|
||||
- secrets kept outside source control,
|
||||
- no direct public Internet exposure,
|
||||
- TLS delegated to a trusted reverse proxy/VPN when required,
|
||||
- local device keys and HA token protected in environment/database files,
|
||||
- invalid/self-signed HA HTTPS certificates are rejected by default; bypass is an explicit trusted-LAN opt-in.
|
||||
|
||||
|
||||
## Per-zone room sensors
|
||||
|
||||
A zone represents one room and normally maps one GREE indoor unit to one optional external room-temperature sensor. External sensors are not global. For example, Living Room can use `sensor.living_room_temperature` while Bedroom independently uses `sensor.bedroom_temperature`.
|
||||
|
||||
The zone controller supports `device`, `combined`, and `home_assistant` temperature strategies. Combined mode uses a configurable external-sensor weight (40% by default), validates the difference between sensors, and falls back to the GREE sensor when the external source is missing or outside the configured discrepancy limit. The local GREE measurement therefore remains available even when Home Assistant is offline.
|
||||
|
||||
|
||||
## Operational packaging conventions
|
||||
|
||||
- All operator-facing shell/Python utilities live under `scripts/`.
|
||||
- `scripts/install.sh` performs the first systemd/LXC installation.
|
||||
- `scripts/update.sh` performs an in-place update with a stopped SQLite backup, health check and automatic rollback.
|
||||
- `scripts/service.sh` provides common systemd operations.
|
||||
- Cargo's `build.rs` stays at the package root because Cargo requires that location.
|
||||
- Every SQLite statement and schema definition is centralized in `src/queries.rs`; database/domain code must not embed SQL strings elsewhere.
|
||||
|
||||
|
||||
## Web UI design system (v0.4.4)
|
||||
|
||||
The embedded UI uses the classic GREE Controller visual language: large rounded surfaces, circular thermostat controls, comfortable spacing, compact desktop navigation and mobile bottom navigation. Light, Dark and System themes use neutral surfaces with a restrained green accent.
|
||||
|
||||
The UI ships as a normal static `web/styles.css` file embedded into the Rust binary. There is no frontend package manager, CDN, or CSS build pipeline.
|
||||
|
||||
### Rich zone history
|
||||
|
||||
Zone history stores GREE temperature, optional Home Assistant room temperature, calculated control temperature, profile target, actual device setpoint, outdoor temperature assist, power, mode, fan speed, demand, active preset and control source. The UI can compare all zones on common temperature/target charts or inspect one zone in detail.
|
||||
|
||||
### History information architecture (v0.4.4)
|
||||
|
||||
History is not a single long page. It is split into URL-addressable views: Overview, Zones, GREE devices, HA sensors and Custom chart. Device history exists independently from zones, HA sensors have their own history stream, and zone history adds the thermostat/control context. A compatibility fallback maps existing GREE device readings into a zone timeline when the richer zone table is still empty.
|
||||
|
||||
The custom chart composer can mix GREE indoor/outdoor/target values, zone GREE/HA/control/target/device-setpoint/outdoor values, and HA temperature entities. Saved definitions persist in browser storage and a chart definition can be serialized into the `/history/custom?chart=...` URL for direct sharing/bookmarking.
|
||||
|
||||
Quick thermostat temperature nudges are optimistic in the browser and stored separately from the selected Comfort/Sleep/Away/Auto preset. Changing `+/-` therefore does not silently leave Auto mode.
|
||||
@@ -1,32 +0,0 @@
|
||||
# Reverse proxy
|
||||
|
||||
The web UI supports HTTPS/WSS and sub-path deployments. The server honors `GREE_CONTROLLER_BASE_PATH` and also understands `X-Forwarded-Prefix` when a proxy strips the prefix before forwarding.
|
||||
|
||||
## Root deployment
|
||||
|
||||
Proxy `/` to `http://127.0.0.1:8787` and forward WebSocket upgrade headers. No controller setting is required.
|
||||
|
||||
## Sub-path deployment
|
||||
|
||||
For a public path such as `/gree`, either:
|
||||
|
||||
- set `GREE_CONTROLLER_BASE_PATH=/gree` and proxy `/gree` to the controller without stripping the path, or
|
||||
- strip `/gree` in the proxy and send `X-Forwarded-Prefix: /gree`.
|
||||
|
||||
Static assets, language packs, browser routes, API requests, PWA scope and WebSocket URLs use the effective base path.
|
||||
|
||||
### nginx example (prefix stripped)
|
||||
|
||||
```nginx
|
||||
location /gree/ {
|
||||
proxy_pass http://127.0.0.1:8787/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Prefix /gree;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
```
|
||||
|
||||
For an Internet-facing endpoint, add TLS and proxy authentication. If the application token is enabled, avoid access-log formats that record query strings because the browser WebSocket connection includes the token in its URL.
|
||||
@@ -1,31 +0,0 @@
|
||||
# Security notes
|
||||
|
||||
The controller is designed primarily for a trusted LAN. This release adds safe defaults without forcing authentication on existing installations.
|
||||
|
||||
## Implemented safeguards
|
||||
|
||||
- `GREE_CONTROLLER_APP_TOKEN=` remains supported. Authentication is optional and existing installations are compatible.
|
||||
- Permissive CORS was removed. The web UI and API are same-origin by default.
|
||||
- Responses include `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Content-Security-Policy` and a restrictive `Permissions-Policy`; API responses are marked `no-store`.
|
||||
- Runtime secrets for Home Assistant, InfluxDB and notifications are stored server-side and never returned to the browser. Empty secret fields keep the previously stored value.
|
||||
- Slack and Discord notification webhooks require HTTPS and are restricted to the official webhook hosts. Redirect following is disabled for these outbound webhook requests. Pushover uses its fixed official API endpoint.
|
||||
- API access tokens are stored hashed. Administrator token behavior is unchanged for backwards compatibility.
|
||||
- History/event API limits and SQLite indexes bound common read paths. History retention/compaction is configurable.
|
||||
|
||||
## Remaining risks / recommendations
|
||||
|
||||
1. With an empty application token, every client that can reach the controller HTTP port can operate the air conditioners and change configuration. Keep port 8787 on a trusted VLAN or put authentication in the reverse proxy.
|
||||
2. The WebSocket administrator token is still passed in its query string because browser WebSocket APIs cannot set an Authorization header. Avoid logging query strings at the proxy and prefer HTTPS/WSS when a token is enabled.
|
||||
3. The application does not terminate TLS. Use a reverse proxy or VPN for traffic crossing an untrusted network.
|
||||
4. Home Assistant `allow_invalid_tls` should be used only with a known local instance; keep it disabled otherwise.
|
||||
5. Configuration exports contain device keys and may contain integration secrets. Treat backup files as secrets.
|
||||
6. There is no per-client API rate limiter. For exposure beyond a trusted LAN, configure rate limiting at the reverse proxy.
|
||||
7. No CSRF token is used. Same-origin operation, removal of permissive CORS and bearer-token authentication reduce the risk, but an authenticated public deployment should also use proxy-level origin/access controls.
|
||||
|
||||
## Suggested reverse-proxy hardening
|
||||
|
||||
- TLS only; redirect HTTP to HTTPS.
|
||||
- Do not log URL query strings if administrator authentication is enabled.
|
||||
- Pass WebSocket upgrades for `/ws`.
|
||||
- Add authentication at the proxy when `GREE_CONTROLLER_APP_TOKEN` is empty and the service is reachable outside the trusted LAN.
|
||||
- Limit request body sizes and add basic request-rate limiting.
|
||||
+10009
File diff suppressed because it is too large
Load Diff
@@ -1,73 +0,0 @@
|
||||
1. Timer powrotu do automatyzacji
|
||||
|
||||
Backend już przechowywał local_thermostat_resume_at, ale przy takim scenariuszu:
|
||||
|
||||
lokalny OFF → licznik 15 min → pilot/direct ON → pilot/direct OFF
|
||||
|
||||
stary deadline nadal istniał. Sterowanie pilotem miało wyższy priorytet, ale timer lokalnego OFF nadal „leciał pod spodem”.
|
||||
|
||||
Poprawiłem to tak:
|
||||
|
||||
dodałem centralną metodę set_local_thermostat_power(...),
|
||||
każde nowe OFF ustawia świeże now + 15 min,
|
||||
ON kasuje deadline,
|
||||
podczas device_manual_override timer lokalnego OFF nie może wygasnąć,
|
||||
gdy użytkownik po pilocie/direct wróci do wcześniejszego stanu OFF, backend ustawia nowe pełne 15 minut,
|
||||
HA bierze dokładny local_thermostat_resume_at zwrócony przez backend, więc stary odczyt nie powinien chwilowo przywracać poprzedniego timera.
|
||||
|
||||
Frontend nadal może odświeżać MM:SS co sekundę lokalnie — to jest właściwe. Frontend tylko wyświetla czas, backend decyduje kiedy przejąć sterowanie. Jeśli chcesz całkowicie uniezależnić wyświetlanie od zegara przeglądarki, później można dodać server_time albo remaining_seconds.
|
||||
|
||||
2. Pilot + Home Assistant + Web — jak powinno to działać
|
||||
|
||||
Tutaj obecna architektura jest już częściowo dobra. Masz dwa różne rodzaje poleceń:
|
||||
|
||||
bezpośrednie urządzenie — pilot, Web „Devices”, fizyczny climate w HA,
|
||||
termostat strefy — Quick Thermostat w Web i climate.<strefa>_thermostat w HA.
|
||||
|
||||
Logicznie ustawiłbym następujące zasady:
|
||||
|
||||
Pilot/direct przejmuje urządzenie. Powstaje device_manual_override. Harmonogram, grupa i automatyka nie walczą wtedy z człowiekiem.
|
||||
Kolejna komenda direct z HA/Web zmienia tę samą ręczną sesję i nadal nie uruchamia automatyki.
|
||||
Komenda do termostatu strefy oznacza świadome: „teraz znowu steruje regulator”. Wtedy device_manual_override jest kasowany i zaczyna obowiązywać termostat.
|
||||
Grupy, harmonogram i automatyzacje nie powinny po cichu odbierać sterowania pilotowi.
|
||||
Globalne OFF powinno pozostać nadrzędne — i obecny kod właśnie tak działa.
|
||||
|
||||
To jest bardzo sensowna polityka.
|
||||
|
||||
W panelu dodałbym jednak jawny status jednego właściciela:
|
||||
|
||||
Automatyka / Termostat lokalny / Pilot / HA — sterowanie bezpośrednie / Web — sterowanie bezpośrednie / Globalnie wyłączone
|
||||
|
||||
oraz:
|
||||
|
||||
od kiedy,
|
||||
do kiedy,
|
||||
dlaczego,
|
||||
przycisk Przejmij sterowanie termostatem.
|
||||
|
||||
API też powinno to wystawiać, np. logicznie jako:
|
||||
|
||||
control_owner
|
||||
control_source
|
||||
control_since
|
||||
resume_at
|
||||
|
||||
Obecnie device_manual_override jest zapisany, ale dokładne źródło przejęcia jest głównie w logach. Przy dalszym rozwoju warto zrobić z tego normalny stan.
|
||||
|
||||
Szczególnie w HA rozważyłbym, żeby fizyczny climate urządzenia był oznaczony jako „sterowanie bezpośrednie / zaawansowane”, a głównym encją użytkową był termostat strefy. Teraz użytkownik może mieć dwa climate dla jednego klimatyzatora, które celowo mają inne znaczenie — łatwo się pomylić.
|
||||
|
||||
3. Co jeszcze brakuje przed używaniem tego jako głównego ogrzewania/chłodzenia
|
||||
|
||||
Najważniejsze rzeczy, w tej kolejności:
|
||||
|
||||
Blokada współbieżnych zmian strefy. To jest realny problem w obecnym kodzie. HA i Web mogą prawie równocześnie pobrać ten sam Zone, zmienić różne pola i zapisać cały JSON. Ostatni zapis może nadpisać poprzednią zmianę. Dałbym per-zone mutex oraz docelowo revision/optimistic locking. To jest dla mnie najwyższy następny priorytet.
|
||||
Formalny model własności sterowania. Zamiast sprawdzania w wielu miejscach device_manual_override, local_thermostat_power, group gate itd., jedna metoda typu resolve_control_owner() powinna określać kto aktualnie może wysyłać komendy. Panel, HA i engine korzystałyby z tej samej odpowiedzi.
|
||||
Ochrona przed szybkim przełączaniem Heat ↔ Cool / OFF ↔ ON. W modelu masz min_on_seconds i min_off_seconds, ale obecnie praktycznie nie są wykorzystywane przez regulator. Zmiana trybu jest wręcz traktowana jako pilna. Dla głównego źródła ogrzewania dodałbym minimum 3–5 minut blokady po wyłączeniu oraz przy zmianie Heat/Cool.
|
||||
Kontrola świeżości czujnika HA. read_temperature() sprawdza, czy wartość jest liczbą, ale nie sprawdza wieku last_updated. Sensor może wisieć z poprawną wartością przez wiele godzin. Potrzebny jest np. sensor_stale_after=5 min, fallback na GREE i informacja/alarm.
|
||||
Tryby ręcznego „hold”. Zamiast jednej zasady warto mieć: 15 min, 30 min, 1 h, do kolejnego harmonogramu, do godziny..., bezterminowo. Ten sam mechanizm można potem wykorzystać dla temperatury, profilu, pilota i lokalnego OFF.
|
||||
Zmiana harmonogramu podczas ręcznego przejęcia. Kod aktualizuje granicę manual_override_until dla ręcznego profilu/setpointu po edycji harmonogramu, ale nie widzę analogicznego przeliczania device_manual_override_until. To może pozostawić nieaktualny termin przejęcia po zmianie grafiku.
|
||||
Desired state vs actual state. Warto w API jasno publikować „czego chce regulator” i „co faktycznie ma klimatyzator”, plus powód rozbieżności: pilot, offline, timer, group OFF, lockout. Część tego już masz w control-plan, więc to jest naturalne rozszerzenie.
|
||||
Ochrona temperaturowa. Dla głównego ogrzewania przyda się opcjonalny „frost protection”, np. alarm lub awaryjne grzanie poniżej określonej temperatury. Powinno być osobno konfigurowalne, żeby świadome globalne OFF nie zostało niespodziewanie złamane.
|
||||
Dopiero później rozważałbym automatyczne Heat/Cool na podstawie temperatury z dużą martwą strefą i minimalnym czasem pozostawania w jednym trybie.
|
||||
|
||||
Najbliższa zmiana architektoniczna, którą zrobiłbym teraz, to więc ControlOwnership + per-zone locking/revision. To rozwiąże większość przyszłych konfliktów HA ↔ Web ↔ pilot w jednym miejscu, zamiast dodawać kolejne wyjątki.
|
||||
@@ -0,0 +1,17 @@
|
||||
# OCI image
|
||||
REGISTRY=repo.example.com
|
||||
IMAGE_NAME=gree-controller
|
||||
DISTRO=alpine
|
||||
RUST_VERSION=1.98.1
|
||||
BUILD_PLATFORMS=linux/amd64,linux/arm64
|
||||
BUILDER=gree-controller-multiarch
|
||||
BUILDER_CONFIG=
|
||||
|
||||
# Home Assistant distribution repository
|
||||
HA_REPO_URL=https://repo.example.com/gree-controller-ha-addon/
|
||||
HA_REPO_BRANCH=master
|
||||
HA_REPO_PATH=../gree-controller-ha-addon
|
||||
|
||||
# Repository layout
|
||||
ADDON_DIR=gree-controller
|
||||
INTEGRATION_DOMAIN=gree_controller
|
||||
@@ -0,0 +1,126 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
ARG RUST_VERSION=1.98.1
|
||||
ARG XX_VERSION=1.9.0
|
||||
|
||||
# Cross-compilation helpers. Builder stages always run on BUILDPLATFORM, so an
|
||||
# amd64 host can compile arm64 without running rustc/cargo under QEMU.
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-trixie AS builder-trixie
|
||||
COPY --from=xx / /
|
||||
WORKDIR /src
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends clang lld llvm file pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Fetch Rust dependencies before TARGETPLATFORM enters the cache chain, so the
|
||||
# same registry/git cache is reused by amd64 and arm64 builds.
|
||||
COPY Cargo.toml ./
|
||||
RUN --mount=type=cache,id=gree-controller-cargo-registry,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,id=gree-controller-cargo-git,target=/usr/local/cargo/git \
|
||||
mkdir -p src \
|
||||
&& printf 'fn main() {}\n' > src/main.rs \
|
||||
&& cargo generate-lockfile \
|
||||
&& cargo fetch --locked \
|
||||
&& rm -rf src
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETARCH
|
||||
# Prepare the target C sysroot and Rust stdlib in a stable layer. Do not call
|
||||
# the xx-cargo version probe: xx-cargo is a build wrapper and that invocation
|
||||
# makes Cargo receive a target option without a subcommand.
|
||||
RUN xx-apt-get update \
|
||||
&& xx-apt-get install -y --no-install-recommends xx-c-essentials \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rustup target add "$(xx-cargo --print-target-triple)"
|
||||
|
||||
COPY build.rs ./
|
||||
COPY src ./src
|
||||
COPY web ./web
|
||||
COPY lang ./lang
|
||||
COPY presets ./presets
|
||||
COPY docs/openapi.json ./docs/openapi.json
|
||||
RUN --mount=type=cache,id=gree-controller-cargo-registry,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,id=gree-controller-cargo-git,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,id=gree-controller-target-trixie-${TARGETARCH},target=/src/target \
|
||||
set -eux; \
|
||||
xx-cargo build --locked --release --target-dir /src/target; \
|
||||
binary="/src/target/$(xx-cargo --print-target-triple)/release/gree-controller"; \
|
||||
xx-verify "$binary"; \
|
||||
mkdir -p /out; \
|
||||
cp "$binary" /out/gree-controller
|
||||
|
||||
FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-alpine AS builder-alpine
|
||||
COPY --from=xx / /
|
||||
WORKDIR /src
|
||||
RUN apk add --no-cache clang lld llvm file pkgconf
|
||||
|
||||
# Keep dependency fetching architecture-independent here as well.
|
||||
COPY Cargo.toml ./
|
||||
RUN --mount=type=cache,id=gree-controller-cargo-registry,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,id=gree-controller-cargo-git,target=/usr/local/cargo/git \
|
||||
mkdir -p src \
|
||||
&& printf 'fn main() {}\n' > src/main.rs \
|
||||
&& cargo generate-lockfile \
|
||||
&& cargo fetch --locked \
|
||||
&& rm -rf src
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETARCH
|
||||
RUN xx-apk add --no-cache xx-c-essentials \
|
||||
&& rustup target add "$(xx-cargo --print-target-triple)"
|
||||
|
||||
COPY build.rs ./
|
||||
COPY src ./src
|
||||
COPY web ./web
|
||||
COPY lang ./lang
|
||||
COPY presets ./presets
|
||||
COPY docs/openapi.json ./docs/openapi.json
|
||||
RUN --mount=type=cache,id=gree-controller-cargo-registry,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,id=gree-controller-cargo-git,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,id=gree-controller-target-alpine-${TARGETARCH},target=/src/target \
|
||||
set -eux; \
|
||||
xx-cargo build --locked --release --target-dir /src/target; \
|
||||
binary="/src/target/$(xx-cargo --print-target-triple)/release/gree-controller"; \
|
||||
xx-verify "$binary"; \
|
||||
mkdir -p /out; \
|
||||
cp "$binary" /out/gree-controller
|
||||
|
||||
FROM --platform=$TARGETPLATFORM debian:trixie-slim AS runtime-trixie
|
||||
ARG BUILD_VERSION=dev
|
||||
ARG TARGETARCH
|
||||
LABEL io.hass.version="${BUILD_VERSION}" \
|
||||
io.hass.type="app" \
|
||||
org.opencontainers.image.base.name="debian:trixie-slim" \
|
||||
org.opencontainers.image.description="GREE Controller Home Assistant add-on" \
|
||||
org.opencontainers.image.vendor="MateuszG" \
|
||||
org.opencontainers.image.title="GREE Controller" \
|
||||
org.opencontainers.image.version="${BUILD_VERSION}" \
|
||||
org.opencontainers.image.architecture="${TARGETARCH}"
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates tzdata jq iproute2 curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder-trixie /out/gree-controller /usr/local/bin/gree-controller
|
||||
COPY ha-addon/run.sh /usr/local/bin/run.sh
|
||||
RUN chmod 0755 /usr/local/bin/run.sh /usr/local/bin/gree-controller
|
||||
EXPOSE 8787/tcp
|
||||
ENTRYPOINT ["/usr/local/bin/run.sh"]
|
||||
|
||||
FROM --platform=$TARGETPLATFORM alpine:latest AS runtime-alpine
|
||||
ARG BUILD_VERSION=dev
|
||||
ARG TARGETARCH
|
||||
LABEL io.hass.version="${BUILD_VERSION}" \
|
||||
io.hass.type="app" \
|
||||
org.opencontainers.image.base.name="alpine:latest" \
|
||||
org.opencontainers.image.description="GREE Controller Home Assistant add-on" \
|
||||
org.opencontainers.image.vendor="MateuszG" \
|
||||
org.opencontainers.image.title="GREE Controller" \
|
||||
org.opencontainers.image.version="${BUILD_VERSION}" \
|
||||
org.opencontainers.image.architecture="${TARGETARCH}"
|
||||
RUN apk add --no-cache bash ca-certificates tzdata jq iproute2 curl
|
||||
COPY --from=builder-alpine /out/gree-controller /usr/local/bin/gree-controller
|
||||
COPY ha-addon/run.sh /usr/local/bin/run.sh
|
||||
RUN chmod 0755 /usr/local/bin/run.sh /usr/local/bin/gree-controller
|
||||
EXPOSE 8787/tcp
|
||||
ENTRYPOINT ["/usr/local/bin/run.sh"]
|
||||
@@ -0,0 +1,85 @@
|
||||
# GREE Controller — Home Assistant distribution
|
||||
|
||||
This directory contains the OCI build, Home Assistant add-on repository source, custom integration and repository synchronization tooling.
|
||||
|
||||
The packaged controller includes its HTTP/WebSocket API and built-in Swagger documentation at `/api-docs` (`/api-docs/openapi.json` for OpenAPI 3.1).
|
||||
|
||||
## Configuration
|
||||
|
||||
Create the local build/repository configuration once:
|
||||
|
||||
```bash
|
||||
cd ha-addon
|
||||
cp .env.example .env
|
||||
$EDITOR .env
|
||||
```
|
||||
|
||||
`build.sh` and `sync-repository.sh` read all deployment-specific values from `.env`; `.env` is ignored by Git.
|
||||
|
||||
Important variables:
|
||||
|
||||
```dotenv
|
||||
REGISTRY=repo.example.com
|
||||
IMAGE_NAME=gree-controller
|
||||
DISTRO=alpine
|
||||
RUST_VERSION=1.98.1
|
||||
BUILD_PLATFORMS=linux/amd64,linux/arm64
|
||||
BUILDER=gree-controller-multiarch
|
||||
BUILDER_CONFIG=
|
||||
HA_REPO_URL=https://repo.example.com/gree-controller-ha-addon/
|
||||
HA_REPO_BRANCH=master
|
||||
HA_REPO_PATH=../gree-controller-ha-addon
|
||||
ADDON_DIR=gree-controller
|
||||
INTEGRATION_DOMAIN=gree_controller
|
||||
```
|
||||
|
||||
## OCI image
|
||||
|
||||
```bash
|
||||
./build.sh render
|
||||
./build.sh load amd64
|
||||
./build.sh load aarch64
|
||||
./build.sh push
|
||||
```
|
||||
|
||||
The project version comes only from root `Cargo.toml`. `render` synchronizes it to add-on `config.yaml` and the custom integration `manifest.json`, and sets the OCI image from `.env`. The container build generates `Cargo.lock` from the exact direct dependency versions in `Cargo.toml` before fetching and compiling dependencies.
|
||||
|
||||
For the Home Assistant package, TCP `8787` is an intentional fixed host-network/ingress port contract. `build.sh render` verifies `config.yaml`, `run.sh`, the watchdog URL and Docker image metadata stay consistent. The runtime also compares the Supervisor-reported ingress port before starting. Standalone installations remain free to use any `GREE_CONTROLLER_BIND` port.
|
||||
|
||||
`DISTRO` still selects the target image system: `alpine` builds a musl binary for Alpine and `trixie` builds a GNU/glibc binary for Debian Trixie. Multi-architecture builds continue to run through `build.sh`; the Docker builder cross-compiles Rust on `BUILDPLATFORM` so ARM compilation does not run under QEMU, and BuildKit cache mounts preserve Cargo downloads and target artifacts between builds.
|
||||
|
||||
## Home Assistant repository
|
||||
|
||||
```bash
|
||||
./sync-repository.sh
|
||||
```
|
||||
|
||||
The command updates/clones `HA_REPO_URL`, mirrors the repository and pushes only when content changed. The resulting repository contains:
|
||||
|
||||
```text
|
||||
repository.yaml
|
||||
gree-controller/ # Home Assistant add-on
|
||||
custom_components/gree_controller/ # Home Assistant custom integration
|
||||
```
|
||||
|
||||
The custom integration source is maintained only in:
|
||||
|
||||
```text
|
||||
ha-addon/home-assistant/custom_components/gree_controller/
|
||||
```
|
||||
|
||||
It is added to the separate HA repository during synchronization, so it is not duplicated in this project.
|
||||
|
||||
Adding the repository to the Home Assistant add-on store installs the add-on only. The bundled custom integration can be copied from the same repository to `/config/custom_components/gree_controller/` when required.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
.env.example local deployment configuration template
|
||||
Dockerfile multi-architecture image
|
||||
build.sh metadata + OCI build/push
|
||||
run.sh add-on entrypoint
|
||||
sync-repository.sh separate HA repository synchronization
|
||||
repository/ add-on repository source
|
||||
home-assistant/ custom integration source and migration output
|
||||
```
|
||||
Executable
+165
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
ENV_FILE="${HA_ADDON_ENV_FILE:-$SCRIPT_DIR/.env}"
|
||||
|
||||
[[ -f "$ENV_FILE" ]] || {
|
||||
echo "Missing $ENV_FILE. Copy $SCRIPT_DIR/.env.example to $SCRIPT_DIR/.env and edit it." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
required=(REGISTRY IMAGE_NAME DISTRO RUST_VERSION BUILD_PLATFORMS BUILDER HA_REPO_URL ADDON_DIR INTEGRATION_DOMAIN)
|
||||
for name in "${required[@]}"; do
|
||||
[[ -n "${!name:-}" ]] || { echo "Missing $name in $ENV_FILE" >&2; exit 1; }
|
||||
done
|
||||
|
||||
case "$DISTRO" in
|
||||
trixie|alpine) ;;
|
||||
*) echo "DISTRO must be one of: trixie, alpine" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
ACTION="${1:-push}"
|
||||
VERSION="$(awk -F '"' '/^version = "/ {print $2; exit}' "$PROJECT_ROOT/Cargo.toml")"
|
||||
[[ -n "$VERSION" ]] || { echo "Cannot determine version from Cargo.toml" >&2; exit 1; }
|
||||
|
||||
IMAGE_REPO="${REGISTRY%/}/${IMAGE_NAME#/}"
|
||||
IMAGE_TAG="${IMAGE_REPO}:${VERSION}"
|
||||
CONFIG="$SCRIPT_DIR/repository/$ADDON_DIR/config.yaml"
|
||||
INTEGRATION_MANIFEST="$SCRIPT_DIR/home-assistant/custom_components/$INTEGRATION_DOMAIN/manifest.json"
|
||||
REPOSITORY_CONFIG="$SCRIPT_DIR/repository/repository.yaml"
|
||||
RUN_SCRIPT="$SCRIPT_DIR/run.sh"
|
||||
DOCKERFILE="$SCRIPT_DIR/Dockerfile"
|
||||
|
||||
[[ -f "$CONFIG" ]] || { echo "Missing add-on config: $CONFIG" >&2; exit 1; }
|
||||
[[ -f "$INTEGRATION_MANIFEST" ]] || { echo "Missing integration manifest: $INTEGRATION_MANIFEST" >&2; exit 1; }
|
||||
[[ -f "$REPOSITORY_CONFIG" ]] || { echo "Missing repository config: $REPOSITORY_CONFIG" >&2; exit 1; }
|
||||
|
||||
render_metadata() {
|
||||
command -v python3 >/dev/null 2>&1 || { echo "python3 is required" >&2; exit 1; }
|
||||
python3 - "$CONFIG" "$INTEGRATION_MANIFEST" "$REPOSITORY_CONFIG" "$VERSION" "$IMAGE_REPO" "$HA_REPO_URL" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
config = Path(sys.argv[1])
|
||||
manifest = Path(sys.argv[2])
|
||||
repository = Path(sys.argv[3])
|
||||
version = sys.argv[4]
|
||||
image = sys.argv[5]
|
||||
repo_url = sys.argv[6]
|
||||
|
||||
lines = config.read_text(encoding="utf-8").splitlines()
|
||||
out = []
|
||||
for line in lines:
|
||||
if line.startswith("version:"):
|
||||
out.append(f'version: "{version}"')
|
||||
elif line.startswith("image:"):
|
||||
out.append(f'image: "{image}"')
|
||||
elif line.startswith("url:"):
|
||||
out.append(f'url: "{repo_url}"')
|
||||
else:
|
||||
out.append(line)
|
||||
config.write_text("\n".join(out) + "\n", encoding="utf-8")
|
||||
|
||||
import json
|
||||
data = json.loads(manifest.read_text(encoding="utf-8"))
|
||||
data["version"] = version
|
||||
manifest.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
repo_lines = repository.read_text(encoding="utf-8").splitlines()
|
||||
repo_out = [f"url: {repo_url}" if line.startswith("url:") else line for line in repo_lines]
|
||||
repository.write_text("\n".join(repo_out) + "\n", encoding="utf-8")
|
||||
PY
|
||||
printf 'Synced version %s; image %s\n' "$VERSION" "$IMAGE_REPO"
|
||||
}
|
||||
|
||||
validate_addon_port_contract() {
|
||||
local ingress_port
|
||||
ingress_port="$(awk '/^ingress_port:/ {print $2; exit}' "$CONFIG")"
|
||||
[[ "$ingress_port" =~ ^[0-9]+$ ]] || { echo "Invalid ingress_port in $CONFIG" >&2; exit 1; }
|
||||
grep -Fqx "readonly HA_HTTP_PORT=${ingress_port}" "$RUN_SCRIPT" || {
|
||||
echo "HA port contract mismatch: ingress_port=${ingress_port}, run.sh differs" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -Fq "watchdog: \"http://[HOST]:[PORT:${ingress_port}]/api/health\"" "$CONFIG" || {
|
||||
echo "HA port contract mismatch: watchdog must use [PORT:${ingress_port}]" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -Fqx "hassio_api: true" "$CONFIG" || {
|
||||
echo "HA add-on contract mismatch: hassio_api must be enabled for Supervisor runtime discovery" >&2
|
||||
exit 1
|
||||
}
|
||||
local expose_count
|
||||
expose_count="$(grep -Ec "^EXPOSE ${ingress_port}/tcp$" "$DOCKERFILE" || true)"
|
||||
(( expose_count >= 1 )) || {
|
||||
echo "HA port contract mismatch: Dockerfile must expose ${ingress_port}/tcp" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_builder() {
|
||||
command -v docker >/dev/null 2>&1 || { echo "docker is required" >&2; exit 1; }
|
||||
docker buildx version >/dev/null 2>&1 || { echo "docker buildx is required" >&2; exit 1; }
|
||||
|
||||
if docker buildx inspect "$BUILDER" >/dev/null 2>&1; then
|
||||
docker buildx use "$BUILDER"
|
||||
else
|
||||
args=(create --name "$BUILDER" --driver docker-container --use)
|
||||
[[ -z "${BUILDER_CONFIG:-}" ]] || args+=(--config "$BUILDER_CONFIG")
|
||||
docker buildx "${args[@]}" >/dev/null
|
||||
fi
|
||||
docker buildx inspect --bootstrap >/dev/null
|
||||
}
|
||||
|
||||
build_push() {
|
||||
ensure_builder
|
||||
docker buildx build \
|
||||
--platform "$BUILD_PLATFORMS" \
|
||||
-f "$SCRIPT_DIR/Dockerfile" \
|
||||
--target "runtime-${DISTRO}" \
|
||||
--build-arg "BUILD_VERSION=$VERSION" \
|
||||
--build-arg "RUST_VERSION=$RUST_VERSION" \
|
||||
-t "$IMAGE_TAG" \
|
||||
--push \
|
||||
"$PROJECT_ROOT"
|
||||
printf 'Published %s (%s, %s)\n' "$IMAGE_TAG" "$BUILD_PLATFORMS" "$DISTRO"
|
||||
}
|
||||
|
||||
build_load() {
|
||||
local arch="${2:-amd64}" platform
|
||||
case "$arch" in
|
||||
amd64) platform="linux/amd64" ;;
|
||||
aarch64|arm64) platform="linux/arm64" ;;
|
||||
*) echo "Usage: $0 load [amd64|aarch64]" >&2; exit 2 ;;
|
||||
esac
|
||||
ensure_builder
|
||||
docker buildx build \
|
||||
--platform "$platform" \
|
||||
-f "$SCRIPT_DIR/Dockerfile" \
|
||||
--target "runtime-${DISTRO}" \
|
||||
--build-arg "BUILD_VERSION=$VERSION" \
|
||||
--build-arg "RUST_VERSION=$RUST_VERSION" \
|
||||
-t "${IMAGE_REPO}:local" \
|
||||
--load \
|
||||
"$PROJECT_ROOT"
|
||||
printf 'Loaded %s:local for %s (%s)\n' "$IMAGE_REPO" "$arch" "$DISTRO"
|
||||
}
|
||||
|
||||
render_metadata
|
||||
validate_addon_port_contract
|
||||
|
||||
case "$ACTION" in
|
||||
render) ;;
|
||||
push) build_push ;;
|
||||
load) build_load "${2:-amd64}" ;;
|
||||
*)
|
||||
echo "Usage: $0 [render|push|load [amd64|aarch64]]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -3,14 +3,16 @@
|
||||
The project includes a custom Home Assistant integration under:
|
||||
|
||||
```text
|
||||
home-assistant/custom_components/gree_controller/
|
||||
ha-addon/home-assistant/custom_components/gree_controller/
|
||||
```
|
||||
|
||||
It creates HA entities for physical units, thermostat zones, whole-house controls and climate groups, while sending every command to the standalone Rust controller. Home Assistant therefore remains a client and UDP/AES GREE communication stays outside HA.
|
||||
|
||||
The climate proxy supports power/turn on/off, HVAC modes, target temperature, fan mode, vertical swing and horizontal swing.
|
||||
The controller exposes the API used by the integration and Web UI under `/api/*` and `/ws`; interactive Swagger documentation is available at `/api-docs`, with OpenAPI JSON at `/api-docs/openapi.json`.
|
||||
|
||||
Version 0.5.0 additionally exposes the controller's automation plan:
|
||||
The climate proxy supports power/turn on/off, HVAC modes, target temperature, fan mode, granular vertical louver positions and granular horizontal louver positions. `off`/`on` remain the first two swing modes for compatibility, followed by fixed positions and the supported vertical partial-swing ranges. The card UI translates those modes using the Home Assistant language while the underlying mode IDs (for example `fixed_upper`) remain unchanged for services, scripts and automations.
|
||||
|
||||
Automation plan:
|
||||
|
||||
- `sensor` for the whole-house plan (house mode, upcoming events and active rules),
|
||||
- one `sensor` per thermostat zone with current demand, active preset/schedule and upcoming transitions,
|
||||
@@ -27,6 +29,8 @@ Copy the directory into your HA configuration:
|
||||
/config/custom_components/gree_controller/
|
||||
```
|
||||
|
||||
`sync-repository.sh` also publishes this integration as `custom_components/gree_controller/` in the same Git repository as the add-on. The add-on itself does not install the integration automatically.
|
||||
|
||||
Before adding the integration, open the standalone controller Web UI and go to **More -> Home Assistant / Sensors -> Create new token**. Copy the generated secret; it is shown only once.
|
||||
|
||||
Restart Home Assistant, then open **Settings -> Devices & services -> Add integration -> GREE Controller** and enter only:
|
||||
@@ -110,21 +114,16 @@ Tokens are used only during validation and are not written to the mapping file.
|
||||
|
||||
## HA as an external temperature source
|
||||
|
||||
This is independent from the custom climate integration. Each Rust controller zone can assign its own HA room-temperature entity, for example:
|
||||
This is independent from the custom climate integration. Home Assistant room-temperature sensors are configured per zone with `ha_entity_id`. The recommended `combined` strategy keeps the GREE sensor as the primary input and uses the selected Home Assistant room sensor as a configurable supporting measurement (40% weight by default). If HA or the selected room entity becomes unavailable, the controller falls back to the corresponding GREE unit, so local control and schedules continue to run.
|
||||
|
||||
```text
|
||||
Living room -> GREE Living Room + sensor.living_room_temperature
|
||||
Bedroom -> GREE Bedroom + sensor.bedroom_temperature
|
||||
```
|
||||
|
||||
The recommended `combined` strategy keeps the GREE sensor as the primary input and uses the room sensor as a configurable supporting measurement (40% weight by default). A zone may also select the room sensor as its preferred source. If HA or that entity becomes unavailable, the controller falls back to the corresponding GREE unit, so local control and schedules continue to run.
|
||||
## Zone climate and optional unit features (0.5.4)
|
||||
Outdoor temperature is separate: `outdoor_entity_id` / `HA_OUTDOOR_ENTITY_ID` is the global Home Assistant outdoor-temperature sensor. A zone may optionally set `ha_outdoor_entity_id` to use a different outdoor sensor for that zone's outdoor-temperature assist. If the override is unavailable, the global outdoor source is used. Global and per-zone outdoor entities participate in aliases, metrics/history and Flow entity suggestions. The Home Assistant connection test validates only the server URL/API token and does not require any entity.
|
||||
## Zone climate and optional unit features
|
||||
|
||||
Each controller zone is exposed as a full Home Assistant `climate` entity with current/target temperature and HVAC modes: Off, Auto (follow the controller house mode), Cool and Heat. For a **zone thermostat**, Auto does not mean the GREE unit's native automatic heat/cool algorithm: it means **inherit the whole-house Heating/Cooling selection from GREE Controller**. Direct physical-device climate entities still use the native GREE Auto mode. The target temperature remains published while a zone is Off, so Home Assistant can display the configured setpoint instead of `unknown`. The existing zone target `number` and enabled `switch` remain available for compatibility.
|
||||
|
||||
From version 0.7.9, each zone climate also supports preset modes `auto`, `comfort`, `sleep` and `away`, and the same choices are exposed as a separate **Work profile** `select` on the zone device. `auto` removes the temporary per-zone profile override and returns the zone to its schedule. The zone enable switch now reports the configured zone state independently from group power gates; `effective_enabled` remains available in attributes/sensors to show when an enabled zone is currently blocked by a disabled group. From version 0.7.10, climate entity ON/OFF controls local thermostat power rather than technically enabling/disabling the zone: local ON may run the zone while its group is off, with full thermostat logic intact. From 0.7.12, every local OFF creates a fresh backend-owned 15-minute hand-back deadline exposed as `local_thermostat_resume_at`. A direct/pilot takeover suspends expiry of that local timer; if the unit is returned to the previous OFF state, the backend re-arms a fresh 15-minute countdown from that moment. The separate zone Enabled switch remains the technical availability switch.
|
||||
Each zone climate also supports preset modes `auto`, `comfort`, `sleep` and `away`, and the same choices are exposed as a separate **Work profile** `select` on the zone device. `auto` removes the temporary per-zone profile override and returns the zone to its schedule. The zone enable switch reports the configured zone state independently from group control. Turning a group OFF powers its member units down and releases group ownership. Members are not blocked merely because the group remains OFF: an individual thermostat can be turned back on independently. Turning the group ON clears that scoped OFF state and immediately resumes group thermostat arbitration. From version 0.7.10, climate entity ON/OFF controls local thermostat power rather than technically enabling/disabling the zone. From 0.7.12, every local OFF creates a fresh backend-owned 15-minute hand-back deadline exposed as `local_thermostat_resume_at`. A direct/pilot takeover suspends expiry of that local timer; if the unit is returned to the previous OFF state, the backend re-arms a fresh 15-minute countdown from that moment. The separate zone Enabled switch remains the technical availability switch.
|
||||
|
||||
From version 0.6.4, zone climate entities use `climate.<zone_name>_thermostat`, for example `climate.igor_thermostat`. On integration reload, existing zone climate registry entries are migrated to this scheme using the zone's current name. If the target entity ID is already occupied, the old ID is retained and Home Assistant logs a warning.
|
||||
Zone climate entities use `climate.<zone_name>_thermostat`, for example `climate.igor_thermostat`. On integration reload, existing zone climate registry entries are migrated to this scheme using the zone's current name. If the target entity ID is already occupied, the old ID is retained and Home Assistant logs a warning.
|
||||
|
||||
When the controller detects optional GREE properties, the integration also creates switches for supported features such as panel light, Quiet, Turbo, X-FAN, Air, Health and native Sleep. Reload the integration (or restart Home Assistant) after upgrading so newly added entity types are created.
|
||||
|
||||
@@ -134,13 +133,13 @@ The integration also exposes three controller-level entities on the **GREE Contr
|
||||
|
||||
- **Thermostat mode** (`select`) — Cooling, Heating or **Do not control**. Do not control pauses house-level thermostat commands but leaves direct device control and explicit per-zone Heat/Cool overrides untouched.
|
||||
- **Work profile** (`select`) — Auto schedule, Comfort, Sleep or Away. If zones have mixed manual profiles, the select has no single current option until a whole-house profile is chosen again.
|
||||
- Choosing **Cooling/Heating** in the thermostat-mode select or any whole-house **Work profile** is treated as explicit whole-house activation: it switches **All air conditioners** back on. **Do not control** does not change master power.
|
||||
- **All air conditioners** (`switch`) — separate master power. Turning it off powers every enabled unit down and prevents zones/controller automations from restarting them. Turning it on powers all enabled units on again without changing the selected thermostat mode or profile.
|
||||
- Choosing **Cooling/Heating** changes the house rule for zones that inherit the global mode. **Do not control** pauses only that inherited house rule; explicit local/group/manual control remains independent.
|
||||
- **All air conditioners** (`switch`) performs bulk thermostat power control. OFF powers all enabled units down and leaves their thermostats locally OFF with no hand-back timer; later local/group/global ON can re-enable the selected scope. ON releases local OFF states and respects compressor protection. Its displayed state reflects whether all currently enabled units are physically ON.
|
||||
|
||||
These controls use the Home Assistant integration token and the dedicated `/api/integrations/home-assistant/house/*` endpoints.
|
||||
|
||||
|
||||
## Climate groups (0.7.3)
|
||||
## Climate groups
|
||||
|
||||
Every group configured in **GREE Controller -> Groups** is published as a separate Home Assistant device. A group does not receive a fake common target temperature because its member zones can legitimately use different profile temperatures. Instead, the group device exposes the controls that exactly match the controller model:
|
||||
|
||||
@@ -153,6 +152,6 @@ If member zones have been changed individually and no longer share one mode or p
|
||||
|
||||
After upgrading the custom integration, restart Home Assistant or reload **Settings -> Devices & services -> GREE Controller**. Also reload the integration after adding/removing/renaming groups so new group devices/entities are created.
|
||||
|
||||
## Command state stability (0.7.6)
|
||||
## Command state stability
|
||||
|
||||
Direct physical-unit commands use a short pending-state guard in the Home Assistant coordinator. Some GREE firmware acknowledges a command before its status endpoint stops returning the previous value; the guard prevents that transient stale read from rendering as an `ON -> OFF -> ON` (or reverse) bounce. From version 0.7.9 the same guard also covers per-zone enable, HVAC mode, profile and target-temperature commands. The standalone controller also retries post-command verification for a bounded settling window. Failed commands drop the guard immediately and refresh factual state.
|
||||
+13
@@ -60,6 +60,19 @@ class GreeControllerClient:
|
||||
"""Return the public controller health payload."""
|
||||
return await self._request("GET", "/api/health")
|
||||
|
||||
async def snapshot(self) -> dict[str, Any]:
|
||||
"""Return devices, groups and the control plan in one restricted request."""
|
||||
data = await self._request("GET", "/api/integrations/home-assistant/snapshot")
|
||||
if not isinstance(data, dict):
|
||||
raise GreeControllerApiError("Controller returned an invalid snapshot payload")
|
||||
if not isinstance(data.get("devices"), list):
|
||||
raise GreeControllerApiError("Controller returned invalid snapshot devices")
|
||||
if not isinstance(data.get("control_plan"), dict):
|
||||
raise GreeControllerApiError("Controller returned an invalid snapshot control plan")
|
||||
if not isinstance(data.get("groups"), list):
|
||||
raise GreeControllerApiError("Controller returned invalid snapshot groups")
|
||||
return data
|
||||
|
||||
async def devices(self) -> list[dict[str, Any]]:
|
||||
"""Return all controller devices."""
|
||||
data = await self._request("GET", "/api/integrations/home-assistant/devices")
|
||||
+94
-12
@@ -39,6 +39,40 @@ HA_TO_MODE = {value: key for key, value in MODE_TO_HA.items()}
|
||||
FAN_TO_NAME = {0: "auto", 1: "low", 2: "medium_low", 3: "medium", 4: "medium_high", 5: "high"}
|
||||
NAME_TO_FAN = {value: key for key, value in FAN_TO_NAME.items()}
|
||||
|
||||
VERTICAL_SWING_TO_VALUE = {
|
||||
SWING_OFF: 0,
|
||||
SWING_ON: 1,
|
||||
"fixed_upper": 2,
|
||||
"fixed_upper_middle": 3,
|
||||
"fixed_middle": 4,
|
||||
"fixed_lower_middle": 5,
|
||||
"fixed_lower": 6,
|
||||
"swing_upper": 7,
|
||||
"swing_upper_middle": 8,
|
||||
"swing_middle": 9,
|
||||
"swing_lower_middle": 10,
|
||||
"swing_lower": 11,
|
||||
}
|
||||
HORIZONTAL_SWING_TO_VALUE = {
|
||||
SWING_OFF: 0,
|
||||
SWING_ON: 1,
|
||||
"fixed_left": 2,
|
||||
"fixed_left_middle": 3,
|
||||
"fixed_middle": 4,
|
||||
"fixed_right_middle": 5,
|
||||
"fixed_right": 6,
|
||||
}
|
||||
VALUE_TO_VERTICAL_SWING = {value: mode for mode, value in VERTICAL_SWING_TO_VALUE.items()}
|
||||
VALUE_TO_HORIZONTAL_SWING = {value: mode for mode, value in HORIZONTAL_SWING_TO_VALUE.items()}
|
||||
|
||||
|
||||
def _louver_mode(raw: Any, modes: dict[int, str]) -> str:
|
||||
try:
|
||||
value = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
value = 0
|
||||
return modes.get(value, SWING_OFF)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -126,14 +160,15 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat
|
||||
"""Home Assistant climate entity controlled through the Rust service."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "direct_control"
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_min_temp = 8.0
|
||||
_attr_max_temp = 30.0
|
||||
_attr_target_temperature_step = 1.0
|
||||
_attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT, HVACMode.DRY, HVACMode.FAN_ONLY]
|
||||
_attr_fan_modes = list(NAME_TO_FAN)
|
||||
_attr_swing_modes = [SWING_OFF, SWING_ON]
|
||||
_attr_swing_horizontal_modes = [SWING_OFF, SWING_ON]
|
||||
_attr_swing_modes = list(VERTICAL_SWING_TO_VALUE)
|
||||
_attr_swing_horizontal_modes = list(HORIZONTAL_SWING_TO_VALUE)
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
@@ -200,11 +235,11 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat
|
||||
|
||||
@property
|
||||
def swing_mode(self) -> str:
|
||||
return SWING_ON if self._device.get("swing_vertical", False) else SWING_OFF
|
||||
return _louver_mode(self._device.get("swing_vertical", 0), VALUE_TO_VERTICAL_SWING)
|
||||
|
||||
@property
|
||||
def swing_horizontal_mode(self) -> str:
|
||||
return SWING_ON if self._device.get("swing_horizontal", False) else SWING_OFF
|
||||
return _louver_mode(self._device.get("swing_horizontal", 0), VALUE_TO_HORIZONTAL_SWING)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
@@ -219,6 +254,8 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat
|
||||
"air": bool(device.get("air", False)),
|
||||
"health": bool(device.get("health", False)),
|
||||
"sleep": bool(device.get("sleep", False)),
|
||||
"vertical_louver_position": int(device.get("swing_vertical", 0) or 0),
|
||||
"horizontal_louver_position": int(device.get("swing_horizontal", 0) or 0),
|
||||
"last_seen": device.get("last_seen"),
|
||||
}
|
||||
|
||||
@@ -250,15 +287,20 @@ class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], Climat
|
||||
await self._command({"fan_speed": NAME_TO_FAN[fan_mode]})
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
await self._command({"swing_vertical": swing_mode == SWING_ON})
|
||||
value = VERTICAL_SWING_TO_VALUE.get(swing_mode)
|
||||
if value is not None:
|
||||
await self._command({"swing_vertical": value})
|
||||
|
||||
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
|
||||
await self._command({"swing_horizontal": swing_horizontal_mode == SWING_ON})
|
||||
value = HORIZONTAL_SWING_TO_VALUE.get(swing_horizontal_mode)
|
||||
if value is not None:
|
||||
await self._command({"swing_horizontal": value})
|
||||
|
||||
class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], ClimateEntity):
|
||||
"""Full climate entity for a controller thermostat zone."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "zone_thermostat"
|
||||
_attr_name = "Thermostat"
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_min_temp = 8.0
|
||||
@@ -266,12 +308,8 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
|
||||
_attr_target_temperature_step = 0.5
|
||||
_attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT]
|
||||
_attr_preset_modes = ["auto", "comfort", "sleep", "away"]
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
_attr_swing_modes = list(VERTICAL_SWING_TO_VALUE)
|
||||
_attr_swing_horizontal_modes = list(HORIZONTAL_SWING_TO_VALUE)
|
||||
|
||||
def __init__(self, coordinator: GreeControllerCoordinator, zone_id: str) -> None:
|
||||
super().__init__(coordinator)
|
||||
@@ -285,10 +323,30 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
|
||||
return zone
|
||||
return {}
|
||||
|
||||
@property
|
||||
def _device(self) -> dict[str, Any]:
|
||||
device_id = str(self._zone.get("device_id") or "").strip()
|
||||
return self.coordinator.data.get(device_id, {}) if device_id else {}
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return super().available and bool(self._zone)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> ClimateEntityFeature:
|
||||
features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
capabilities = self._device.get("capabilities") or {}
|
||||
if capabilities.get("vertical_swing", True) is not False:
|
||||
features |= ClimateEntityFeature.SWING_MODE
|
||||
if capabilities.get("horizontal_swing", True) is not False:
|
||||
features |= ClimateEntityFeature.SWING_HORIZONTAL_MODE
|
||||
return features
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
zone = self._zone
|
||||
@@ -308,6 +366,8 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
value = self._zone.get("target_temperature")
|
||||
if value is None:
|
||||
value = self._zone.get("setpoint")
|
||||
return float(value) if value is not None else None
|
||||
|
||||
@property
|
||||
@@ -323,6 +383,14 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
|
||||
def preset_mode(self) -> str:
|
||||
return str(self._zone.get("preset_override") or "auto")
|
||||
|
||||
@property
|
||||
def swing_mode(self) -> str:
|
||||
return _louver_mode(self._device.get("swing_vertical", 0), VALUE_TO_VERTICAL_SWING)
|
||||
|
||||
@property
|
||||
def swing_horizontal_mode(self) -> str:
|
||||
return _louver_mode(self._device.get("swing_horizontal", 0), VALUE_TO_HORIZONTAL_SWING)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
zone = self._zone
|
||||
@@ -352,6 +420,8 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
|
||||
"actual_mode": zone.get("actual_mode"),
|
||||
"actual_setpoint": zone.get("actual_setpoint"),
|
||||
"current_schedule": zone.get("current_schedule_name"),
|
||||
"vertical_louver_position": int(self._device.get("swing_vertical", 0) or 0),
|
||||
"horizontal_louver_position": int(self._device.get("swing_horizontal", 0) or 0),
|
||||
}
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
@@ -378,6 +448,18 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
|
||||
return
|
||||
await self.coordinator.async_zone_control(self._zone_id, {"preset": preset_mode})
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
device_id = str(self._zone.get("device_id") or "").strip()
|
||||
value = VERTICAL_SWING_TO_VALUE.get(swing_mode)
|
||||
if device_id and value is not None:
|
||||
await self.coordinator.async_device_command(device_id, {"swing_vertical": value})
|
||||
|
||||
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
|
||||
device_id = str(self._zone.get("device_id") or "").strip()
|
||||
value = HORIZONTAL_SWING_TO_VALUE.get(swing_horizontal_mode)
|
||||
if device_id and value is not None:
|
||||
await self.coordinator.async_device_command(device_id, {"swing_horizontal": value})
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
await self.coordinator.async_zone_control(self._zone_id, {"power": True})
|
||||
|
||||
+8
-5
@@ -40,11 +40,10 @@ class GreeControllerCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict]:
|
||||
try:
|
||||
devices, plan, groups = await asyncio.gather(
|
||||
self.client.devices(),
|
||||
self.client.control_plan(),
|
||||
self.client.groups(),
|
||||
)
|
||||
snapshot = await self.client.snapshot()
|
||||
devices = snapshot["devices"]
|
||||
plan = snapshot["control_plan"]
|
||||
groups = snapshot["groups"]
|
||||
except GreeControllerApiError as err:
|
||||
raise UpdateFailed(str(err)) from err
|
||||
self._overlay_pending_zone_controls(plan)
|
||||
@@ -68,6 +67,10 @@ class GreeControllerCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||
expected["target_temperature"] = float(int(value + 0.5))
|
||||
if "fan_speed" in expected:
|
||||
expected["fan_speed"] = min(5, max(0, int(expected["fan_speed"])))
|
||||
if "swing_vertical" in expected:
|
||||
expected["swing_vertical"] = min(11, max(0, int(expected["swing_vertical"])))
|
||||
if "swing_horizontal" in expected:
|
||||
expected["swing_horizontal"] = min(6, max(0, int(expected["swing_horizontal"])))
|
||||
return expected
|
||||
|
||||
def _overlay_pending_device_commands(self, devices: dict[str, dict[str, Any]]) -> None:
|
||||
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.8.10",
|
||||
"version": "0.15.17",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
+2
@@ -62,6 +62,8 @@ class GreeControllerZoneTargetNumber(CoordinatorEntity[GreeControllerCoordinator
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
value = self._zone.get("target_temperature")
|
||||
if value is None:
|
||||
value = self._zone.get("setpoint")
|
||||
return float(value) if value is not None else None
|
||||
|
||||
@property
|
||||
+3
-3
@@ -63,7 +63,9 @@ class GreeControllerHousePlanSensor(CoordinatorEntity[GreeControllerCoordinator]
|
||||
"outdoor_temperature": plan.get("outdoor_temperature"),
|
||||
"control_strategy": plan.get("control_strategy"),
|
||||
"work_profile": plan.get("house_preset"),
|
||||
"master_power": bool(plan.get("house_power", False)),
|
||||
"all_units_powered": bool(self.coordinator.data) and all(
|
||||
bool(device.get("power")) for device in self.coordinator.data.values() if device.get("enabled", True)
|
||||
),
|
||||
"enabled_zones": sum(bool(zone.get("enabled")) for zone in zones),
|
||||
"effective_enabled_zones": sum(bool(zone.get("effective_enabled", zone.get("enabled"))) for zone in zones),
|
||||
"demanding_zones": sum(bool(zone.get("demand")) for zone in zones),
|
||||
@@ -165,8 +167,6 @@ class GreeControllerGroupPlanSensor(CoordinatorEntity[GreeControllerCoordinator]
|
||||
group = self._group
|
||||
if not group.get("power_enabled", False):
|
||||
return "off"
|
||||
if not group.get("effective_power", False):
|
||||
return "master_off"
|
||||
if group.get("mode") == "house" and group.get("house_mode") == "off":
|
||||
return "paused"
|
||||
return "requesting" if int(group.get("demanding_zones") or 0) > 0 else "satisfied"
|
||||
+3
-2
@@ -62,7 +62,8 @@ class GreeControllerHousePowerSwitch(CoordinatorEntity[GreeControllerCoordinator
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
return bool(self.coordinator.plan.get("house_power", False))
|
||||
devices = [device for device in self.coordinator.data.values() if device.get("enabled", True)]
|
||||
return bool(devices) and all(bool(device.get("power")) for device in devices)
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
@@ -83,7 +84,7 @@ class GreeControllerHousePowerSwitch(CoordinatorEntity[GreeControllerCoordinator
|
||||
|
||||
|
||||
class GreeControllerGroupPowerSwitch(CoordinatorEntity[GreeControllerCoordinator], SwitchEntity):
|
||||
"""Enable or disable one controller climate group."""
|
||||
"""Power a controller climate group on or off."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Power"
|
||||
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"title": "GREE Controller",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to GREE Controller",
|
||||
"description": "Connect Home Assistant to the standalone Rust controller. Device commands will be proxied through the controller instead of the built-in GREE integration.",
|
||||
"data": {
|
||||
"url": "Controller URL",
|
||||
"token": "API token"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Cannot connect to GREE Controller",
|
||||
"invalid_auth": "Invalid controller API token"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "GREE Controller is already configured"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"climate": {
|
||||
"direct_control": {
|
||||
"name": "Direct control",
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"auto": "Auto",
|
||||
"low": "Low",
|
||||
"medium_low": "Medium-low",
|
||||
"medium": "Medium",
|
||||
"medium_high": "Medium-high",
|
||||
"high": "High"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"on": "Full range (Auto)",
|
||||
"fixed_upper": "Fixed: Top",
|
||||
"fixed_upper_middle": "Fixed: Upper-middle",
|
||||
"fixed_middle": "Fixed: Middle",
|
||||
"fixed_lower_middle": "Fixed: Lower-middle",
|
||||
"fixed_lower": "Fixed: Bottom",
|
||||
"swing_upper": "Swing: Top",
|
||||
"swing_upper_middle": "Swing: Upper-middle",
|
||||
"swing_middle": "Swing: Middle",
|
||||
"swing_lower_middle": "Swing: Lower-middle",
|
||||
"swing_lower": "Swing: Bottom"
|
||||
}
|
||||
},
|
||||
"swing_horizontal_mode": {
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"on": "Full range (Auto)",
|
||||
"fixed_left": "Fixed: Left",
|
||||
"fixed_left_middle": "Fixed: Left-middle",
|
||||
"fixed_middle": "Fixed: Middle",
|
||||
"fixed_right_middle": "Fixed: Middle-right",
|
||||
"fixed_right": "Fixed: Right"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"zone_thermostat": {
|
||||
"name": "Thermostat",
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"auto": "Auto schedule",
|
||||
"comfort": "Comfort",
|
||||
"sleep": "Sleep",
|
||||
"away": "Away"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"on": "Full range (Auto)",
|
||||
"fixed_upper": "Fixed: Top",
|
||||
"fixed_upper_middle": "Fixed: Upper-middle",
|
||||
"fixed_middle": "Fixed: Middle",
|
||||
"fixed_lower_middle": "Fixed: Lower-middle",
|
||||
"fixed_lower": "Fixed: Bottom",
|
||||
"swing_upper": "Swing: Top",
|
||||
"swing_upper_middle": "Swing: Upper-middle",
|
||||
"swing_middle": "Swing: Middle",
|
||||
"swing_lower_middle": "Swing: Lower-middle",
|
||||
"swing_lower": "Swing: Bottom"
|
||||
}
|
||||
},
|
||||
"swing_horizontal_mode": {
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"on": "Full range (Auto)",
|
||||
"fixed_left": "Fixed: Left",
|
||||
"fixed_left_middle": "Fixed: Left-middle",
|
||||
"fixed_middle": "Fixed: Middle",
|
||||
"fixed_right_middle": "Fixed: Middle-right",
|
||||
"fixed_right": "Fixed: Right"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"title": "GREE Controller",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Połącz z GREE Controller",
|
||||
"description": "Połącz Home Assistant z niezależnym kontrolerem Rust. Polecenia urządzeń będą przechodziły przez kontroler zamiast wbudowanej integracji GREE.",
|
||||
"data": {
|
||||
"url": "Adres URL kontrolera",
|
||||
"token": "Token API"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nie można połączyć się z GREE Controller",
|
||||
"invalid_auth": "Nieprawidłowy token API kontrolera"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "GREE Controller jest już skonfigurowany"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"climate": {
|
||||
"direct_control": {
|
||||
"name": "Sterowanie bezpośrednie",
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"auto": "Automatyczny",
|
||||
"low": "Niski",
|
||||
"medium_low": "Średnio-niski",
|
||||
"medium": "Średni",
|
||||
"medium_high": "Średnio-wysoki",
|
||||
"high": "Wysoki"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "Wyłączony",
|
||||
"on": "Pełny zakres (Auto)",
|
||||
"fixed_upper": "Stała: Góra",
|
||||
"fixed_upper_middle": "Stała: Środek-góra",
|
||||
"fixed_middle": "Stała: Środek",
|
||||
"fixed_lower_middle": "Stała: Środek-dół",
|
||||
"fixed_lower": "Stała: Dół",
|
||||
"swing_upper": "Ruch: Góra",
|
||||
"swing_upper_middle": "Ruch: Środek-góra",
|
||||
"swing_middle": "Ruch: Środek",
|
||||
"swing_lower_middle": "Ruch: Środek-dół",
|
||||
"swing_lower": "Ruch: Dół"
|
||||
}
|
||||
},
|
||||
"swing_horizontal_mode": {
|
||||
"state": {
|
||||
"off": "Wyłączony",
|
||||
"on": "Pełny zakres (Auto)",
|
||||
"fixed_left": "Stała: Lewo",
|
||||
"fixed_left_middle": "Stała: Lewo-środek",
|
||||
"fixed_middle": "Stała: Środek",
|
||||
"fixed_right_middle": "Stała: Środek-prawo",
|
||||
"fixed_right": "Stała: Prawo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"zone_thermostat": {
|
||||
"name": "Termostat",
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"auto": "Harmonogram (Auto)",
|
||||
"comfort": "Komfort",
|
||||
"sleep": "Sen",
|
||||
"away": "Poza domem"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "Wyłączony",
|
||||
"on": "Pełny zakres (Auto)",
|
||||
"fixed_upper": "Stała: Góra",
|
||||
"fixed_upper_middle": "Stała: Środek-góra",
|
||||
"fixed_middle": "Stała: Środek",
|
||||
"fixed_lower_middle": "Stała: Środek-dół",
|
||||
"fixed_lower": "Stała: Dół",
|
||||
"swing_upper": "Ruch: Góra",
|
||||
"swing_upper_middle": "Ruch: Środek-góra",
|
||||
"swing_middle": "Ruch: Środek",
|
||||
"swing_lower_middle": "Ruch: Środek-dół",
|
||||
"swing_lower": "Ruch: Dół"
|
||||
}
|
||||
},
|
||||
"swing_horizontal_mode": {
|
||||
"state": {
|
||||
"off": "Wyłączony",
|
||||
"on": "Pełny zakres (Auto)",
|
||||
"fixed_left": "Stała: Lewo",
|
||||
"fixed_left_middle": "Stała: Lewo-środek",
|
||||
"fixed_middle": "Stała: Środek",
|
||||
"fixed_right_middle": "Stała: Środek-prawo",
|
||||
"fixed_right": "Stała: Prawo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,4 +6,4 @@
|
||||
"device_id": "gree-aabbccddeeff"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
# Changelog
|
||||
|
||||
## 0.15.17
|
||||
|
||||
- Hides Dashboard → Global power Emergency STOP whenever all units are off and no automatic control is currently requesting work; an active STOP always keeps the green return-to-normal action visible.
|
||||
- Improves Control plan → Recently triggered automations so Flow-generated entries show the source Flow name, for example `Generated by Flow: Igor - Heating`, instead of only the generated automation identifier.
|
||||
- Refreshes Emergency STOP visibility immediately from live device and control-plan state.
|
||||
|
||||
## 0.15.16
|
||||
|
||||
- Makes Dashboard → Global power emergency STOP context-aware instead of permanently prominent whenever any zone is enabled.
|
||||
- Keeps the yellow STOP prominent while any unit is powered or automatic thermostat control is currently active.
|
||||
- Hides STOP while the system is idle; an active emergency STOP always remains visible as the green return-to-normal action.
|
||||
- Refreshes the STOP state immediately on device, schedule, automation, Flow and control-plan realtime updates.
|
||||
|
||||
## 0.15.15
|
||||
|
||||
- Adds a persistent emergency STOP to Dashboard → Global power. Activating it stores a restart-resistant safety gate, clears queued automatic compressor actions, pauses schedules/thermostat automation and makes a one-time attempt to power off enabled units; startup does not replay the OFF command.
|
||||
- Adds a green return-to-normal action that releases the safety gate and immediately re-evaluates current conditions without forcing devices ON.
|
||||
- Expands Control plan with a full-width “why the controller is acting this way” section showing per-zone control source/reason and the most recently triggered automations with trigger context.
|
||||
- Keeps emergency-stop state operationally separate from configuration backup/import, and documents the new `POST /api/house/emergency-stop` endpoint in the API/OpenAPI contract.
|
||||
|
||||
## 0.15.14
|
||||
|
||||
- Fixes Home Assistant energy sampling in the packaged add-on when authentication comes from `SUPERVISOR_TOKEN`; background sampling no longer requires persisted manual URL/token fields.
|
||||
- Centralizes Home Assistant readiness detection so Supervisor authentication and standalone manual URL/token authentication use the same runtime check across background jobs and integration endpoints.
|
||||
|
||||
## 0.15.13
|
||||
|
||||
- Flow cards now show an explicit execution type: Schedule, Automation, Schedule + Automation, Draft, or None, based on the compiled Flow output.
|
||||
- Removes whole-card navigation to the Flow editor, so clicking statistics, compile counts, descriptions, and other non-action areas no longer opens the editor accidentally.
|
||||
- Adds a dedicated compact Editor button next to Simulator and gives both actions the same visual treatment.
|
||||
|
||||
## 0.15.12
|
||||
|
||||
- Flow cards now allow editing the description directly from the Flow list without opening the visual editor.
|
||||
- Adds a dedicated simple Flow simulator next to the editor button. It opens in a modal, uses the saved Flow, current live values by default, and allows quick scenario overrides plus simulation date/time.
|
||||
- Simulator results are intentionally simplified to clear “will run / will not run / blocked” outcomes with readable checked conditions and friendly block reasons, while remaining non-mutating.
|
||||
|
||||
## 0.15.11
|
||||
|
||||
- Routine Visual Flow edit confirmations now appear as a compact status inside the editor instead of global toasts for save/copy/cut/paste/duplicate/undo actions.
|
||||
- Adds right-click context menus: selected blocks can be copied, cut, duplicated or deleted; the canvas can add/paste/select-all/fit/undo; and connections can be removed directly.
|
||||
- Context-menu paste can place a copied subgraph at the clicked canvas position while preserving internal connections.
|
||||
- Adds `Ctrl/Cmd+Z` undo with a bounded edit history for block creation/deletion, connections, paste/duplicate/cut, block movement, inspector setting changes, template application, Flow name changes and Enabled state changes.
|
||||
- Updates the built-in shortcut reference and PL/EN labels for the new undo and context-menu workflow.
|
||||
|
||||
## 0.15.10
|
||||
|
||||
- Visual Flow adds keyboard-first authoring: `A`, `Insert` or `Ctrl/Cmd+K` opens block search, and `Enter` inserts the first matching block.
|
||||
- Flow editing now supports `Ctrl/Cmd+S` save, `Ctrl/Cmd+A/C/X/V/D`, Delete/Backspace, Escape, arrow-key movement, precise Shift+arrow movement, `Ctrl/Cmd+0` fit-to-view and `Ctrl/Cmd++/-` zoom.
|
||||
- The Flow editor includes an in-app keyboard shortcut reference available from the toolbar/mobile actions and with `?`.
|
||||
- Every Flow card can now export its `.flow.json` directly from the Flow list without opening the editor; the existing export endpoint/portable format is reused.
|
||||
|
||||
## 0.15.9
|
||||
|
||||
- Visual Flow `on_change` events are now latched while their upstream condition remains true and are acknowledged only after the downstream action executes successfully, so cooldowns, ownership suppression and same-cycle target conflicts no longer silently consume a change event.
|
||||
- Rolling mean/median blocks now require one complete configured time window after their runtime window starts or restarts before they can match; unavailable sources reset the rolling window instead of reusing partial history.
|
||||
- Thermostat action summaries now show explicit power and HVAC mode alongside the preset/target, so `power=false` is displayed as OFF instead of looking like a preset-only Auto action.
|
||||
- The Flow canvas adds drag-box multi-selection, keeps Shift/Ctrl/Cmd additive selection and moves the whole selection together.
|
||||
- Selected Flow blocks can be copied/pasted or duplicated with internal connections preserved; Ctrl/Cmd+C, Ctrl/Cmd+V and Ctrl/Cmd+D are supported.
|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
- Uses the inner GREE `ver` metadata as a discovery protocol hint (`V1.*` -> V1, `V2.*`/newer -> V2), while keeping unknown results in Auto mode.
|
||||
- Auto discovery now verifies the protocol during bind with V1/V2 fallback and persists the generation that actually succeeds; explicit V1/V2 discovery remains locked to the selected protocol.
|
||||
- The discovery review UI shows Auto for unresolved candidates instead of incorrectly labeling them V1.
|
||||
|
||||
## 0.15.6
|
||||
|
||||
- Removes the obsolete add-all `POST /api/discovery` endpoint and its compatibility-only merge path.
|
||||
- Local discovery now has one flow only: `POST /api/discovery/scan` followed by `POST /api/discovery/add`.
|
||||
- Simplifies selected-device binding: `/api/discovery/add` no longer accepts a redundant top-level `protocol_version`; each unit is bound strictly with the protocol detected during scan.
|
||||
- Removes unused discovery merge/export code and the matching obsolete OpenAPI schema.
|
||||
|
||||
## 0.15.5
|
||||
|
||||
- Local discovery no longer adds every detected unit automatically; scan results are reviewed first and only selected units are persisted and bound.
|
||||
- Discovery results show each unit model, MAC address, IP address and detected protocol, with already-added units clearly marked.
|
||||
- Explicit V1 AES-ECB or V2 AES-GCM discovery now strictly filters the other protocol and binds only with the selected protocol; Auto keeps V1/V2 fallback.
|
||||
- Adds `POST /api/discovery/scan` and `POST /api/discovery/add` for the selective local-discovery flow.
|
||||
|
||||
## 0.15.4
|
||||
|
||||
- Updates the Rust dependency stack to current major/minor releases, including Axum 0.8, Reqwest 0.13, Rusqlite 0.40, Rand 0.10, AES 0.9, AES-GCM 0.11, Base64 0.23, SHA-2 0.11, Tower HTTP 0.7 and utoipa-swagger-ui 9.
|
||||
- Migrates Axum route parameters and WebSocket text frames, Rand APIs, RustCrypto AES/AES-GCM APIs and Reqwest TLS configuration for the new dependency versions.
|
||||
- Updates the Home Assistant add-on Rust builder to Rust 1.98.1.
|
||||
- Pins direct Rust dependencies to exact updated releases; container builds generate a fresh lockfile before fetching and compiling with `--locked`.
|
||||
- Uses `ring` as the single Rustls crypto provider across Reqwest and the direct MQTT TLS client, avoiding the Rustls provider ambiguity introduced by Reqwest 0.13.
|
||||
|
||||
## 0.15.3
|
||||
|
||||
- Simplifies the built-in Swagger API documentation: removes the embedded release-history changelog, starts with all endpoint groups collapsed and hides the global Schemas section.
|
||||
- Documents the HTTP/WebSocket API, Swagger UI (`/api-docs`) and OpenAPI document (`/api-docs/openapi.json`) in the main documentation and Home Assistant add-on package.
|
||||
|
||||
## 0.15.2
|
||||
|
||||
- Adds a dedicated Visual Flow action for changing exactly one GREE unit function without modifying unrelated settings.
|
||||
- Legacy device automations now expose fan speed plus Quiet, Turbo, Panel Light, Air, X-Fan, Health and Sleep as optional single-field controls.
|
||||
|
||||
## 0.15.1
|
||||
|
||||
- Web UI now treats temporary WebSocket loss as a reconnecting state, escalates to a connection error only after 15 seconds, and returns cleanly to connected after recovery while preserving the existing HTTP fallback.
|
||||
|
||||
## 0.15.0
|
||||
|
||||
- Adds full vertical and horizontal louver positions to manual unit control and thermostat cards, with compact dropdowns that preserve the existing view layout.
|
||||
- Extends legacy automations, Visual Flow and the bundled Home Assistant climate entities with granular louver positions; legacy boolean swing API payloads remain accepted.
|
||||
- Direct add-on access no longer shows `Invalid access token` on initial page load; that validation message appears only after a token is submitted.
|
||||
- Home Assistant climate cards localize direct-control names, fan modes, thermostat presets and all vertical/horizontal louver positions while keeping stable technical mode IDs for services and automations.
|
||||
|
||||
## 0.14.24
|
||||
|
||||
- Public Custom Charts fetch chart metadata before language assets, so a chart loads only its selected language pack; missing chart IDs fall back to English.
|
||||
- Public Custom Charts support pinch-to-zoom, one-finger horizontal panning while zoomed and per-metric visibility toggles through the legend.
|
||||
- Missing public chart IDs now return `chart id not found`, the favicon is no longer redundantly pre-cached, and invalid Access Tokens show an inline authentication error.
|
||||
|
||||
## 0.14.23
|
||||
|
||||
- Code cleanup and small UX changes.
|
||||
|
||||
## 0.14.22
|
||||
|
||||
- Code cleanup and small UX changes.
|
||||
|
||||
## 0.14.21
|
||||
|
||||
- Code cleanup and small UX changes.
|
||||
|
||||
## 0.14.20
|
||||
|
||||
- Installs the Rust target standard library in a cacheable layer before project sources are copied, so source-only changes do not redownload/reinstall it.
|
||||
- Fetches Cargo dependencies before target-specific setup so amd64/arm64 reuse the same registry/git cache.
|
||||
- Separates compiled `target/` caches by target architecture to avoid multi-platform build contention while retaining incremental dependency reuse.
|
||||
|
||||
## 0.14.19
|
||||
|
||||
- Speeds up multi-architecture OCI builds by running Rust/Cargo on `BUILDPLATFORM` and cross-compiling to amd64/arm64 instead of compiling ARM under QEMU.
|
||||
- Reduces Docker build context and avoids invalidating Rust compilation for unrelated documentation/repository changes; only `docs/openapi.json` is copied because it is embedded by the binary.
|
||||
- Deduplicates `/api/integrations/gree-cloud/status` across the initial HTTP bootstrap and immediate WebSocket bootstrap render while keeping manual Cloud reconnect refreshes forced.
|
||||
|
||||
## 0.14.18
|
||||
|
||||
- Renames the shared segmented navigation class from feature-specific `automation-tabs` to generic `segmented-tabs`.
|
||||
- Generalizes shared configuration layout classes to `config-form`, `form-section`, `form-section-head`, `form-grid`, `sticky-form-actions` and `full-width-form`.
|
||||
- Generalizes reusable chart, enable-toggle and diagram classes used across History/public charts, Zones/Flow and Simulator/Flow editor.
|
||||
- Replaces Flow-palette-specific category modifiers with shared `block-category-*` classes also used by the block library.
|
||||
- Removes all references to the replaced shared class names without changing component behavior or visual styling.
|
||||
|
||||
## 0.14.17
|
||||
|
||||
- Makes History navigation visually identical to the Flow / Schedules / Automations segmented navigation.
|
||||
- Reuses the shared `automation-tabs` component for Overview, Zones, GREE devices, Energy, Pings, HA sensors and Custom chart.
|
||||
- Removes redundant History-only tab CSS, including its separate mobile overrides.
|
||||
|
||||
## 0.14.16
|
||||
|
||||
- Adds an explicit Saved Charts list to History → Custom Chart with load, edit, update and delete actions.
|
||||
- Keeps chart name, selected series and range editable when updating an existing saved chart instead of creating duplicates.
|
||||
- Adds a range selector to standalone public Custom Chart links; the public data endpoint accepts an optional `hours` override while keeping the shared series definition fixed.
|
||||
- Fits the standalone public chart to the viewport and prevents vertical page scrolling, including compact/mobile layouts.
|
||||
|
||||
## 0.14.15
|
||||
|
||||
- Treats TCP `8787` as the fixed internal Home Assistant add-on application/ingress port and validates the Supervisor-reported ingress port at startup.
|
||||
- Makes the add-on build fail when `run.sh`, `ingress_port` and watchdog port metadata diverge.
|
||||
- Uses the Home Assistant Supervisor network API to resolve the primary host IPv4 for generated chart-only share links instead of depending on the browser/ingress hostname.
|
||||
- Adds optional `public_chart_base_url` for reverse proxies, alternate hostnames or non-standard external routing while keeping the public surface limited to generated Custom Chart shares.
|
||||
- Uses Home Assistant's `[PORT:8787]` watchdog placeholder so Supervisor resolves the effective watchdog port from the add-on port contract.
|
||||
- Keeps standalone installations fully port-configurable through `GREE_CONTROLLER_BIND`.
|
||||
|
||||
## 0.14.14
|
||||
|
||||
- Changes History → Custom Charts links to open a standalone chart-only page instead of the full dashboard.
|
||||
- Persists chart shares behind random bearer URLs whose token is stored only as a hash; shared URLs do not expose metric/device selectors.
|
||||
- In Supervisor mode, requires `app_token` for direct dashboard/API/WebSocket access on port `8787`, while trusted HA ingress remains automatic.
|
||||
- Keeps only generated Custom Chart data public to the network; the add-on watchdog may call `/api/health` anonymously only from the Supervisor peer.
|
||||
- Makes the Home Assistant entity picker in `Reusable data -> Shared Flow input` explicit with a separate search box and selected `entity_id` field.
|
||||
- Filters the live Home Assistant entity catalog by friendly name, `entity_id`, current state, unit and device class while typing.
|
||||
- Keeps automatic Supervisor authentication, failed-test manual fallback and standalone Home Assistant behavior unchanged.
|
||||
|
||||
## 0.14.13
|
||||
|
||||
- Shows Home Assistant Supervisor authentication as automatic in the add-on and hides manual URL/token/TLS fields by default.
|
||||
- Unlocks a persisted manual Home Assistant URL/token fallback only after an automatic HA test fails; standalone installations keep their existing manual behavior.
|
||||
- Extends `Test HA` to verify the state registry and show a live sample entity/state in the success toast.
|
||||
- Adds a searchable live Home Assistant entity picker to `Reusable data -> Shared Flow input`, with source-type-aware fields and numeric filtering for numeric HA inputs.
|
||||
- Adds an authenticated compact Home Assistant entity-catalog endpoint for UI suggestions.
|
||||
|
||||
## 0.14.12
|
||||
|
||||
- Home Assistant add-on now uses the Supervisor-managed Home Assistant Core API connection automatically.
|
||||
- Enables `homeassistant_api` for the add-on and authenticates Core API requests with runtime `SUPERVISOR_TOKEN`.
|
||||
- Keeps the Supervisor token out of SQLite and prevents add-on UI edits from replacing stored standalone Home Assistant URL/token settings.
|
||||
- Standalone installations keep the existing manual Home Assistant URL and Long-Lived Access Token behavior unchanged.
|
||||
- Fixes custom select mouse hover/click inside modal dialogs by keeping each dropdown popover inside the same modal top-layer subtree; keyboard behavior is unchanged.
|
||||
|
||||
## 0.14.11
|
||||
|
||||
- Replaces font-dependent Unicode glyphs used as Web UI icons with a shared inline SVG icon set.
|
||||
- Covers navigation, theme/refresh controls, dialogs, menus, Flow editor/statuses, chart zoom/fullscreen controls, thermostat/device +/- controls, custom-select checks/chevrons and event-category icons.
|
||||
- Leaves semantic text symbols such as `°C`, `±`, comparison operators, mathematical multiplication and Flow expression notation as text.
|
||||
- Keeps the 0.14.10 full-width Night mode and Home Assistant / Sensors layout fix.
|
||||
|
||||
## 0.14.10
|
||||
|
||||
- Makes the standalone Night mode and Home Assistant / Sensors forms use the full available page width, matching the other application sections on desktop and responsive layouts.
|
||||
|
||||
## 0.14.9
|
||||
|
||||
- Replaces the font-dependent Unicode power symbol in Manual Control and thermostat quick power controls with an embedded SVG icon for consistent Android/Chrome rendering.
|
||||
- Home Assistant room-sensor stale/error event messages now include the affected `entity_id` and zone name.
|
||||
- Event rendering enriches older `ha.sensor_*` rows with the sensor alias/entity and zone when that context is available in saved metadata.
|
||||
|
||||
## 0.14.8
|
||||
|
||||
- Removes the remaining non-device `502 Bad Gateway` mappings from Home Assistant connection/entity diagnostics and notification tests.
|
||||
- Returns `400 Bad Request` for missing/invalid integration configuration and `424 Failed Dependency` when a configured external service cannot be reached or rejects the request.
|
||||
- Reclassifies background Home Assistant energy-read failures as external-dependency errors.
|
||||
- Keeps `502 Bad Gateway` only for actual Local/LAN GREE device transport failures.
|
||||
- Aligns API/OpenAPI error documentation with the runtime status mapping.
|
||||
|
||||
## 0.14.7
|
||||
|
||||
- Treats an unconfigured Home Assistant integration as an optional unavailable source when listing HA energy sensors instead of emitting a 502 error while opening split/multisplit installation settings.
|
||||
- Maps configured-but-failing Home Assistant energy discovery to the external-dependency response path rather than a device communication 502.
|
||||
- Fixes short custom-select menus near the bottom of dialogs opening far above their trigger by positioning from the menu's rendered height.
|
||||
|
||||
## 0.14.6
|
||||
|
||||
- Fixes custom select duplication when a select is moved in the DOM, including repeated History → Energy refreshes.
|
||||
- Hides the Energy history controls and Refresh action when no energy source is configured, leaving a single empty-state message.
|
||||
- Hardens global custom-select cleanup so dynamically removed controls do not leave stale wrappers or popup menus.
|
||||
|
||||
## 0.14.5
|
||||
|
||||
- Fixed the outdoor-temperature modal chart returning from fullscreen with fullscreen-sized layout remnants.
|
||||
- Added quick links from the outdoor-temperature modal to 7-day, 30-day, yearly and full History views.
|
||||
|
||||
- Replaces native single-choice dropdown UI across the Web UI with one consistent custom select matching the energy chart picker, including language/theme controls and dynamically generated Flow/history forms.
|
||||
- Loads only the active language pack at startup and fetches other language packs when selected.
|
||||
- Makes the dashboard outdoor temperature clickable and opens a modal with its last 24 hours of history.
|
||||
|
||||
## 0.14.4
|
||||
|
||||
- Removes the obsolete Home Assistant connection-test/default entity; HA connection testing uses only the configured URL and token.
|
||||
- `outdoor_entity_id` / `HA_OUTDOOR_ENTITY_ID` is the global Home Assistant outdoor-temperature sensor.
|
||||
- Adds optional per-zone `ha_outdoor_entity_id`; when empty, the zone uses the global outdoor sensor, and when set it overrides the outdoor source only for that zone.
|
||||
- Keeps Home Assistant room temperature sensors per zone via `ha_entity_id`.
|
||||
- Global and per-zone outdoor sensors are available in aliases, metrics/history and Home Assistant entity suggestions used by Visual Flow.
|
||||
|
||||
## 0.14.3
|
||||
|
||||
- Home Assistant connection test now validates the configured server and token without requiring any entity.
|
||||
|
||||
## 0.14.2
|
||||
|
||||
- Improves connectivity controls: Local/LAN ping measurement can be explicitly enabled/disabled in Settings, disabled workers gray out their interval/sample fields, Cloud REST/MQTT measurement settings live in a dedicated block, and History → Pings can hide/show jitter with one button while rendering jitter as a dashed line.
|
||||
- Adds a configurable background connectivity worker for Local/LAN units with measurement interval and sample-count settings.
|
||||
- Adds a History → Connectivity/Pings view with per-endpoint latency, jitter and packet-loss charts plus latest batch summaries.
|
||||
- Stores connectivity readings in SQLite using the same retention/compaction lifecycle as other history metrics and archives them to InfluxDB when enabled.
|
||||
- Keeps direct unit probing Local/LAN-only; GREE Cloud diagnostics instead measure optional REST response time and MQTT PINGRESP round-trip.
|
||||
- Adds opt-in GREE Cloud connectivity metrics with configurable interval/sample count; Cloud measurements are disabled by default.
|
||||
- Adds separate vertical and horizontal swing controls for local units in Manual Control, respecting per-device capabilities.
|
||||
- Adds vertical and horizontal swing controls to thermostat zone cards without switching the zone into manual device ownership.
|
||||
- Adds optional `swing_vertical` and `swing_horizontal` actions to legacy direct-device automations.
|
||||
- Extends Visual Flow so both `device_action` and `zone_thermostat` can control vertical/horizontal swing, and device-state selectors use readable swing labels.
|
||||
- Prevents swing-enabled thermostat Flows from being reduced to native schedules, preserving the auxiliary swing action at runtime.
|
||||
- Exposes vertical and horizontal swing on Home Assistant zone thermostat climate entities when supported by the assigned unit.
|
||||
|
||||
## 0.14.1
|
||||
|
||||
- Adds split and multisplit installation groups in the Devices section, including assignment of indoor units to a shared outdoor installation.
|
||||
- Adds installation-level energy sources so a multisplit can use one cumulative GREE Cloud meter or one Home Assistant energy entity instead of reporting duplicated per-unit consumption.
|
||||
- Shows on each grouped device that its energy is provided by the assigned split/multisplit installation and identifies the configured shared source.
|
||||
- Adds an optional shared outdoor-temperature source for an installation, allowing all member units to use the outdoor temperature reported by one selected unit.
|
||||
- Adds installation groups to configuration export/import, bootstrap/API data and persistence, with validation that prevents duplicate membership and invalid energy/outdoor source references.
|
||||
- Keeps installation configuration consistent when devices are removed, including removing empty groups and resetting a deleted GREE Cloud energy source safely.
|
||||
- Adds multi-select energy charts so several devices and/or installations can be displayed together.
|
||||
- Adds energy period comparison against the previous day, previous equivalent period or previous year.
|
||||
- Fixes daily energy chart labels so daily aggregation is rendered as calendar dates instead of timestamps such as `02:00`.
|
||||
- Unifies GREE Cloud validation/error alerts with the alert style used for device errors and removes redundant styling paths.
|
||||
- Hides empty GREE Cloud status fields such as disabled/disconnected, `0 / 0` and `Unavailable`; status details are shown only when useful data or an actionable error exists.
|
||||
- Preserves and updates the Polish/English translations used by the new installation, energy and Cloud UI, including the simplified Cloud details label (`Details` / `Szczegóły`).
|
||||
- Fixes repeated `GREE Cloud MQTT message does not match a registered device` warnings after deleting all Cloud devices by ignoring in-flight stale MQTT frames when no devices are registered and closing the now-unused MQTT session after the last device is unregistered.
|
||||
- Adds regression coverage ensuring stale MQTT payloads are ignored only when no Cloud devices remain, while unknown-device payloads still fail when registered devices exist.
|
||||
|
||||
## 0.14.0
|
||||
|
||||
- Adds GREE Cloud as a first-class provider while preserving the existing LAN/UDP transport.
|
||||
- Adds MQTT/TLS status push, Cloud commands, reconnect/resubscribe lifecycle and safe Cloud diagnostics.
|
||||
- Adds per-device capabilities and Cloud-specific Devices/Manual Control UI.
|
||||
- Adds GREE Cloud and Home Assistant cumulative-energy sources, delta conversion, energy history, aggregation, charts and InfluxDB archive support.
|
||||
- Keeps Local and Cloud entries independent and never performs automatic transport fallback.
|
||||
|
||||
## 0.13.10
|
||||
|
||||
- Accepts multi-state non-zero values for optional GREE feature flags without dropping an otherwise valid status frame.
|
||||
- Preserves unit-specific active `Quiet` encodings (`1`, `2` or `3`) and reuses them when Quiet is enabled; legacy units still default to `Quiet=1`.
|
||||
- Prevents units reporting `Quiet=2` from being marked offline because of parser validation.
|
||||
|
||||
## 0.13.9
|
||||
|
||||
- Home Assistant add-on with `amd64` + `aarch64` multi-arch OCI image.
|
||||
- Host networking for GREE UDP discovery and multi-interface/VLAN hosts.
|
||||
- Home Assistant ingress, watchdog and cold backup support.
|
||||
- Polish and English documentation consolidated into one `DOCS.md`.
|
||||
@@ -0,0 +1,144 @@
|
||||
# GREE Controller — Home Assistant add-on
|
||||
|
||||
Repozytorium: `https://git.linuxiarz.pl/gru/gree-controller-ha-addon/`
|
||||
Obraz OCI: `zot.linuxiarz.pl/gree-controller:<version>`
|
||||
|
||||
---
|
||||
|
||||
## PL
|
||||
|
||||
### Instalacja
|
||||
|
||||
W Home Assistant dodaj jako własne repozytorium:
|
||||
|
||||
```text
|
||||
https://git.linuxiarz.pl/gru/gree-controller-ha-addon/
|
||||
```
|
||||
|
||||
Odśwież sklep dodatków/aplikacji i zainstaluj **GREE Controller**. `config.yaml` wskazuje gotowy wieloarchitekturowy obraz OCI; jego tag musi być równy polu `version`.
|
||||
|
||||
### Sieć, VLAN i discovery
|
||||
|
||||

|
||||
|
||||
Dodatek celowo używa `host_network: true`, więc korzysta bezpośrednio z interfejsów hosta Home Assistant OS. Nie twórz dla niego Docker `macvlan`.
|
||||
|
||||
Interfejsy hosta:
|
||||
|
||||
```bash
|
||||
ha network info
|
||||
```
|
||||
|
||||
Przykład VLAN 50 na `eth0`, bez dodatkowej bramy domyślnej:
|
||||
|
||||
```bash
|
||||
ha network vlan eth0 50 \
|
||||
--ipv4-method static \
|
||||
--ipv4-address 192.168.50.2/24 \
|
||||
--ipv6-method disabled
|
||||
```
|
||||
|
||||
Dla jednej sieci GREE ustaw `gree_interface` na nazwę interfejsu (np. `eth0.50`) albo jego lokalny IPv4 (np. `192.168.50.2`) oraz `discovery_broadcast` na broadcast tej podsieci, np. `192.168.50.255:7000`.
|
||||
|
||||
Dla wielu bezpośrednio podłączonych podsieci pozostaw `gree_interface` puste. Kontroler dobiera lokalny interfejs najlepiej pasujący do IP znanego urządzenia. Discovery nadal wysyła jeden broadcast na skan, więc każdą podsieć/VLAN skanuj osobno, zmieniając `discovery_broadcast`, albo dodaj znane urządzenia ręcznie.
|
||||
|
||||
Broadcast zwykle nie przechodzi przez router. Sterowanie unicast może działać przez routing/firewall, jeżeli UDP jest dozwolone. Gdy urządzenia są w routowanym VLAN-ie, zapewnij hostowi HA interfejs w tej sieci, relay broadcast UDP albo wykonuj discovery lokalnie dla każdej podsieci.
|
||||
|
||||
### Opcje
|
||||
|
||||
| Opcja | Znaczenie |
|
||||
|---|---|
|
||||
| `gree_interface` | interfejs lub lokalny IPv4; puste = automatyczny dobór trasy |
|
||||
| `discovery_broadcast` | cel discovery UDP, np. `192.168.50.255:7000` |
|
||||
| `simulate` | praca bez fizycznych urządzeń |
|
||||
| `auto_seed` | przykładowe urządzenie w pustej bazie symulatora |
|
||||
| `poll_interval_seconds` | interwał odpytywania urządzeń |
|
||||
| `zone_interval_seconds` | interwał sterowania strefami/termostatem |
|
||||
| `discovery_timeout_ms` | timeout discovery UDP |
|
||||
| `app_token` | token bezpośredniego Web UI/API; w trybie Supervisor pusty = direct access zablokowany |
|
||||
| `public_chart_base_url` | opcjonalny bazowy URL publicznych linków Custom Chart; puste = automatyczne główne IPv4 hosta HA + `:8787` |
|
||||
| `log_level` | `error`, `warn`, `info`, `debug`, `trace` |
|
||||
|
||||
### API i dokumentacja API
|
||||
|
||||
Aplikacja udostępnia HTTP API pod `/api/*` oraz WebSocket pod `/ws`. Interaktywna dokumentacja Swagger jest dostępna pod `/api-docs`, a dokument OpenAPI 3.1 pod `/api-docs/openapi.json`. Te same ścieżki działają przez ingress Home Assistant; bezpośredni dostęp na porcie `8787` podlega regułom `app_token`.
|
||||
|
||||
### Home Assistant API
|
||||
|
||||
Dodatek korzysta automatycznie z wewnętrznego proxy Home Assistant Core (`http://supervisor/core/api/`) i tokenu `SUPERVISOR_TOKEN` przekazywanego przez Supervisor. Gdy token jest wykryty, UI pokazuje automatyczną autoryzację i ukrywa ręczne pola URL/token. Dopiero nieudany `Test HA` odblokowuje awaryjny ręczny fallback. Token Supervisor jest używany tylko w pamięci procesu i nie jest zapisywany w SQLite.
|
||||
|
||||
### Dostęp, dane i bezpieczeństwo
|
||||
|
||||
Usługa używa stałego wewnętrznego portu TCP `8787`; ingress Home Assistant przekazuje Web UI na ten port. Port nie jest opcją użytkownika w zakładce Network. Przy starcie add-on porównuje port raportowany przez Supervisor z kontraktem `8787` i odmawia startu, jeśli ręcznie zmodyfikowana paczka jest niespójna. Gdy wykryty jest `SUPERVISOR_TOKEN`, bezpośredni dashboard/API na `:8787` wymaga `app_token`; przy pustym `app_token` bezpośredni dashboard jest zablokowany. Z sieci bez poświadczeń dostępny jest tylko publiczny widok i endpoint danych pojedynczego Custom Chart (`/charts/custom/<share-token>`, `/api/public/charts/custom/<share-token>`). Token udostępnienia jest losowy, jego skrót jest zapisywany w SQLite, a sam URL nie zawiera nazw urządzeń ani listy metryk. Link kopiowany z History → Custom chart omija HA ingress; domyślnie add-on pobiera główne IPv4 hosta z Supervisor API i tworzy `http://<IP-HA>:8787/charts/custom/...`. `public_chart_base_url` pozwala jawnie wskazać reverse proxy, inną nazwę hosta lub alternatywną trasę. `/api/health` bez tokenu jest akceptowane tylko od peera Supervisora na potrzeby watchdoga; bezpośrednie żądanie sieciowe wymaga `app_token`. Baza jest zapisywana w `/data/gree-controller.db`; konfiguracja używa `backup: cold`, więc dane są objęte backupem dodatku.
|
||||
|
||||
Watchdog sprawdza `/api/health`. Do diagnostyki sieci najpierw sprawdź `ha network info`, poprawność `gree_interface`, broadcast konkretnej podsieci i reguły UDP/firewalla.
|
||||
|
||||
---
|
||||
|
||||
## EN
|
||||
|
||||
### Installation
|
||||
|
||||
Add this custom repository in Home Assistant:
|
||||
|
||||
```text
|
||||
https://git.linuxiarz.pl/gru/gree-controller-ha-addon/
|
||||
```
|
||||
|
||||
Refresh the app/add-on store and install **GREE Controller**. `config.yaml` points to a pre-built multi-architecture OCI image; its tag must match `version`.
|
||||
|
||||
### Networking, VLANs and discovery
|
||||
|
||||

|
||||
|
||||
The add-on intentionally uses `host_network: true`, so it uses the Home Assistant OS host interfaces directly. Do not attach a Docker `macvlan` network to the add-on.
|
||||
|
||||
Inspect host interfaces with:
|
||||
|
||||
```bash
|
||||
ha network info
|
||||
```
|
||||
|
||||
Example VLAN 50 on `eth0`, without adding another default gateway:
|
||||
|
||||
```bash
|
||||
ha network vlan eth0 50 \
|
||||
--ipv4-method static \
|
||||
--ipv4-address 192.168.50.2/24 \
|
||||
--ipv6-method disabled
|
||||
```
|
||||
|
||||
For one GREE subnet, set `gree_interface` to the host interface name (for example `eth0.50`) or its local IPv4 address (for example `192.168.50.2`), and set `discovery_broadcast` to that subnet broadcast, e.g. `192.168.50.255:7000`.
|
||||
|
||||
For multiple directly attached subnets, leave `gree_interface` empty. The controller selects the local interface that best matches a known device IP. Discovery still sends one broadcast per scan, so scan each VLAN/subnet separately by changing `discovery_broadcast`, or add known devices manually.
|
||||
|
||||
Broadcast normally does not cross routers. Unicast control may work through routing/firewall rules when UDP is allowed. For routed GREE VLANs, give the HA host an interface in the VLAN, use a suitable UDP broadcast relay, or perform discovery locally per subnet.
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Meaning |
|
||||
|---|---|
|
||||
| `gree_interface` | interface or local IPv4; empty = automatic route selection |
|
||||
| `discovery_broadcast` | UDP discovery target, e.g. `192.168.50.255:7000` |
|
||||
| `simulate` | run without physical devices |
|
||||
| `auto_seed` | create a sample device in an empty simulator database |
|
||||
| `poll_interval_seconds` | device polling interval |
|
||||
| `zone_interval_seconds` | thermostat/zone control interval |
|
||||
| `discovery_timeout_ms` | UDP discovery timeout |
|
||||
| `app_token` | token for direct Web UI/API access; in Supervisor mode empty = direct access disabled |
|
||||
| `public_chart_base_url` | optional base URL for public Custom Chart links; empty = primary HA host IPv4 + `:8787` automatically |
|
||||
| `log_level` | `error`, `warn`, `info`, `debug`, `trace` |
|
||||
|
||||
### API and API documentation
|
||||
|
||||
The application exposes its HTTP API under `/api/*` and WebSocket under `/ws`. Interactive Swagger documentation is available at `/api-docs`, with the OpenAPI 3.1 document at `/api-docs/openapi.json`. The same paths work through Home Assistant ingress; direct access on port `8787` follows the configured `app_token` rules.
|
||||
|
||||
### Home Assistant API
|
||||
|
||||
The add-on automatically uses the internal Home Assistant Core proxy (`http://supervisor/core/api/`) and the runtime `SUPERVISOR_TOKEN` provided by Supervisor. When the token is detected, the UI shows automatic authorization and hides manual URL/token fields. A failed `Test HA` unlocks the optional manual fallback. The Supervisor token is used only at runtime and is not stored in SQLite.
|
||||
|
||||
### Access, data and security
|
||||
|
||||
The service uses fixed internal TCP port `8787`; Home Assistant ingress proxies the Web UI to that port. The port is not a user-facing Network option. On startup the add-on compares the Supervisor-reported ingress port with the `8787` contract and refuses to start if a manually modified package is inconsistent. When `SUPERVISOR_TOKEN` is detected, direct dashboard/API access on `:8787` requires `app_token`; with an empty `app_token`, direct dashboard access is disabled. From the network, the only unauthenticated application data is the single public Custom Chart view/data endpoint (`/charts/custom/<share-token>`, `/api/public/charts/custom/<share-token>`). The share token is random, only its hash is stored in SQLite, and the URL does not expose device names or metric selectors. Links copied from History → Custom chart bypass HA ingress; by default the add-on obtains the primary host IPv4 from the Supervisor API and builds `http://<HA-IP>:8787/charts/custom/...`. `public_chart_base_url` can explicitly select a reverse proxy, alternate hostname or routing path. `/api/health` is accepted without a token only from the Supervisor peer for the add-on watchdog; direct network requests require `app_token`. The database is stored in `/data/gree-controller.db`; `backup: cold` keeps it in the add-on backup.
|
||||
|
||||
The watchdog checks `/api/health`. For network troubleshooting, verify `ha network info`, `gree_interface`, the selected subnet broadcast, and UDP/firewall rules first.
|
||||
@@ -0,0 +1,17 @@
|
||||
# GREE Controller
|
||||
|
||||
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.17 hides the Global power emergency STOP while the system is idle, keeps the green return-to-normal action visible whenever STOP is active, and shows the source Flow name for Flow-generated entries in Recently triggered automations.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
Full documentation: [DOCS.md](./DOCS.md).
|
||||
|
||||
The controller exposes `/api/*` and `/ws`. Interactive API documentation is available at `/api-docs`, with the OpenAPI 3.1 document at `/api-docs/openapi.json`. The same paths work through Home Assistant ingress; direct access on port `8787` follows the configured `app_token` rules.
|
||||
|
||||
The same Git repository also contains the optional `custom_components/gree_controller` integration. It is distributed with the repository but is not installed automatically with the add-on.
|
||||
|
||||
Repository: https://git.linuxiarz.pl/gru/gree-controller-ha-addon/
|
||||
@@ -0,0 +1,45 @@
|
||||
name: "GREE Controller"
|
||||
version: "0.15.17"
|
||||
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/"
|
||||
arch:
|
||||
- amd64
|
||||
- aarch64
|
||||
image: "zot.linuxiarz.pl/gree-controller"
|
||||
startup: application
|
||||
boot: auto
|
||||
init: false
|
||||
host_network: true
|
||||
homeassistant_api: true
|
||||
hassio_api: true
|
||||
ingress: true
|
||||
ingress_port: 8787
|
||||
ingress_stream: true
|
||||
panel_icon: mdi:air-conditioner
|
||||
panel_title: GREE Controller
|
||||
panel_admin: true
|
||||
watchdog: "http://[HOST]:[PORT:8787]/api/health"
|
||||
backup: cold
|
||||
options:
|
||||
gree_interface: ""
|
||||
discovery_broadcast: "255.255.255.255:7000"
|
||||
simulate: false
|
||||
auto_seed: false
|
||||
poll_interval_seconds: 15
|
||||
zone_interval_seconds: 5
|
||||
discovery_timeout_ms: 3000
|
||||
app_token: ""
|
||||
public_chart_base_url: ""
|
||||
log_level: info
|
||||
schema:
|
||||
gree_interface: str
|
||||
discovery_broadcast: str
|
||||
simulate: bool
|
||||
auto_seed: bool
|
||||
poll_interval_seconds: "int(2,3600)"
|
||||
zone_interval_seconds: "int(2,3600)"
|
||||
discovery_timeout_ms: "int(500,30000)"
|
||||
app_token: password
|
||||
public_chart_base_url: str
|
||||
log_level: "list(error|warn|info|debug|trace)"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,31 @@
|
||||
configuration:
|
||||
gree_interface:
|
||||
name: GREE network interface
|
||||
description: Interface name or local IPv4 address. Leave empty for automatic routing.
|
||||
discovery_broadcast:
|
||||
name: Discovery broadcast
|
||||
description: Broadcast target, for example 192.168.50.255:7000. Use an explicit subnet broadcast for VLANs.
|
||||
simulate:
|
||||
name: Simulation mode
|
||||
description: Run without physical GREE units.
|
||||
auto_seed:
|
||||
name: Seed simulator
|
||||
description: Create a sample simulated unit when the database is empty.
|
||||
poll_interval_seconds:
|
||||
name: Poll interval
|
||||
description: Device polling interval in seconds.
|
||||
zone_interval_seconds:
|
||||
name: Zone interval
|
||||
description: Thermostat control interval in seconds.
|
||||
discovery_timeout_ms:
|
||||
name: Discovery timeout
|
||||
description: UDP discovery timeout in milliseconds.
|
||||
app_token:
|
||||
name: Application token
|
||||
description: Token required for direct dashboard/API access on port 8787. With SUPERVISOR_TOKEN active, an empty token disables direct access; only generated Custom Chart share links remain public.
|
||||
public_chart_base_url:
|
||||
name: Public chart base URL
|
||||
description: Optional override for generated Custom Chart links, for example http://192.168.1.20:8787 or a reverse-proxy URL. Leave empty to use the primary Home Assistant host IPv4 and the fixed add-on port 8787 automatically.
|
||||
log_level:
|
||||
name: Log level
|
||||
description: Controller log verbosity.
|
||||
@@ -0,0 +1,31 @@
|
||||
configuration:
|
||||
gree_interface:
|
||||
name: Interfejs sieci GREE
|
||||
description: Nazwa interfejsu lub lokalny adres IPv4. Puste pole oznacza automatyczny wybór trasy.
|
||||
discovery_broadcast:
|
||||
name: Broadcast discovery
|
||||
description: Adres broadcast, np. 192.168.50.255:7000. Dla VLAN użyj broadcastu konkretnej podsieci.
|
||||
simulate:
|
||||
name: Tryb symulacji
|
||||
description: Uruchomienie bez fizycznych urządzeń GREE.
|
||||
auto_seed:
|
||||
name: Dane testowe symulatora
|
||||
description: Dodaj przykładowe urządzenie, gdy baza jest pusta.
|
||||
poll_interval_seconds:
|
||||
name: Interwał odpytywania
|
||||
description: Odpytywanie urządzeń w sekundach.
|
||||
zone_interval_seconds:
|
||||
name: Interwał stref
|
||||
description: Interwał sterowania termostatem w sekundach.
|
||||
discovery_timeout_ms:
|
||||
name: Timeout discovery
|
||||
description: Timeout wykrywania UDP w milisekundach.
|
||||
app_token:
|
||||
name: Token aplikacji
|
||||
description: Token wymagany do bezpośredniego dostępu do dashboardu/API na porcie 8787. Przy aktywnym SUPERVISOR_TOKEN pusty token blokuje direct access; publiczne pozostają wyłącznie wygenerowane linki Custom Chart.
|
||||
public_chart_base_url:
|
||||
name: Bazowy URL publicznych wykresów
|
||||
description: Opcjonalne nadpisanie adresu generowanych linków Custom Chart, np. http://192.168.1.20:8787 albo adres reverse proxy. Pozostaw puste, aby automatycznie użyć głównego IPv4 hosta Home Assistant i stałego portu add-onu 8787.
|
||||
log_level:
|
||||
name: Poziom logowania
|
||||
description: Szczegółowość logów kontrolera.
|
||||
@@ -0,0 +1,3 @@
|
||||
name: GREE Controller
|
||||
url: https://git.linuxiarz.pl/gru/gree-controller-ha-addon/
|
||||
maintainer: linuxiarz.pl
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
CONFIG=/data/options.json
|
||||
readonly HA_HTTP_PORT=8787
|
||||
SUPERVISOR_API="${SUPERVISOR:-http://supervisor}"
|
||||
|
||||
read_option() {
|
||||
local key="$1" default_value="${2-}"
|
||||
if [[ -f "$CONFIG" ]]; then
|
||||
jq -r --arg key "$key" --arg default "$default_value" \
|
||||
'if has($key) and .[$key] != null then .[$key] else $default end' "$CONFIG"
|
||||
else
|
||||
printf '%s\n' "$default_value"
|
||||
fi
|
||||
}
|
||||
|
||||
supervisor_get() {
|
||||
local path="$1"
|
||||
[[ -n "${SUPERVISOR_TOKEN:-}" ]] || return 1
|
||||
curl --fail --silent --show-error --max-time 5 \
|
||||
-H "Authorization: Bearer ${SUPERVISOR_TOKEN}" \
|
||||
"${SUPERVISOR_API%/}${path}"
|
||||
}
|
||||
|
||||
validate_ingress_port_contract() {
|
||||
local payload ingress_port
|
||||
payload="$(supervisor_get /addons/self/info 2>/dev/null || true)"
|
||||
[[ -n "$payload" ]] || return 0
|
||||
ingress_port="$(jq -r '.data.ingress_port // empty' <<<"$payload" 2>/dev/null || true)"
|
||||
if [[ "$ingress_port" =~ ^[0-9]+$ ]] && (( ingress_port != HA_HTTP_PORT )); then
|
||||
printf 'GREE Controller: refusing to start: HA ingress_port=%s but the add-on HTTP port contract is %s. Reinstall/update the official add-on package instead of changing ingress_port manually.\n' \
|
||||
"$ingress_port" "$HA_HTTP_PORT" >&2
|
||||
exit 78
|
||||
fi
|
||||
}
|
||||
|
||||
detect_primary_host_ipv4() {
|
||||
local payload address
|
||||
payload="$(supervisor_get /network/info 2>/dev/null || true)"
|
||||
[[ -n "$payload" ]] || return 1
|
||||
address="$(jq -r '
|
||||
(.data.interfaces // [])
|
||||
| (if type == "object" then [to_entries[] | (.value + {interface: (.value.interface // .key)})] else . end)
|
||||
| map(select((.enabled // true) == true and (.connected // true) == true))
|
||||
| map(. + {candidate_ipv4: (.ipv4.ip_address // .ip_address // (try .ipv4.address[0] catch empty) // empty)})
|
||||
| map(select(.candidate_ipv4 != ""))
|
||||
| ((map(select((.primary // false) == true)) | .[0]) // .[0] // {})
|
||||
| (.candidate_ipv4 // empty)
|
||||
' <<<"$payload" 2>/dev/null || true)"
|
||||
address="${address%%/*}"
|
||||
[[ "$address" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
|
||||
printf '%s\n' "$address"
|
||||
}
|
||||
|
||||
GREE_INTERFACE="$(read_option gree_interface '')"
|
||||
DISCOVERY_BROADCAST="$(read_option discovery_broadcast '255.255.255.255:7000')"
|
||||
SIMULATE="$(read_option simulate 'false')"
|
||||
AUTO_SEED="$(read_option auto_seed 'false')"
|
||||
POLL_INTERVAL="$(read_option poll_interval_seconds '15')"
|
||||
ZONE_INTERVAL="$(read_option zone_interval_seconds '5')"
|
||||
DISCOVERY_TIMEOUT="$(read_option discovery_timeout_ms '3000')"
|
||||
APP_TOKEN="$(read_option app_token '')"
|
||||
PUBLIC_CHART_BASE_URL="$(read_option public_chart_base_url '')"
|
||||
LOG_LEVEL="$(read_option log_level 'info')"
|
||||
|
||||
validate_ingress_port_contract
|
||||
PUBLIC_CHART_BASE_SOURCE="configured"
|
||||
if [[ -z "$PUBLIC_CHART_BASE_URL" ]]; then
|
||||
PUBLIC_CHART_BASE_SOURCE="browser-fallback"
|
||||
if PRIMARY_HOST_IPV4="$(detect_primary_host_ipv4)"; then
|
||||
PUBLIC_CHART_BASE_URL="http://${PRIMARY_HOST_IPV4}:${HA_HTTP_PORT}"
|
||||
PUBLIC_CHART_BASE_SOURCE="supervisor-primary-ipv4"
|
||||
fi
|
||||
fi
|
||||
|
||||
export GREE_CONTROLLER_BIND="0.0.0.0:${HA_HTTP_PORT}"
|
||||
export GREE_CONTROLLER_DATABASE="/data/gree-controller.db"
|
||||
export GREE_CONTROLLER_GREE_INTERFACE="$GREE_INTERFACE"
|
||||
export GREE_CONTROLLER_DISCOVERY_BROADCAST="$DISCOVERY_BROADCAST"
|
||||
export GREE_CONTROLLER_SIMULATE="$SIMULATE"
|
||||
export GREE_CONTROLLER_AUTO_SEED="$AUTO_SEED"
|
||||
export GREE_CONTROLLER_POLL_INTERVAL_SECONDS="$POLL_INTERVAL"
|
||||
export GREE_CONTROLLER_ZONE_INTERVAL_SECONDS="$ZONE_INTERVAL"
|
||||
export GREE_CONTROLLER_DISCOVERY_TIMEOUT_MS="$DISCOVERY_TIMEOUT"
|
||||
export GREE_CONTROLLER_APP_TOKEN="$APP_TOKEN"
|
||||
export GREE_CONTROLLER_PUBLIC_CHART_BASE_URL="$PUBLIC_CHART_BASE_URL"
|
||||
export GREE_CONTROLLER_HA_AUTH="supervisor"
|
||||
export RUST_LOG="gree_controller=${LOG_LEVEL},tower_http=${LOG_LEVEL}"
|
||||
|
||||
printf 'GREE Controller: interface=%s discovery=%s database=%s http_port=%s public_chart_base_source=%s\n' \
|
||||
"${GREE_INTERFACE:-auto}" "$DISCOVERY_BROADCAST" "$GREE_CONTROLLER_DATABASE" "$HA_HTTP_PORT" \
|
||||
"$PUBLIC_CHART_BASE_SOURCE"
|
||||
exec /usr/local/bin/gree-controller
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
ENV_FILE="${HA_ADDON_ENV_FILE:-$SCRIPT_DIR/.env}"
|
||||
|
||||
[[ -f "$ENV_FILE" ]] || {
|
||||
echo "Missing $ENV_FILE. Copy $SCRIPT_DIR/.env.example to $SCRIPT_DIR/.env and edit it." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
required=(HA_REPO_URL HA_REPO_BRANCH HA_REPO_PATH ADDON_DIR INTEGRATION_DOMAIN)
|
||||
for name in "${required[@]}"; do
|
||||
[[ -n "${!name:-}" ]] || { echo "Missing $name in $ENV_FILE" >&2; exit 1; }
|
||||
done
|
||||
|
||||
command -v git >/dev/null 2>&1 || { echo "git is required" >&2; exit 1; }
|
||||
command -v rsync >/dev/null 2>&1 || { echo "rsync is required" >&2; exit 1; }
|
||||
|
||||
if [[ "$HA_REPO_PATH" != /* ]]; then
|
||||
HA_REPO_PATH="$PROJECT_ROOT/$HA_REPO_PATH"
|
||||
fi
|
||||
HA_REPO_PATH="$(realpath -m "$HA_REPO_PATH")"
|
||||
INTEGRATION_SOURCE="$SCRIPT_DIR/home-assistant/custom_components/$INTEGRATION_DOMAIN"
|
||||
|
||||
[[ -d "$SCRIPT_DIR/repository/$ADDON_DIR" ]] || { echo "Missing add-on source" >&2; exit 1; }
|
||||
[[ -d "$INTEGRATION_SOURCE" ]] || { echo "Missing integration source: $INTEGRATION_SOURCE" >&2; exit 1; }
|
||||
|
||||
"$SCRIPT_DIR/build.sh" render
|
||||
|
||||
if [[ ! -e "$HA_REPO_PATH" ]]; then
|
||||
git clone --branch "$HA_REPO_BRANCH" "$HA_REPO_URL" "$HA_REPO_PATH"
|
||||
elif [[ ! -d "$HA_REPO_PATH/.git" ]]; then
|
||||
echo "HA_REPO_PATH exists but is not a Git repository: $HA_REPO_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$(git -C "$HA_REPO_PATH" status --porcelain)" ]]; then
|
||||
echo "HA repository has uncommitted changes: $HA_REPO_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git -C "$HA_REPO_PATH" checkout "$HA_REPO_BRANCH"
|
||||
git -C "$HA_REPO_PATH" pull --ff-only origin "$HA_REPO_BRANCH"
|
||||
|
||||
STAGE="$(mktemp -d)"
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
rsync -a "$SCRIPT_DIR/repository/" "$STAGE/"
|
||||
mkdir -p "$STAGE/custom_components/$INTEGRATION_DOMAIN"
|
||||
rsync -a "$INTEGRATION_SOURCE/" "$STAGE/custom_components/$INTEGRATION_DOMAIN/"
|
||||
rsync -a --delete --exclude='.git/' "$STAGE/" "$HA_REPO_PATH/"
|
||||
|
||||
git -C "$HA_REPO_PATH" add -A
|
||||
if git -C "$HA_REPO_PATH" diff --cached --quiet; then
|
||||
echo "HA repository is already synchronized."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
VERSION="$(awk -F '"' '/^version = "/ {print $2; exit}' "$PROJECT_ROOT/Cargo.toml")"
|
||||
[[ -n "$VERSION" ]] || { echo "Cannot determine version from Cargo.toml" >&2; exit 1; }
|
||||
|
||||
git -C "$HA_REPO_PATH" commit -m "GREE Controller ${VERSION}"
|
||||
git -C "$HA_REPO_PATH" push origin "$HA_REPO_BRANCH"
|
||||
printf 'Synchronized add-on + custom integration -> %s (%s)\n' "$HA_REPO_URL" "$HA_REPO_BRANCH"
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# Home Assistant distribution
|
||||
|
||||
All Home Assistant packaging is maintained under [`ha-addon/`](ha-addon/).
|
||||
|
||||
```text
|
||||
ha-addon/
|
||||
├── .env.example
|
||||
├── build.sh
|
||||
├── sync-repository.sh
|
||||
├── repository/ # add-on repository source
|
||||
└── home-assistant/ # custom integration source
|
||||
```
|
||||
|
||||
Initial setup:
|
||||
|
||||
```bash
|
||||
cd ha-addon
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Release:
|
||||
|
||||
```bash
|
||||
./build.sh push
|
||||
./sync-repository.sh
|
||||
```
|
||||
|
||||
`build.sh` reads OCI/build settings from `.env`. `sync-repository.sh` publishes both the add-on and `custom_components/gree_controller` to the separate Home Assistant repository.
|
||||
|
||||
See [`ha-addon/README.md`](ha-addon/README.md) for details.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.9 KiB |
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"title": "GREE Controller",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to GREE Controller",
|
||||
"description": "Connect Home Assistant to the standalone Rust controller. Device commands will be proxied through the controller instead of the built-in GREE integration.",
|
||||
"data": {
|
||||
"url": "Controller URL",
|
||||
"token": "API token"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Cannot connect to GREE Controller",
|
||||
"invalid_auth": "Invalid controller API token"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "GREE Controller is already configured"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"title": "GREE Controller",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Połącz z GREE Controller",
|
||||
"description": "Połącz Home Assistant z niezależnym kontrolerem Rust. Polecenia urządzeń będą przechodziły przez kontroler zamiast wbudowanej integracji GREE.",
|
||||
"data": {
|
||||
"url": "Adres URL kontrolera",
|
||||
"token": "Token API"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nie można połączyć się z GREE Controller",
|
||||
"invalid_auth": "Nieprawidłowy token API kontrolera"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "GREE Controller jest już skonfigurowany"
|
||||
}
|
||||
}
|
||||
}
|
||||
+883
-43
File diff suppressed because it is too large
Load Diff
+889
-49
File diff suppressed because it is too large
Load Diff
Regular → Executable
+1
-1
@@ -67,4 +67,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"id": "bedroom_window_night",
|
||||
"category": "night",
|
||||
"name": {
|
||||
"pl": "Sypialnia: noc + zamknięte okno",
|
||||
"en": "Bedroom: night + closed window"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Łączy tryb nocny z NOT otwartego okna, aby profil sleep działał tylko przy zamkniętym oknie.",
|
||||
"en": "Combines night mode with NOT open-window logic so the sleep profile runs only with a closed window."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000148",
|
||||
"kind": "night_mode",
|
||||
"x": 35,
|
||||
"y": 65,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000149",
|
||||
"kind": "ha_state",
|
||||
"x": 35,
|
||||
"y": 185,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.bedroom_window",
|
||||
"operator": "eq",
|
||||
"value": "on"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000150",
|
||||
"kind": "logic_not",
|
||||
"x": 275,
|
||||
"y": 185,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000151",
|
||||
"kind": "logic_and",
|
||||
"x": 500,
|
||||
"y": 125,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000152",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 745,
|
||||
"y": 125,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "sleep",
|
||||
"setpoint": 20,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 180
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000153",
|
||||
"from": "node-00000000-0000-4000-8000-000000000149",
|
||||
"to": "node-00000000-0000-4000-8000-000000000150"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000154",
|
||||
"from": "node-00000000-0000-4000-8000-000000000148",
|
||||
"to": "node-00000000-0000-4000-8000-000000000151"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000155",
|
||||
"from": "node-00000000-0000-4000-8000-000000000150",
|
||||
"to": "node-00000000-0000-4000-8000-000000000151"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000156",
|
||||
"from": "node-00000000-0000-4000-8000-000000000151",
|
||||
"to": "node-00000000-0000-4000-8000-000000000152"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"id": "device_resilience",
|
||||
"category": "reliability",
|
||||
"name": {
|
||||
"pl": "Sterowanie tylko przy sprawnym urządzeniu",
|
||||
"en": "Only when device is healthy"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Uruchamia termostat tylko gdy urządzenie jest włączone, online i tryb domu pozwala na pracę.",
|
||||
"en": "Runs the thermostat only when the device is enabled, online and house mode allows operation."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000157",
|
||||
"kind": "device_state",
|
||||
"x": 40,
|
||||
"y": 55,
|
||||
"config": {
|
||||
"device_id": "$device1",
|
||||
"field": "online",
|
||||
"operator": "eq",
|
||||
"value": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000158",
|
||||
"kind": "device_state",
|
||||
"x": 40,
|
||||
"y": 175,
|
||||
"config": {
|
||||
"device_id": "$device1",
|
||||
"field": "enabled",
|
||||
"operator": "eq",
|
||||
"value": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000159",
|
||||
"kind": "house_mode",
|
||||
"x": 40,
|
||||
"y": 295,
|
||||
"config": {
|
||||
"operator": "neq",
|
||||
"value": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000160",
|
||||
"kind": "logic_and",
|
||||
"x": 315,
|
||||
"y": 175,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000161",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 570,
|
||||
"y": 175,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "auto",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 90
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000162",
|
||||
"from": "node-00000000-0000-4000-8000-000000000157",
|
||||
"to": "node-00000000-0000-4000-8000-000000000160"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000163",
|
||||
"from": "node-00000000-0000-4000-8000-000000000158",
|
||||
"to": "node-00000000-0000-4000-8000-000000000160"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000164",
|
||||
"from": "node-00000000-0000-4000-8000-000000000159",
|
||||
"to": "node-00000000-0000-4000-8000-000000000160"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000165",
|
||||
"from": "node-00000000-0000-4000-8000-000000000160",
|
||||
"to": "node-00000000-0000-4000-8000-000000000161"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"id": "dual_threshold_control",
|
||||
"category": "advanced",
|
||||
"name": {
|
||||
"pl": "Dwa progi: grzanie i chłodzenie",
|
||||
"en": "Dual threshold heating/cooling"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Dwie niezależne gałęzie sterują grzaniem poniżej dolnego progu i chłodzeniem powyżej górnego.",
|
||||
"en": "Two independent branches heat below the lower threshold and cool above the upper threshold."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000312",
|
||||
"kind": "zone_temperature",
|
||||
"x": 35,
|
||||
"y": 70,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "lt",
|
||||
"value": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000313",
|
||||
"kind": "zone_temperature",
|
||||
"x": 35,
|
||||
"y": 245,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "gt",
|
||||
"value": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000314",
|
||||
"kind": "house_mode",
|
||||
"x": 250,
|
||||
"y": 70,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "heat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000315",
|
||||
"kind": "house_mode",
|
||||
"x": 250,
|
||||
"y": 245,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "cool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000316",
|
||||
"kind": "logic_and",
|
||||
"x": 470,
|
||||
"y": 70,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000317",
|
||||
"kind": "logic_and",
|
||||
"x": 470,
|
||||
"y": 245,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000318",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 715,
|
||||
"y": 70,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "comfort",
|
||||
"setpoint": 21,
|
||||
"mode": "heat",
|
||||
"cooldown_seconds": 90
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000319",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 715,
|
||||
"y": 245,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "comfort",
|
||||
"setpoint": 21,
|
||||
"mode": "cool",
|
||||
"cooldown_seconds": 90
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000320",
|
||||
"from": "node-00000000-0000-4000-8000-000000000312",
|
||||
"to": "node-00000000-0000-4000-8000-000000000316"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000321",
|
||||
"from": "node-00000000-0000-4000-8000-000000000314",
|
||||
"to": "node-00000000-0000-4000-8000-000000000316"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000322",
|
||||
"from": "node-00000000-0000-4000-8000-000000000313",
|
||||
"to": "node-00000000-0000-4000-8000-000000000317"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000323",
|
||||
"from": "node-00000000-0000-4000-8000-000000000315",
|
||||
"to": "node-00000000-0000-4000-8000-000000000317"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000324",
|
||||
"from": "node-00000000-0000-4000-8000-000000000316",
|
||||
"to": "node-00000000-0000-4000-8000-000000000318"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000325",
|
||||
"from": "node-00000000-0000-4000-8000-000000000317",
|
||||
"to": "node-00000000-0000-4000-8000-000000000319"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"id": "energy_price_eco",
|
||||
"category": "energy",
|
||||
"name": {
|
||||
"pl": "Droga energia + brak domowników",
|
||||
"en": "Expensive energy + nobody home"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Przy wysokiej cenie energii i nieobecności przełącza wybraną strefę w oszczędny profil away.",
|
||||
"en": "When energy is expensive and nobody is home, switches the selected zone to an economical away profile."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000054",
|
||||
"kind": "ha_numeric",
|
||||
"x": 40,
|
||||
"y": 70,
|
||||
"config": {
|
||||
"entity_id": "sensor.energy_price",
|
||||
"operator": "gt",
|
||||
"value": 0.8
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000055",
|
||||
"kind": "ha_state",
|
||||
"x": 40,
|
||||
"y": 190,
|
||||
"config": {
|
||||
"entity_id": "person.someone",
|
||||
"operator": "neq",
|
||||
"value": "home"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000056",
|
||||
"kind": "logic_and",
|
||||
"x": 315,
|
||||
"y": 130,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000057",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 570,
|
||||
"y": 130,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "away",
|
||||
"setpoint": 18,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 300
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000058",
|
||||
"from": "node-00000000-0000-4000-8000-000000000054",
|
||||
"to": "node-00000000-0000-4000-8000-000000000056"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000059",
|
||||
"from": "node-00000000-0000-4000-8000-000000000055",
|
||||
"to": "node-00000000-0000-4000-8000-000000000056"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000060",
|
||||
"from": "node-00000000-0000-4000-8000-000000000056",
|
||||
"to": "node-00000000-0000-4000-8000-000000000057"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"id": "frost_guard",
|
||||
"category": "safety",
|
||||
"name": {
|
||||
"pl": "Ochrona przeciwzamrożeniowa",
|
||||
"en": "Frost protection"
|
||||
},
|
||||
"description": {
|
||||
"pl": "W trybie grzania, przy niskiej temperaturze zewnętrznej i pokojowej, ustawia bezpieczny niski cel termostatu.",
|
||||
"en": "In heating mode, low outdoor and room temperatures select a safe low thermostat target."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000084",
|
||||
"kind": "house_mode",
|
||||
"x": 35,
|
||||
"y": 50,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "heat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000085",
|
||||
"kind": "outdoor_temperature",
|
||||
"x": 35,
|
||||
"y": 170,
|
||||
"config": {
|
||||
"operator": "lt",
|
||||
"value": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000086",
|
||||
"kind": "zone_temperature",
|
||||
"x": 35,
|
||||
"y": 290,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "lt",
|
||||
"value": 9
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000087",
|
||||
"kind": "logic_and",
|
||||
"x": 315,
|
||||
"y": 170,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000088",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 575,
|
||||
"y": 170,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "custom",
|
||||
"setpoint": 12,
|
||||
"mode": "heat",
|
||||
"cooldown_seconds": 300
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000089",
|
||||
"from": "node-00000000-0000-4000-8000-000000000084",
|
||||
"to": "node-00000000-0000-4000-8000-000000000087"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000090",
|
||||
"from": "node-00000000-0000-4000-8000-000000000085",
|
||||
"to": "node-00000000-0000-4000-8000-000000000087"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000091",
|
||||
"from": "node-00000000-0000-4000-8000-000000000086",
|
||||
"to": "node-00000000-0000-4000-8000-000000000087"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000092",
|
||||
"from": "node-00000000-0000-4000-8000-000000000087",
|
||||
"to": "node-00000000-0000-4000-8000-000000000088"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"id": "ha_attribute_mode_guard",
|
||||
"category": "advanced",
|
||||
"name": {
|
||||
"pl": "Atrybut HA + okno + dostępność",
|
||||
"en": "HA attribute + window + availability"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Czyta atrybut hvac_action, dostępność encji i stan okna, aby zatrzymać HVAC tylko przy wiarygodnych danych.",
|
||||
"en": "Reads hvac_action, entity availability and window state to stop HVAC only when the data is trustworthy."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000337",
|
||||
"kind": "ha_attribute",
|
||||
"x": 35,
|
||||
"y": 60,
|
||||
"config": {
|
||||
"entity_id": "climate.living_room",
|
||||
"attribute": "hvac_action",
|
||||
"operator": "neq",
|
||||
"value": "idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000338",
|
||||
"kind": "ha_state",
|
||||
"x": 35,
|
||||
"y": 180,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.window",
|
||||
"operator": "eq",
|
||||
"value": "on"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000339",
|
||||
"kind": "ha_available",
|
||||
"x": 35,
|
||||
"y": 300,
|
||||
"config": {
|
||||
"entity_id": "climate.living_room"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000340",
|
||||
"kind": "logic_and",
|
||||
"x": 315,
|
||||
"y": 180,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000341",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 575,
|
||||
"y": 180,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "auto",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 60,
|
||||
"power": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000342",
|
||||
"from": "node-00000000-0000-4000-8000-000000000337",
|
||||
"to": "node-00000000-0000-4000-8000-000000000340"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000343",
|
||||
"from": "node-00000000-0000-4000-8000-000000000338",
|
||||
"to": "node-00000000-0000-4000-8000-000000000340"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000344",
|
||||
"from": "node-00000000-0000-4000-8000-000000000339",
|
||||
"to": "node-00000000-0000-4000-8000-000000000340"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000345",
|
||||
"from": "node-00000000-0000-4000-8000-000000000340",
|
||||
"to": "node-00000000-0000-4000-8000-000000000341"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"id": "ha_boiler_supply_boost",
|
||||
"category": "home_assistant",
|
||||
"name": {
|
||||
"pl": "Gorące zasilanie kotła + zimny pokój → boost",
|
||||
"en": "Hot boiler supply + cold room → boost"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Czyta sensor.boiler_supply_temperature. Przy temperaturze zasilania powyżej 45°C i zimnym pokoju ustawia GREE na 23,5°C.",
|
||||
"en": "Reads sensor.boiler_supply_temperature. Above 45°C with a cold room, GREE is set to 23.5°C."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000256",
|
||||
"kind": "ha_available",
|
||||
"x": 25,
|
||||
"y": 45,
|
||||
"config": {
|
||||
"entity_id": "sensor.boiler_supply_temperature"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000257",
|
||||
"kind": "ha_numeric",
|
||||
"x": 25,
|
||||
"y": 160,
|
||||
"config": {
|
||||
"entity_id": "sensor.boiler_supply_temperature",
|
||||
"operator": "gt",
|
||||
"value": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000258",
|
||||
"kind": "zone_temperature",
|
||||
"x": 25,
|
||||
"y": 275,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "lt",
|
||||
"value": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000259",
|
||||
"kind": "house_mode",
|
||||
"x": 25,
|
||||
"y": 390,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "heat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000260",
|
||||
"kind": "logic_and",
|
||||
"x": 320,
|
||||
"y": 215,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000261",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 590,
|
||||
"y": 215,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "custom",
|
||||
"setpoint": 23.5,
|
||||
"mode": "heat",
|
||||
"cooldown_seconds": 180,
|
||||
"power": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000262",
|
||||
"from": "node-00000000-0000-4000-8000-000000000256",
|
||||
"to": "node-00000000-0000-4000-8000-000000000260"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000263",
|
||||
"from": "node-00000000-0000-4000-8000-000000000257",
|
||||
"to": "node-00000000-0000-4000-8000-000000000260"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000264",
|
||||
"from": "node-00000000-0000-4000-8000-000000000258",
|
||||
"to": "node-00000000-0000-4000-8000-000000000260"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000265",
|
||||
"from": "node-00000000-0000-4000-8000-000000000259",
|
||||
"to": "node-00000000-0000-4000-8000-000000000260"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000266",
|
||||
"from": "node-00000000-0000-4000-8000-000000000260",
|
||||
"to": "node-00000000-0000-4000-8000-000000000261"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"id": "ha_external_heat_source_assist",
|
||||
"category": "home_assistant",
|
||||
"name": {
|
||||
"pl": "Inne źródło ciepła ON → wspomagaj GREE",
|
||||
"en": "Other heat source ON → assist with GREE"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Gdy zewnętrzne źródło działa, ale pokój nadal ma mniej niż 20°C, GREE wspomaga je w trybie heat z celem 23°C.",
|
||||
"en": "When an external heat source is running but the room is still below 20°C, GREE assists in heat mode with a 23°C target."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000236",
|
||||
"kind": "ha_available",
|
||||
"x": 25,
|
||||
"y": 40,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.external_heat_source"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000237",
|
||||
"kind": "ha_state",
|
||||
"x": 25,
|
||||
"y": 155,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.external_heat_source",
|
||||
"operator": "eq",
|
||||
"value": "on"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000238",
|
||||
"kind": "zone_temperature",
|
||||
"x": 25,
|
||||
"y": 270,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "lt",
|
||||
"value": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000239",
|
||||
"kind": "house_mode",
|
||||
"x": 25,
|
||||
"y": 385,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "heat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000240",
|
||||
"kind": "logic_and",
|
||||
"x": 320,
|
||||
"y": 210,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000241",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 590,
|
||||
"y": 210,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "custom",
|
||||
"setpoint": 23,
|
||||
"mode": "heat",
|
||||
"cooldown_seconds": 180,
|
||||
"power": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000242",
|
||||
"from": "node-00000000-0000-4000-8000-000000000236",
|
||||
"to": "node-00000000-0000-4000-8000-000000000240"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000243",
|
||||
"from": "node-00000000-0000-4000-8000-000000000237",
|
||||
"to": "node-00000000-0000-4000-8000-000000000240"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000244",
|
||||
"from": "node-00000000-0000-4000-8000-000000000238",
|
||||
"to": "node-00000000-0000-4000-8000-000000000240"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000245",
|
||||
"from": "node-00000000-0000-4000-8000-000000000239",
|
||||
"to": "node-00000000-0000-4000-8000-000000000240"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000246",
|
||||
"from": "node-00000000-0000-4000-8000-000000000240",
|
||||
"to": "node-00000000-0000-4000-8000-000000000241"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"id": "ha_external_heat_source_off",
|
||||
"category": "home_assistant",
|
||||
"name": {
|
||||
"pl": "Inne źródło ciepła ON → wyłącz GREE",
|
||||
"en": "Other heat source ON → turn GREE off"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Ogólny preset dla kominka, pieca lub innego źródła. Stan binary_sensor.external_heat_source = on wyłącza grupę albo strefę GREE.",
|
||||
"en": "Generic fireplace, furnace or other source preset. binary_sensor.external_heat_source = on turns the GREE group or zone off."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000229",
|
||||
"kind": "ha_available",
|
||||
"x": 35,
|
||||
"y": 70,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.external_heat_source"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000230",
|
||||
"kind": "ha_state",
|
||||
"x": 35,
|
||||
"y": 200,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.external_heat_source",
|
||||
"operator": "eq",
|
||||
"value": "on"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000231",
|
||||
"kind": "logic_and",
|
||||
"x": 315,
|
||||
"y": 135,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000232",
|
||||
"kind": "group_action",
|
||||
"x": 575,
|
||||
"y": 135,
|
||||
"config": {
|
||||
"group_id": "$group1",
|
||||
"power": false,
|
||||
"mode": "auto",
|
||||
"preset": "auto",
|
||||
"setpoint": 21,
|
||||
"cooldown_seconds": 120
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000233",
|
||||
"from": "node-00000000-0000-4000-8000-000000000229",
|
||||
"to": "node-00000000-0000-4000-8000-000000000231"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000234",
|
||||
"from": "node-00000000-0000-4000-8000-000000000230",
|
||||
"to": "node-00000000-0000-4000-8000-000000000231"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000235",
|
||||
"from": "node-00000000-0000-4000-8000-000000000231",
|
||||
"to": "node-00000000-0000-4000-8000-000000000232"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"id": "ha_gas_backup_heat",
|
||||
"category": "home_assistant",
|
||||
"name": {
|
||||
"pl": "Gaz nie grzeje + zimno → GREE jako backup",
|
||||
"en": "Gas not heating + room cold → GREE backup"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Gdy encja kotła jest dostępna, ale nie zgłasza heating, a w pokoju jest poniżej 19°C, GREE przejmuje awaryjne grzanie do 22°C.",
|
||||
"en": "When the boiler entity is available but does not report heating and the room drops below 19°C, GREE provides backup heat to 22°C."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000218",
|
||||
"kind": "ha_available",
|
||||
"x": 25,
|
||||
"y": 40,
|
||||
"config": {
|
||||
"entity_id": "climate.gas_boiler"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000219",
|
||||
"kind": "ha_attribute",
|
||||
"x": 25,
|
||||
"y": 155,
|
||||
"config": {
|
||||
"entity_id": "climate.gas_boiler",
|
||||
"attribute": "hvac_action",
|
||||
"operator": "neq",
|
||||
"value": "heating"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000220",
|
||||
"kind": "zone_temperature",
|
||||
"x": 25,
|
||||
"y": 270,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "lt",
|
||||
"value": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000221",
|
||||
"kind": "house_mode",
|
||||
"x": 25,
|
||||
"y": 385,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "heat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000222",
|
||||
"kind": "logic_and",
|
||||
"x": 320,
|
||||
"y": 210,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000223",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 590,
|
||||
"y": 210,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "custom",
|
||||
"setpoint": 22,
|
||||
"mode": "heat",
|
||||
"cooldown_seconds": 180,
|
||||
"power": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000224",
|
||||
"from": "node-00000000-0000-4000-8000-000000000218",
|
||||
"to": "node-00000000-0000-4000-8000-000000000222"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000225",
|
||||
"from": "node-00000000-0000-4000-8000-000000000219",
|
||||
"to": "node-00000000-0000-4000-8000-000000000222"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000226",
|
||||
"from": "node-00000000-0000-4000-8000-000000000220",
|
||||
"to": "node-00000000-0000-4000-8000-000000000222"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000227",
|
||||
"from": "node-00000000-0000-4000-8000-000000000221",
|
||||
"to": "node-00000000-0000-4000-8000-000000000222"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000228",
|
||||
"from": "node-00000000-0000-4000-8000-000000000222",
|
||||
"to": "node-00000000-0000-4000-8000-000000000223"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"id": "ha_gas_heating_boost",
|
||||
"category": "home_assistant",
|
||||
"name": {
|
||||
"pl": "Kocioł gazowy grzeje → dogrzewanie GREE",
|
||||
"en": "Gas boiler heating → GREE heat boost"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Gdy kocioł gazowy aktywnie grzeje, pokój ma mniej niż 20°C i dom jest w trybie heat, GREE wspomaga ogrzewanie celem 23,5°C.",
|
||||
"en": "When the gas boiler is actively heating, the room is below 20°C and house mode is heat, GREE assists with a 23.5°C target."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000198",
|
||||
"kind": "ha_available",
|
||||
"x": 25,
|
||||
"y": 35,
|
||||
"config": {
|
||||
"entity_id": "climate.gas_boiler"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000199",
|
||||
"kind": "ha_attribute",
|
||||
"x": 25,
|
||||
"y": 145,
|
||||
"config": {
|
||||
"entity_id": "climate.gas_boiler",
|
||||
"attribute": "hvac_action",
|
||||
"operator": "eq",
|
||||
"value": "heating"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000200",
|
||||
"kind": "zone_temperature",
|
||||
"x": 25,
|
||||
"y": 255,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "lt",
|
||||
"value": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000201",
|
||||
"kind": "house_mode",
|
||||
"x": 25,
|
||||
"y": 365,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "heat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000202",
|
||||
"kind": "logic_and",
|
||||
"x": 320,
|
||||
"y": 200,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000203",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 590,
|
||||
"y": 200,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "custom",
|
||||
"setpoint": 23.5,
|
||||
"mode": "heat",
|
||||
"cooldown_seconds": 180,
|
||||
"power": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000204",
|
||||
"from": "node-00000000-0000-4000-8000-000000000198",
|
||||
"to": "node-00000000-0000-4000-8000-000000000202"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000205",
|
||||
"from": "node-00000000-0000-4000-8000-000000000199",
|
||||
"to": "node-00000000-0000-4000-8000-000000000202"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000206",
|
||||
"from": "node-00000000-0000-4000-8000-000000000200",
|
||||
"to": "node-00000000-0000-4000-8000-000000000202"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000207",
|
||||
"from": "node-00000000-0000-4000-8000-000000000201",
|
||||
"to": "node-00000000-0000-4000-8000-000000000202"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000208",
|
||||
"from": "node-00000000-0000-4000-8000-000000000202",
|
||||
"to": "node-00000000-0000-4000-8000-000000000203"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"id": "ha_gas_heating_off",
|
||||
"category": "home_assistant",
|
||||
"name": {
|
||||
"pl": "Kocioł gazowy grzeje → wyłącz GREE",
|
||||
"en": "Gas boiler heating → turn GREE off"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Czyta hvac_action z climate.gas_boiler. Gdy HA zgłasza heating i encja jest dostępna, wyłącza wybraną strefę GREE.",
|
||||
"en": "Reads hvac_action from climate.gas_boiler. When HA reports heating and the entity is available, the selected GREE zone is turned off."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000191",
|
||||
"kind": "ha_available",
|
||||
"x": 35,
|
||||
"y": 70,
|
||||
"config": {
|
||||
"entity_id": "climate.gas_boiler"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000192",
|
||||
"kind": "ha_attribute",
|
||||
"x": 35,
|
||||
"y": 200,
|
||||
"config": {
|
||||
"entity_id": "climate.gas_boiler",
|
||||
"attribute": "hvac_action",
|
||||
"operator": "eq",
|
||||
"value": "heating"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000193",
|
||||
"kind": "logic_and",
|
||||
"x": 315,
|
||||
"y": 135,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000194",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 575,
|
||||
"y": 135,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "auto",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 120,
|
||||
"power": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000195",
|
||||
"from": "node-00000000-0000-4000-8000-000000000191",
|
||||
"to": "node-00000000-0000-4000-8000-000000000193"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000196",
|
||||
"from": "node-00000000-0000-4000-8000-000000000192",
|
||||
"to": "node-00000000-0000-4000-8000-000000000193"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000197",
|
||||
"from": "node-00000000-0000-4000-8000-000000000193",
|
||||
"to": "node-00000000-0000-4000-8000-000000000194"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"id": "ha_gas_heating_reduce",
|
||||
"category": "home_assistant",
|
||||
"name": {
|
||||
"pl": "Kocioł grzeje + pokój ciepły → obniż GREE",
|
||||
"en": "Boiler heating + room warm → reduce GREE"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Jeśli gaz już grzeje, a strefa osiągnęła co najmniej 21°C, obniża cel GREE do 18°C, aby źródła ciepła nie walczyły ze sobą.",
|
||||
"en": "When gas heat is already active and the zone reaches at least 21°C, lowers the GREE target to 18°C so the heat sources do not fight each other."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000209",
|
||||
"kind": "ha_available",
|
||||
"x": 35,
|
||||
"y": 50,
|
||||
"config": {
|
||||
"entity_id": "climate.gas_boiler"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000210",
|
||||
"kind": "ha_attribute",
|
||||
"x": 35,
|
||||
"y": 165,
|
||||
"config": {
|
||||
"entity_id": "climate.gas_boiler",
|
||||
"attribute": "hvac_action",
|
||||
"operator": "eq",
|
||||
"value": "heating"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000211",
|
||||
"kind": "zone_temperature",
|
||||
"x": 35,
|
||||
"y": 280,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "gte",
|
||||
"value": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000212",
|
||||
"kind": "logic_and",
|
||||
"x": 315,
|
||||
"y": 165,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000213",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 575,
|
||||
"y": 165,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "custom",
|
||||
"setpoint": 18,
|
||||
"mode": "heat",
|
||||
"cooldown_seconds": 180,
|
||||
"power": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000214",
|
||||
"from": "node-00000000-0000-4000-8000-000000000209",
|
||||
"to": "node-00000000-0000-4000-8000-000000000212"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000215",
|
||||
"from": "node-00000000-0000-4000-8000-000000000210",
|
||||
"to": "node-00000000-0000-4000-8000-000000000212"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000216",
|
||||
"from": "node-00000000-0000-4000-8000-000000000211",
|
||||
"to": "node-00000000-0000-4000-8000-000000000212"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000217",
|
||||
"from": "node-00000000-0000-4000-8000-000000000212",
|
||||
"to": "node-00000000-0000-4000-8000-000000000213"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"id": "ha_heating_demand_follow",
|
||||
"category": "home_assistant",
|
||||
"name": {
|
||||
"pl": "Podążaj za żądaniem grzania z HA",
|
||||
"en": "Follow Home Assistant heating demand"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Stan binary_sensor.heating_demand = on uruchamia ogrzewanie GREE do 22,5°C, jeśli dom jest w trybie heat.",
|
||||
"en": "binary_sensor.heating_demand = on starts GREE heating to 22.5°C while the house is in heat mode."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000247",
|
||||
"kind": "ha_available",
|
||||
"x": 35,
|
||||
"y": 60,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.heating_demand"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000248",
|
||||
"kind": "ha_state",
|
||||
"x": 35,
|
||||
"y": 180,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.heating_demand",
|
||||
"operator": "eq",
|
||||
"value": "on"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000249",
|
||||
"kind": "house_mode",
|
||||
"x": 35,
|
||||
"y": 300,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "heat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000250",
|
||||
"kind": "logic_and",
|
||||
"x": 315,
|
||||
"y": 180,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000251",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 575,
|
||||
"y": 180,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "custom",
|
||||
"setpoint": 22.5,
|
||||
"mode": "heat",
|
||||
"cooldown_seconds": 120,
|
||||
"power": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000252",
|
||||
"from": "node-00000000-0000-4000-8000-000000000247",
|
||||
"to": "node-00000000-0000-4000-8000-000000000250"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000253",
|
||||
"from": "node-00000000-0000-4000-8000-000000000248",
|
||||
"to": "node-00000000-0000-4000-8000-000000000250"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000254",
|
||||
"from": "node-00000000-0000-4000-8000-000000000249",
|
||||
"to": "node-00000000-0000-4000-8000-000000000250"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000255",
|
||||
"from": "node-00000000-0000-4000-8000-000000000250",
|
||||
"to": "node-00000000-0000-4000-8000-000000000251"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"id": "ha_thermostat_idle_fallback",
|
||||
"category": "home_assistant",
|
||||
"name": {
|
||||
"pl": "Termostat gazowy idle → awaryjne grzanie GREE",
|
||||
"en": "Gas thermostat idle → GREE fallback heat"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Jeśli climate.gas_boiler jest dostępny, ma hvac_action=idle i pokój spadnie poniżej 19°C, GREE uruchamia grzanie do 21,5°C.",
|
||||
"en": "If climate.gas_boiler is available, reports hvac_action=idle and the room falls below 19°C, GREE heats to 21.5°C."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000267",
|
||||
"kind": "ha_available",
|
||||
"x": 25,
|
||||
"y": 40,
|
||||
"config": {
|
||||
"entity_id": "climate.gas_boiler"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000268",
|
||||
"kind": "ha_attribute",
|
||||
"x": 25,
|
||||
"y": 155,
|
||||
"config": {
|
||||
"entity_id": "climate.gas_boiler",
|
||||
"attribute": "hvac_action",
|
||||
"operator": "eq",
|
||||
"value": "idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000269",
|
||||
"kind": "zone_temperature",
|
||||
"x": 25,
|
||||
"y": 270,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "lt",
|
||||
"value": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000270",
|
||||
"kind": "house_mode",
|
||||
"x": 25,
|
||||
"y": 385,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "heat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000271",
|
||||
"kind": "logic_and",
|
||||
"x": 320,
|
||||
"y": 210,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000272",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 590,
|
||||
"y": 210,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "custom",
|
||||
"setpoint": 21.5,
|
||||
"mode": "heat",
|
||||
"cooldown_seconds": 240,
|
||||
"power": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000273",
|
||||
"from": "node-00000000-0000-4000-8000-000000000267",
|
||||
"to": "node-00000000-0000-4000-8000-000000000271"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000274",
|
||||
"from": "node-00000000-0000-4000-8000-000000000268",
|
||||
"to": "node-00000000-0000-4000-8000-000000000271"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000275",
|
||||
"from": "node-00000000-0000-4000-8000-000000000269",
|
||||
"to": "node-00000000-0000-4000-8000-000000000271"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000276",
|
||||
"from": "node-00000000-0000-4000-8000-000000000270",
|
||||
"to": "node-00000000-0000-4000-8000-000000000271"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000277",
|
||||
"from": "node-00000000-0000-4000-8000-000000000271",
|
||||
"to": "node-00000000-0000-4000-8000-000000000272"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"id": "ha_window_guard",
|
||||
"category": "safety",
|
||||
"name": {
|
||||
"pl": "Otwarte okno — wyłącz HVAC",
|
||||
"en": "Open window — stop HVAC"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Przykład integracji z binary_sensor Home Assistant: otwarte okno wyłącza termostat. Ustaw właściwy entity_id.",
|
||||
"en": "Home Assistant binary_sensor example: an open window turns the thermostat off. Set the correct entity_id."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000093",
|
||||
"kind": "ha_state",
|
||||
"x": 45,
|
||||
"y": 90,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.window",
|
||||
"operator": "eq",
|
||||
"value": "on"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000094",
|
||||
"kind": "house_mode",
|
||||
"x": 45,
|
||||
"y": 220,
|
||||
"config": {
|
||||
"operator": "neq",
|
||||
"value": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000095",
|
||||
"kind": "logic_and",
|
||||
"x": 305,
|
||||
"y": 155,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000096",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 560,
|
||||
"y": 155,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "auto",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 60,
|
||||
"power": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000097",
|
||||
"from": "node-00000000-0000-4000-8000-000000000093",
|
||||
"to": "node-00000000-0000-4000-8000-000000000095"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000098",
|
||||
"from": "node-00000000-0000-4000-8000-000000000094",
|
||||
"to": "node-00000000-0000-4000-8000-000000000095"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000099",
|
||||
"from": "node-00000000-0000-4000-8000-000000000095",
|
||||
"to": "node-00000000-0000-4000-8000-000000000096"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"id": "humidity_guard",
|
||||
"category": "safety",
|
||||
"name": {
|
||||
"pl": "Wysoka wilgotność — chłodzenie",
|
||||
"en": "High humidity cooling"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Przy wysokiej wilgotności, obecności domowników i trybie chłodzenia wymusza umiarkowane chłodzenie.",
|
||||
"en": "With high humidity, occupancy and cooling house mode, applies moderate cooling."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000100",
|
||||
"kind": "ha_numeric",
|
||||
"x": 40,
|
||||
"y": 60,
|
||||
"config": {
|
||||
"entity_id": "sensor.living_room_humidity",
|
||||
"operator": "gt",
|
||||
"value": 70
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000101",
|
||||
"kind": "house_mode",
|
||||
"x": 40,
|
||||
"y": 180,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "cool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000102",
|
||||
"kind": "ha_state",
|
||||
"x": 40,
|
||||
"y": 300,
|
||||
"config": {
|
||||
"entity_id": "person.someone",
|
||||
"operator": "eq",
|
||||
"value": "home"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000103",
|
||||
"kind": "logic_and",
|
||||
"x": 315,
|
||||
"y": 180,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000104",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 575,
|
||||
"y": 180,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "custom",
|
||||
"setpoint": 22,
|
||||
"mode": "cool",
|
||||
"cooldown_seconds": 180
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000105",
|
||||
"from": "node-00000000-0000-4000-8000-000000000100",
|
||||
"to": "node-00000000-0000-4000-8000-000000000103"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000106",
|
||||
"from": "node-00000000-0000-4000-8000-000000000101",
|
||||
"to": "node-00000000-0000-4000-8000-000000000103"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000107",
|
||||
"from": "node-00000000-0000-4000-8000-000000000102",
|
||||
"to": "node-00000000-0000-4000-8000-000000000103"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000108",
|
||||
"from": "node-00000000-0000-4000-8000-000000000103",
|
||||
"to": "node-00000000-0000-4000-8000-000000000104"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"id": "mild_weather_eco",
|
||||
"category": "energy",
|
||||
"name": {
|
||||
"pl": "Łagodna pogoda — HVAC off",
|
||||
"en": "Mild weather — HVAC off"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Gdy temperatura zewnętrzna mieści się w komfortowym zakresie i okno jest zamknięte, wyłącza HVAC.",
|
||||
"en": "When outdoor temperature is within a comfortable band and the window is closed, turns HVAC off."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000068",
|
||||
"kind": "outdoor_temperature",
|
||||
"x": 35,
|
||||
"y": 55,
|
||||
"config": {
|
||||
"operator": "gte",
|
||||
"value": 17
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000069",
|
||||
"kind": "outdoor_temperature",
|
||||
"x": 35,
|
||||
"y": 175,
|
||||
"config": {
|
||||
"operator": "lte",
|
||||
"value": 24
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000070",
|
||||
"kind": "ha_state",
|
||||
"x": 35,
|
||||
"y": 295,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.window",
|
||||
"operator": "eq",
|
||||
"value": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000071",
|
||||
"kind": "logic_and",
|
||||
"x": 310,
|
||||
"y": 175,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000072",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 565,
|
||||
"y": 175,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "auto",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 300,
|
||||
"power": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000073",
|
||||
"from": "node-00000000-0000-4000-8000-000000000068",
|
||||
"to": "node-00000000-0000-4000-8000-000000000071"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000074",
|
||||
"from": "node-00000000-0000-4000-8000-000000000069",
|
||||
"to": "node-00000000-0000-4000-8000-000000000071"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000075",
|
||||
"from": "node-00000000-0000-4000-8000-000000000070",
|
||||
"to": "node-00000000-0000-4000-8000-000000000071"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000076",
|
||||
"from": "node-00000000-0000-4000-8000-000000000071",
|
||||
"to": "node-00000000-0000-4000-8000-000000000072"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"id": "morning_boost",
|
||||
"category": "comfort",
|
||||
"name": {
|
||||
"pl": "Poranne dogrzanie przy mrozie",
|
||||
"en": "Cold-weather morning boost"
|
||||
},
|
||||
"description": {
|
||||
"pl": "W dni robocze rano, tylko w trybie grzania i przy mrozie na zewnątrz, podnosi cel do 22,5°C.",
|
||||
"en": "On workday mornings, only in heating mode and during cold weather, raises the target to 22.5°C."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000028",
|
||||
"kind": "weekday",
|
||||
"x": 35,
|
||||
"y": 50,
|
||||
"config": {
|
||||
"days": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000029",
|
||||
"kind": "time_range",
|
||||
"x": 35,
|
||||
"y": 170,
|
||||
"config": {
|
||||
"start": "05:30",
|
||||
"end": "07:30"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000030",
|
||||
"kind": "outdoor_temperature",
|
||||
"x": 35,
|
||||
"y": 290,
|
||||
"config": {
|
||||
"operator": "lt",
|
||||
"value": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000031",
|
||||
"kind": "house_mode",
|
||||
"x": 35,
|
||||
"y": 410,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "heat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000032",
|
||||
"kind": "logic_and",
|
||||
"x": 320,
|
||||
"y": 220,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000033",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 585,
|
||||
"y": 220,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "custom",
|
||||
"setpoint": 22.5,
|
||||
"mode": "heat",
|
||||
"cooldown_seconds": 180
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000034",
|
||||
"from": "node-00000000-0000-4000-8000-000000000028",
|
||||
"to": "node-00000000-0000-4000-8000-000000000032"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000035",
|
||||
"from": "node-00000000-0000-4000-8000-000000000029",
|
||||
"to": "node-00000000-0000-4000-8000-000000000032"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000036",
|
||||
"from": "node-00000000-0000-4000-8000-000000000030",
|
||||
"to": "node-00000000-0000-4000-8000-000000000032"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000037",
|
||||
"from": "node-00000000-0000-4000-8000-000000000031",
|
||||
"to": "node-00000000-0000-4000-8000-000000000032"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000038",
|
||||
"from": "node-00000000-0000-4000-8000-000000000032",
|
||||
"to": "node-00000000-0000-4000-8000-000000000033"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"id": "multi_room_group_guard",
|
||||
"category": "advanced",
|
||||
"name": {
|
||||
"pl": "Wiele pokoi → jedna grupa",
|
||||
"en": "Multiple rooms → one group"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Jeśli dowolna z dwóch stref jest za ciepła i dom jest w trybie cool, uruchamia wspólną grupę lub strefę.",
|
||||
"en": "If either of two zones is too warm and house mode is cooling, starts a shared group or zone."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000326",
|
||||
"kind": "zone_temperature",
|
||||
"x": 30,
|
||||
"y": 55,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "gt",
|
||||
"value": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000327",
|
||||
"kind": "zone_temperature",
|
||||
"x": 30,
|
||||
"y": 175,
|
||||
"config": {
|
||||
"zone_id": "$zone2",
|
||||
"operator": "gt",
|
||||
"value": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000328",
|
||||
"kind": "logic_or",
|
||||
"x": 280,
|
||||
"y": 115,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000329",
|
||||
"kind": "house_mode",
|
||||
"x": 280,
|
||||
"y": 245,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "cool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000330",
|
||||
"kind": "logic_and",
|
||||
"x": 505,
|
||||
"y": 175,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000331",
|
||||
"kind": "group_action",
|
||||
"x": 760,
|
||||
"y": 175,
|
||||
"config": {
|
||||
"group_id": "$group1",
|
||||
"power": true,
|
||||
"mode": "auto",
|
||||
"preset": "comfort",
|
||||
"setpoint": 22,
|
||||
"cooldown_seconds": 120
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000332",
|
||||
"from": "node-00000000-0000-4000-8000-000000000326",
|
||||
"to": "node-00000000-0000-4000-8000-000000000328"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000333",
|
||||
"from": "node-00000000-0000-4000-8000-000000000327",
|
||||
"to": "node-00000000-0000-4000-8000-000000000328"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000334",
|
||||
"from": "node-00000000-0000-4000-8000-000000000328",
|
||||
"to": "node-00000000-0000-4000-8000-000000000330"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000335",
|
||||
"from": "node-00000000-0000-4000-8000-000000000329",
|
||||
"to": "node-00000000-0000-4000-8000-000000000330"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000336",
|
||||
"from": "node-00000000-0000-4000-8000-000000000330",
|
||||
"to": "node-00000000-0000-4000-8000-000000000331"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"id": "nested_guard",
|
||||
"category": "advanced",
|
||||
"name": {
|
||||
"pl": "Złożony warunek z OR i NOT",
|
||||
"en": "Nested OR and NOT guard"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Dni i godziny + temperatura zewnętrzna LUB pokojowa + NOT dla otwartego okna. Przykład wielopoziomowego grafu.",
|
||||
"en": "Weekdays and time + outdoor OR room temperature + NOT open window. An example of a multi-level graph."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000278",
|
||||
"kind": "weekday",
|
||||
"x": 25,
|
||||
"y": 35,
|
||||
"config": {
|
||||
"days": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000279",
|
||||
"kind": "time_range",
|
||||
"x": 25,
|
||||
"y": 145,
|
||||
"config": {
|
||||
"start": "06:00",
|
||||
"end": "22:30"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000280",
|
||||
"kind": "outdoor_temperature",
|
||||
"x": 25,
|
||||
"y": 255,
|
||||
"config": {
|
||||
"operator": "lt",
|
||||
"value": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000281",
|
||||
"kind": "zone_temperature",
|
||||
"x": 25,
|
||||
"y": 365,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "lt",
|
||||
"value": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000282",
|
||||
"kind": "logic_or",
|
||||
"x": 275,
|
||||
"y": 310,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000283",
|
||||
"kind": "ha_available",
|
||||
"x": 275,
|
||||
"y": 430,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.window"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000284",
|
||||
"kind": "ha_state",
|
||||
"x": 275,
|
||||
"y": 540,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.window",
|
||||
"operator": "eq",
|
||||
"value": "on"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000285",
|
||||
"kind": "logic_not",
|
||||
"x": 495,
|
||||
"y": 540,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000286",
|
||||
"kind": "logic_and",
|
||||
"x": 510,
|
||||
"y": 235,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000287",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 770,
|
||||
"y": 235,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "comfort",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 90
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000288",
|
||||
"from": "node-00000000-0000-4000-8000-000000000280",
|
||||
"to": "node-00000000-0000-4000-8000-000000000282"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000289",
|
||||
"from": "node-00000000-0000-4000-8000-000000000281",
|
||||
"to": "node-00000000-0000-4000-8000-000000000282"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000290",
|
||||
"from": "node-00000000-0000-4000-8000-000000000284",
|
||||
"to": "node-00000000-0000-4000-8000-000000000285"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000291",
|
||||
"from": "node-00000000-0000-4000-8000-000000000278",
|
||||
"to": "node-00000000-0000-4000-8000-000000000286"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000292",
|
||||
"from": "node-00000000-0000-4000-8000-000000000279",
|
||||
"to": "node-00000000-0000-4000-8000-000000000286"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000293",
|
||||
"from": "node-00000000-0000-4000-8000-000000000282",
|
||||
"to": "node-00000000-0000-4000-8000-000000000286"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000294",
|
||||
"from": "node-00000000-0000-4000-8000-000000000283",
|
||||
"to": "node-00000000-0000-4000-8000-000000000286"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000295",
|
||||
"from": "node-00000000-0000-4000-8000-000000000285",
|
||||
"to": "node-00000000-0000-4000-8000-000000000286"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000296",
|
||||
"from": "node-00000000-0000-4000-8000-000000000286",
|
||||
"to": "node-00000000-0000-4000-8000-000000000287"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"id": "night_group",
|
||||
"category": "night",
|
||||
"name": {
|
||||
"pl": "Noc dla grupy",
|
||||
"en": "Night group"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Nocny przedział czasu + tryb domu sterują presetem sleep dla grupy lub strefy.",
|
||||
"en": "Night time + house mode applies sleep preset to a group or zone."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000125",
|
||||
"kind": "time_range",
|
||||
"x": 50,
|
||||
"y": 100,
|
||||
"config": {
|
||||
"start": "22:30",
|
||||
"end": "06:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000126",
|
||||
"kind": "house_mode",
|
||||
"x": 50,
|
||||
"y": 230,
|
||||
"config": {
|
||||
"operator": "neq",
|
||||
"value": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000127",
|
||||
"kind": "logic_and",
|
||||
"x": 315,
|
||||
"y": 165,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000128",
|
||||
"kind": "group_action",
|
||||
"x": 570,
|
||||
"y": 165,
|
||||
"config": {
|
||||
"group_id": "$group1",
|
||||
"power": true,
|
||||
"mode": "auto",
|
||||
"preset": "sleep",
|
||||
"setpoint": 20,
|
||||
"cooldown_seconds": 120
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000129",
|
||||
"from": "node-00000000-0000-4000-8000-000000000125",
|
||||
"to": "node-00000000-0000-4000-8000-000000000127"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000130",
|
||||
"from": "node-00000000-0000-4000-8000-000000000126",
|
||||
"to": "node-00000000-0000-4000-8000-000000000127"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000131",
|
||||
"from": "node-00000000-0000-4000-8000-000000000127",
|
||||
"to": "node-00000000-0000-4000-8000-000000000128"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"id": "night_quiet",
|
||||
"category": "night",
|
||||
"name": {
|
||||
"pl": "Nocny profil grupy",
|
||||
"en": "Night group profile"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Korzysta bezpośrednio z istniejącego Trybu nocnego aplikacji i stanu grupy, aby zastosować preset sleep.",
|
||||
"en": "Uses the app Night mode and group state directly to apply the sleep preset."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000132",
|
||||
"kind": "night_mode",
|
||||
"x": 45,
|
||||
"y": 90,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000133",
|
||||
"kind": "logic_and",
|
||||
"x": 310,
|
||||
"y": 155,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000134",
|
||||
"kind": "group_action",
|
||||
"x": 565,
|
||||
"y": 155,
|
||||
"config": {
|
||||
"group_id": "$group1",
|
||||
"power": null,
|
||||
"mode": "auto",
|
||||
"preset": "sleep",
|
||||
"setpoint": 20,
|
||||
"cooldown_seconds": 180
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000136",
|
||||
"kind": "group_state",
|
||||
"x": 45,
|
||||
"y": 225,
|
||||
"config": {
|
||||
"group_id": "$group1",
|
||||
"field": "power_enabled",
|
||||
"operator": "eq",
|
||||
"value": "true"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000135",
|
||||
"from": "node-00000000-0000-4000-8000-000000000132",
|
||||
"to": "node-00000000-0000-4000-8000-000000000133"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000137",
|
||||
"from": "node-00000000-0000-4000-8000-000000000136",
|
||||
"to": "node-00000000-0000-4000-8000-000000000133"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000138",
|
||||
"from": "node-00000000-0000-4000-8000-000000000133",
|
||||
"to": "node-00000000-0000-4000-8000-000000000134"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"id": "occupancy_weather_matrix",
|
||||
"category": "advanced",
|
||||
"name": {
|
||||
"pl": "Obecność + pogoda + okno",
|
||||
"en": "Occupancy + weather + window"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Komfort działa tylko gdy ktoś jest w domu, występuje zapotrzebowanie pogodowe lub pokojowe i okno nie jest otwarte.",
|
||||
"en": "Comfort runs only when someone is home, outdoor or room demand exists, and the window is not open."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000297",
|
||||
"kind": "ha_state",
|
||||
"x": 25,
|
||||
"y": 40,
|
||||
"config": {
|
||||
"entity_id": "person.someone",
|
||||
"operator": "eq",
|
||||
"value": "home"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000298",
|
||||
"kind": "outdoor_temperature",
|
||||
"x": 25,
|
||||
"y": 155,
|
||||
"config": {
|
||||
"operator": "lt",
|
||||
"value": 9
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000299",
|
||||
"kind": "zone_temperature",
|
||||
"x": 25,
|
||||
"y": 270,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "lt",
|
||||
"value": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000300",
|
||||
"kind": "logic_or",
|
||||
"x": 275,
|
||||
"y": 215,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000301",
|
||||
"kind": "ha_state",
|
||||
"x": 275,
|
||||
"y": 345,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.window",
|
||||
"operator": "eq",
|
||||
"value": "on"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000302",
|
||||
"kind": "logic_not",
|
||||
"x": 495,
|
||||
"y": 345,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000303",
|
||||
"kind": "logic_and",
|
||||
"x": 505,
|
||||
"y": 150,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000304",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 765,
|
||||
"y": 150,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "comfort",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 90
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000305",
|
||||
"from": "node-00000000-0000-4000-8000-000000000298",
|
||||
"to": "node-00000000-0000-4000-8000-000000000300"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000306",
|
||||
"from": "node-00000000-0000-4000-8000-000000000299",
|
||||
"to": "node-00000000-0000-4000-8000-000000000300"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000307",
|
||||
"from": "node-00000000-0000-4000-8000-000000000301",
|
||||
"to": "node-00000000-0000-4000-8000-000000000302"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000308",
|
||||
"from": "node-00000000-0000-4000-8000-000000000297",
|
||||
"to": "node-00000000-0000-4000-8000-000000000303"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000309",
|
||||
"from": "node-00000000-0000-4000-8000-000000000300",
|
||||
"to": "node-00000000-0000-4000-8000-000000000303"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000310",
|
||||
"from": "node-00000000-0000-4000-8000-000000000302",
|
||||
"to": "node-00000000-0000-4000-8000-000000000303"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000311",
|
||||
"from": "node-00000000-0000-4000-8000-000000000303",
|
||||
"to": "node-00000000-0000-4000-8000-000000000304"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"id": "offline_safe_off",
|
||||
"category": "reliability",
|
||||
"name": {
|
||||
"pl": "Urządzenie offline — bezpieczne wyłączenie",
|
||||
"en": "Offline device safe-off"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Gdy urządzenie jest offline, a termostat nadal aktywny, wydaje bezpieczne polecenie wyłączenia z długim cooldownem.",
|
||||
"en": "When a device is offline while its thermostat remains active, issues a conservative off command with a long cooldown."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000175",
|
||||
"kind": "device_state",
|
||||
"x": 35,
|
||||
"y": 85,
|
||||
"config": {
|
||||
"device_id": "$device1",
|
||||
"field": "online",
|
||||
"operator": "eq",
|
||||
"value": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000176",
|
||||
"kind": "zone_state",
|
||||
"x": 35,
|
||||
"y": 215,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"field": "enabled",
|
||||
"operator": "eq",
|
||||
"value": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000177",
|
||||
"kind": "logic_and",
|
||||
"x": 310,
|
||||
"y": 150,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000178",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 565,
|
||||
"y": 150,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "auto",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 300,
|
||||
"power": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000179",
|
||||
"from": "node-00000000-0000-4000-8000-000000000175",
|
||||
"to": "node-00000000-0000-4000-8000-000000000177"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000180",
|
||||
"from": "node-00000000-0000-4000-8000-000000000176",
|
||||
"to": "node-00000000-0000-4000-8000-000000000177"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000181",
|
||||
"from": "node-00000000-0000-4000-8000-000000000177",
|
||||
"to": "node-00000000-0000-4000-8000-000000000178"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"id": "overheat_guard",
|
||||
"category": "safety",
|
||||
"name": {
|
||||
"pl": "Ochrona przed przegrzaniem",
|
||||
"en": "Overheat guard"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Jeśli strefa jest bardzo ciepła, tryb domu to chłodzenie i okno jest zamknięte, uruchamia chłodzenie ochronne.",
|
||||
"en": "If a zone is very warm, house mode is cooling and the window is closed, starts protective cooling."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000109",
|
||||
"kind": "zone_temperature",
|
||||
"x": 35,
|
||||
"y": 65,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "gt",
|
||||
"value": 28
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000110",
|
||||
"kind": "house_mode",
|
||||
"x": 35,
|
||||
"y": 185,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "cool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000111",
|
||||
"kind": "ha_state",
|
||||
"x": 35,
|
||||
"y": 305,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.window",
|
||||
"operator": "eq",
|
||||
"value": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000112",
|
||||
"kind": "logic_and",
|
||||
"x": 310,
|
||||
"y": 185,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000113",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 570,
|
||||
"y": 185,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "custom",
|
||||
"setpoint": 22,
|
||||
"mode": "cool",
|
||||
"cooldown_seconds": 120
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000114",
|
||||
"from": "node-00000000-0000-4000-8000-000000000109",
|
||||
"to": "node-00000000-0000-4000-8000-000000000112"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000115",
|
||||
"from": "node-00000000-0000-4000-8000-000000000110",
|
||||
"to": "node-00000000-0000-4000-8000-000000000112"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000116",
|
||||
"from": "node-00000000-0000-4000-8000-000000000111",
|
||||
"to": "node-00000000-0000-4000-8000-000000000112"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000117",
|
||||
"from": "node-00000000-0000-4000-8000-000000000112",
|
||||
"to": "node-00000000-0000-4000-8000-000000000113"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"id": "peak_power_guard",
|
||||
"category": "energy",
|
||||
"name": {
|
||||
"pl": "Ochrona przed szczytem mocy",
|
||||
"en": "Peak power guard"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Wyłącza HVAC, gdy pobór mocy domu przekroczy ustawiony próg i system klimatu jest aktywny.",
|
||||
"en": "Turns HVAC off when household power draw exceeds the configured threshold while climate control is active."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000061",
|
||||
"kind": "ha_numeric",
|
||||
"x": 40,
|
||||
"y": 75,
|
||||
"config": {
|
||||
"entity_id": "sensor.house_power",
|
||||
"operator": "gt",
|
||||
"value": 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000062",
|
||||
"kind": "house_mode",
|
||||
"x": 40,
|
||||
"y": 200,
|
||||
"config": {
|
||||
"operator": "neq",
|
||||
"value": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000063",
|
||||
"kind": "logic_and",
|
||||
"x": 315,
|
||||
"y": 140,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000064",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 570,
|
||||
"y": 140,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "auto",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 180,
|
||||
"power": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000065",
|
||||
"from": "node-00000000-0000-4000-8000-000000000061",
|
||||
"to": "node-00000000-0000-4000-8000-000000000063"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000066",
|
||||
"from": "node-00000000-0000-4000-8000-000000000062",
|
||||
"to": "node-00000000-0000-4000-8000-000000000063"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000067",
|
||||
"from": "node-00000000-0000-4000-8000-000000000063",
|
||||
"to": "node-00000000-0000-4000-8000-000000000064"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"id": "presence_eco",
|
||||
"category": "energy",
|
||||
"name": {
|
||||
"pl": "Obecność: komfort / eco",
|
||||
"en": "Presence: comfort / eco"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Dwie jawne gałęzie Home Assistant: stan home włącza komfort, a not_home ustawia preset away. Ustaw właściwy entity_id osoby lub czujnika obecności.",
|
||||
"en": "Two explicit Home Assistant branches: home enables comfort while not_home selects the away preset. Set the correct person or presence entity_id."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000048",
|
||||
"kind": "ha_state",
|
||||
"x": 45,
|
||||
"y": 75,
|
||||
"config": {
|
||||
"entity_id": "person.someone",
|
||||
"operator": "eq",
|
||||
"value": "home"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000049",
|
||||
"kind": "ha_state",
|
||||
"x": 45,
|
||||
"y": 245,
|
||||
"config": {
|
||||
"entity_id": "person.someone",
|
||||
"operator": "eq",
|
||||
"value": "not_home"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000050",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 360,
|
||||
"y": 65,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "comfort",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 90
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000051",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 360,
|
||||
"y": 245,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "away",
|
||||
"setpoint": 18,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 180
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000052",
|
||||
"from": "node-00000000-0000-4000-8000-000000000048",
|
||||
"to": "node-00000000-0000-4000-8000-000000000050"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000053",
|
||||
"from": "node-00000000-0000-4000-8000-000000000049",
|
||||
"to": "node-00000000-0000-4000-8000-000000000051"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"id": "sensor_availability_guard",
|
||||
"category": "reliability",
|
||||
"name": {
|
||||
"pl": "Chłodzenie z kontrolą sensora",
|
||||
"en": "Cooling with sensor validation"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Najpierw sprawdza dostępność sensora HA, potem temperaturę i tryb domu; dopiero wtedy uruchamia chłodzenie.",
|
||||
"en": "Checks HA sensor availability first, then temperature and house mode before starting cooling."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000166",
|
||||
"kind": "ha_available",
|
||||
"x": 35,
|
||||
"y": 55,
|
||||
"config": {
|
||||
"entity_id": "sensor.room_temperature"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000167",
|
||||
"kind": "ha_numeric",
|
||||
"x": 35,
|
||||
"y": 175,
|
||||
"config": {
|
||||
"entity_id": "sensor.room_temperature",
|
||||
"operator": "gt",
|
||||
"value": 26
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000168",
|
||||
"kind": "house_mode",
|
||||
"x": 35,
|
||||
"y": 295,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "cool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000169",
|
||||
"kind": "logic_and",
|
||||
"x": 310,
|
||||
"y": 175,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000170",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 570,
|
||||
"y": 175,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "comfort",
|
||||
"setpoint": 21,
|
||||
"mode": "cool",
|
||||
"cooldown_seconds": 120
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000171",
|
||||
"from": "node-00000000-0000-4000-8000-000000000166",
|
||||
"to": "node-00000000-0000-4000-8000-000000000169"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000172",
|
||||
"from": "node-00000000-0000-4000-8000-000000000167",
|
||||
"to": "node-00000000-0000-4000-8000-000000000169"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000173",
|
||||
"from": "node-00000000-0000-4000-8000-000000000168",
|
||||
"to": "node-00000000-0000-4000-8000-000000000169"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000174",
|
||||
"from": "node-00000000-0000-4000-8000-000000000169",
|
||||
"to": "node-00000000-0000-4000-8000-000000000170"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"id": "sleep_temperature_guard",
|
||||
"category": "night",
|
||||
"name": {
|
||||
"pl": "Nocne chłodzenie tylko gdy trzeba",
|
||||
"en": "Night cooling only when needed"
|
||||
},
|
||||
"description": {
|
||||
"pl": "W trybie nocnym chłodzi tylko wtedy, gdy temperatura pokoju przekracza próg i dom jest w trybie cool.",
|
||||
"en": "In night mode, cools only when room temperature is above the threshold and the house is in cooling mode."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000139",
|
||||
"kind": "night_mode",
|
||||
"x": 35,
|
||||
"y": 55,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000140",
|
||||
"kind": "zone_temperature",
|
||||
"x": 35,
|
||||
"y": 175,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "gt",
|
||||
"value": 23.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000141",
|
||||
"kind": "house_mode",
|
||||
"x": 35,
|
||||
"y": 295,
|
||||
"config": {
|
||||
"operator": "eq",
|
||||
"value": "cool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000142",
|
||||
"kind": "logic_and",
|
||||
"x": 310,
|
||||
"y": 175,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000143",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 570,
|
||||
"y": 175,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "sleep",
|
||||
"setpoint": 20,
|
||||
"mode": "cool",
|
||||
"cooldown_seconds": 180
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000144",
|
||||
"from": "node-00000000-0000-4000-8000-000000000139",
|
||||
"to": "node-00000000-0000-4000-8000-000000000142"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000145",
|
||||
"from": "node-00000000-0000-4000-8000-000000000140",
|
||||
"to": "node-00000000-0000-4000-8000-000000000142"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000146",
|
||||
"from": "node-00000000-0000-4000-8000-000000000141",
|
||||
"to": "node-00000000-0000-4000-8000-000000000142"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000147",
|
||||
"from": "node-00000000-0000-4000-8000-000000000142",
|
||||
"to": "node-00000000-0000-4000-8000-000000000143"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"id": "smart_demand",
|
||||
"category": "comfort",
|
||||
"name": {
|
||||
"pl": "Inteligentne zapotrzebowanie",
|
||||
"en": "Smart demand"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Złożona logika czasu, trybu domu i alternatywy temperatury zewnętrznej lub pokojowej.",
|
||||
"en": "Complex time, house-mode and outdoor-or-room temperature logic."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000015",
|
||||
"kind": "time_range",
|
||||
"x": 30,
|
||||
"y": 40,
|
||||
"config": {
|
||||
"start": "05:30",
|
||||
"end": "23:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000016",
|
||||
"kind": "outdoor_temperature",
|
||||
"x": 30,
|
||||
"y": 160,
|
||||
"config": {
|
||||
"operator": "lt",
|
||||
"value": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000017",
|
||||
"kind": "zone_temperature",
|
||||
"x": 30,
|
||||
"y": 280,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"operator": "lt",
|
||||
"value": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000018",
|
||||
"kind": "logic_or",
|
||||
"x": 280,
|
||||
"y": 220,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000019",
|
||||
"kind": "house_mode",
|
||||
"x": 280,
|
||||
"y": 70,
|
||||
"config": {
|
||||
"operator": "neq",
|
||||
"value": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000020",
|
||||
"kind": "logic_and",
|
||||
"x": 510,
|
||||
"y": 155,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000021",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 760,
|
||||
"y": 155,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "comfort",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 90
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000022",
|
||||
"from": "node-00000000-0000-4000-8000-000000000016",
|
||||
"to": "node-00000000-0000-4000-8000-000000000018"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000023",
|
||||
"from": "node-00000000-0000-4000-8000-000000000017",
|
||||
"to": "node-00000000-0000-4000-8000-000000000018"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000024",
|
||||
"from": "node-00000000-0000-4000-8000-000000000015",
|
||||
"to": "node-00000000-0000-4000-8000-000000000020"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000025",
|
||||
"from": "node-00000000-0000-4000-8000-000000000019",
|
||||
"to": "node-00000000-0000-4000-8000-000000000020"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000026",
|
||||
"from": "node-00000000-0000-4000-8000-000000000018",
|
||||
"to": "node-00000000-0000-4000-8000-000000000020"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000027",
|
||||
"from": "node-00000000-0000-4000-8000-000000000020",
|
||||
"to": "node-00000000-0000-4000-8000-000000000021"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"id": "thermostat_enabled_guard",
|
||||
"category": "reliability",
|
||||
"name": {
|
||||
"pl": "Steruj tylko aktywnym termostatem",
|
||||
"en": "Control only an active thermostat"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Wymaga włączonej strefy, aktywnego zapotrzebowania i niezerowego trybu domu przed akcją.",
|
||||
"en": "Requires an enabled zone, active demand and a non-off house mode before the action executes."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000182",
|
||||
"kind": "zone_state",
|
||||
"x": 35,
|
||||
"y": 55,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"field": "enabled",
|
||||
"operator": "eq",
|
||||
"value": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000183",
|
||||
"kind": "zone_state",
|
||||
"x": 35,
|
||||
"y": 175,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"field": "demand",
|
||||
"operator": "eq",
|
||||
"value": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000184",
|
||||
"kind": "house_mode",
|
||||
"x": 35,
|
||||
"y": 295,
|
||||
"config": {
|
||||
"operator": "neq",
|
||||
"value": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000185",
|
||||
"kind": "logic_and",
|
||||
"x": 310,
|
||||
"y": 175,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000186",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 570,
|
||||
"y": 175,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "auto",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 120
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000187",
|
||||
"from": "node-00000000-0000-4000-8000-000000000182",
|
||||
"to": "node-00000000-0000-4000-8000-000000000185"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000188",
|
||||
"from": "node-00000000-0000-4000-8000-000000000183",
|
||||
"to": "node-00000000-0000-4000-8000-000000000185"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000189",
|
||||
"from": "node-00000000-0000-4000-8000-000000000184",
|
||||
"to": "node-00000000-0000-4000-8000-000000000185"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000190",
|
||||
"from": "node-00000000-0000-4000-8000-000000000185",
|
||||
"to": "node-00000000-0000-4000-8000-000000000186"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"id": "unoccupied_shutdown",
|
||||
"category": "energy",
|
||||
"name": {
|
||||
"pl": "Wyłącz podczas nieobecności",
|
||||
"en": "Shut down while away"
|
||||
},
|
||||
"description": {
|
||||
"pl": "W godzinach dziennych wyłącza grupę lub strefę, gdy wskazana osoba jest poza domem.",
|
||||
"en": "During daytime hours, turns off a group or zone when the selected person is away."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000077",
|
||||
"kind": "ha_state",
|
||||
"x": 35,
|
||||
"y": 70,
|
||||
"config": {
|
||||
"entity_id": "person.someone",
|
||||
"operator": "eq",
|
||||
"value": "not_home"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000078",
|
||||
"kind": "time_range",
|
||||
"x": 35,
|
||||
"y": 190,
|
||||
"config": {
|
||||
"start": "09:00",
|
||||
"end": "16:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000079",
|
||||
"kind": "logic_and",
|
||||
"x": 310,
|
||||
"y": 130,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000080",
|
||||
"kind": "group_action",
|
||||
"x": 565,
|
||||
"y": 130,
|
||||
"config": {
|
||||
"group_id": "$group1",
|
||||
"power": false,
|
||||
"mode": "",
|
||||
"preset": "",
|
||||
"setpoint": 21,
|
||||
"cooldown_seconds": 300
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000081",
|
||||
"from": "node-00000000-0000-4000-8000-000000000077",
|
||||
"to": "node-00000000-0000-4000-8000-000000000079"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000082",
|
||||
"from": "node-00000000-0000-4000-8000-000000000078",
|
||||
"to": "node-00000000-0000-4000-8000-000000000079"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000083",
|
||||
"from": "node-00000000-0000-4000-8000-000000000079",
|
||||
"to": "node-00000000-0000-4000-8000-000000000080"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"id": "weather_comfort",
|
||||
"category": "comfort",
|
||||
"name": {
|
||||
"pl": "Komfort zależny od pogody",
|
||||
"en": "Weather-aware comfort"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Łączy dzień, godzinę i temperaturę zewnętrzną przez AND przed sterowaniem termostatem.",
|
||||
"en": "Combines weekday, time and outdoor temperature with AND before thermostat control."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000006",
|
||||
"kind": "weekday",
|
||||
"x": 35,
|
||||
"y": 40,
|
||||
"config": {
|
||||
"days": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000007",
|
||||
"kind": "time_range",
|
||||
"x": 35,
|
||||
"y": 155,
|
||||
"config": {
|
||||
"start": "06:00",
|
||||
"end": "22:30"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000008",
|
||||
"kind": "outdoor_temperature",
|
||||
"x": 35,
|
||||
"y": 270,
|
||||
"config": {
|
||||
"operator": "lt",
|
||||
"value": 12
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000009",
|
||||
"kind": "logic_and",
|
||||
"x": 300,
|
||||
"y": 155,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000010",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 555,
|
||||
"y": 155,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "comfort",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 90
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000011",
|
||||
"from": "node-00000000-0000-4000-8000-000000000006",
|
||||
"to": "node-00000000-0000-4000-8000-000000000009"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000012",
|
||||
"from": "node-00000000-0000-4000-8000-000000000007",
|
||||
"to": "node-00000000-0000-4000-8000-000000000009"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000013",
|
||||
"from": "node-00000000-0000-4000-8000-000000000008",
|
||||
"to": "node-00000000-0000-4000-8000-000000000009"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000014",
|
||||
"from": "node-00000000-0000-4000-8000-000000000009",
|
||||
"to": "node-00000000-0000-4000-8000-000000000010"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"id": "weekend_comfort",
|
||||
"category": "comfort",
|
||||
"name": {
|
||||
"pl": "Weekendowy komfort",
|
||||
"en": "Weekend comfort"
|
||||
},
|
||||
"description": {
|
||||
"pl": "W sobotę i niedzielę utrzymuje komfort w ciągu dnia, jeśli tryb domu nie jest wyłączony.",
|
||||
"en": "Keeps daytime comfort on Saturday and Sunday while the house climate mode is not off."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000039",
|
||||
"kind": "weekday",
|
||||
"x": 40,
|
||||
"y": 90,
|
||||
"config": {
|
||||
"days": [
|
||||
6,
|
||||
7
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000040",
|
||||
"kind": "time_range",
|
||||
"x": 250,
|
||||
"y": 90,
|
||||
"config": {
|
||||
"start": "08:00",
|
||||
"end": "23:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000041",
|
||||
"kind": "house_mode",
|
||||
"x": 40,
|
||||
"y": 220,
|
||||
"config": {
|
||||
"operator": "neq",
|
||||
"value": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000042",
|
||||
"kind": "logic_and",
|
||||
"x": 465,
|
||||
"y": 145,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000043",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 710,
|
||||
"y": 145,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "comfort",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 90
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000044",
|
||||
"from": "node-00000000-0000-4000-8000-000000000039",
|
||||
"to": "node-00000000-0000-4000-8000-000000000042"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000045",
|
||||
"from": "node-00000000-0000-4000-8000-000000000040",
|
||||
"to": "node-00000000-0000-4000-8000-000000000042"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000046",
|
||||
"from": "node-00000000-0000-4000-8000-000000000041",
|
||||
"to": "node-00000000-0000-4000-8000-000000000042"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000047",
|
||||
"from": "node-00000000-0000-4000-8000-000000000042",
|
||||
"to": "node-00000000-0000-4000-8000-000000000043"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"id": "window_available_guard",
|
||||
"category": "safety",
|
||||
"name": {
|
||||
"pl": "Okno: dostępność + otwarcie",
|
||||
"en": "Window availability + open state"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Wyłącza HVAC dopiero gdy czujnik okna jest dostępny i zgłasza otwarcie — bez fałszywej reakcji na unavailable.",
|
||||
"en": "Turns HVAC off only when the window sensor is available and reports open, avoiding false reactions to unavailable."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000118",
|
||||
"kind": "ha_available",
|
||||
"x": 35,
|
||||
"y": 80,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.window"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000119",
|
||||
"kind": "ha_state",
|
||||
"x": 35,
|
||||
"y": 210,
|
||||
"config": {
|
||||
"entity_id": "binary_sensor.window",
|
||||
"operator": "eq",
|
||||
"value": "on"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000120",
|
||||
"kind": "logic_and",
|
||||
"x": 310,
|
||||
"y": 145,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000121",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 565,
|
||||
"y": 145,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "auto",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 60,
|
||||
"power": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000122",
|
||||
"from": "node-00000000-0000-4000-8000-000000000118",
|
||||
"to": "node-00000000-0000-4000-8000-000000000120"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000123",
|
||||
"from": "node-00000000-0000-4000-8000-000000000119",
|
||||
"to": "node-00000000-0000-4000-8000-000000000120"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000124",
|
||||
"from": "node-00000000-0000-4000-8000-000000000120",
|
||||
"to": "node-00000000-0000-4000-8000-000000000121"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"id": "workday_comfort",
|
||||
"category": "comfort",
|
||||
"name": {
|
||||
"pl": "Komfort w dni robocze",
|
||||
"en": "Workday comfort"
|
||||
},
|
||||
"description": {
|
||||
"pl": "Pon.–pt. rano uruchamia preset komfortu w wybranej strefie.",
|
||||
"en": "Weekday morning comfort preset for the selected thermostat zone."
|
||||
},
|
||||
"flow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000001",
|
||||
"kind": "weekday",
|
||||
"x": 40,
|
||||
"y": 70,
|
||||
"config": {
|
||||
"days": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000002",
|
||||
"kind": "time_range",
|
||||
"x": 250,
|
||||
"y": 70,
|
||||
"config": {
|
||||
"start": "06:00",
|
||||
"end": "08:30"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node-00000000-0000-4000-8000-000000000003",
|
||||
"kind": "zone_thermostat",
|
||||
"x": 500,
|
||||
"y": 70,
|
||||
"config": {
|
||||
"zone_id": "$zone1",
|
||||
"preset": "comfort",
|
||||
"setpoint": 21,
|
||||
"mode": "auto",
|
||||
"cooldown_seconds": 90
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000004",
|
||||
"from": "node-00000000-0000-4000-8000-000000000001",
|
||||
"to": "node-00000000-0000-4000-8000-000000000002"
|
||||
},
|
||||
{
|
||||
"id": "edge-00000000-0000-4000-8000-000000000005",
|
||||
"from": "node-00000000-0000-4000-8000-000000000002",
|
||||
"to": "node-00000000-0000-4000-8000-000000000003"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
SOURCE_MANIFEST="SOURCE_MANIFEST.sha256"
|
||||
FILE_MANIFEST="FILE_MANIFEST.sha256"
|
||||
SOURCE_TMP="${SOURCE_MANIFEST}.tmp"
|
||||
FILE_TMP="${FILE_MANIFEST}.tmp"
|
||||
|
||||
find . -type f \
|
||||
! -path "./${SOURCE_MANIFEST}" \
|
||||
! -path "./${SOURCE_TMP}" \
|
||||
! -path "./${FILE_MANIFEST}" \
|
||||
! -path "./${FILE_TMP}" \
|
||||
! -path "./.git/*" \
|
||||
! -path "./target/*" \
|
||||
! -name '*.pyc' \
|
||||
! -path '*/__pycache__/*' \
|
||||
-print0 \
|
||||
| sort -z \
|
||||
| xargs -0 sha256sum > "$SOURCE_TMP"
|
||||
|
||||
mv "$SOURCE_TMP" "$SOURCE_MANIFEST"
|
||||
sha256sum -c "$SOURCE_MANIFEST"
|
||||
|
||||
find . -type f \
|
||||
! -path "./${FILE_MANIFEST}" \
|
||||
! -path "./${FILE_TMP}" \
|
||||
! -path "./.git/*" \
|
||||
! -path "./target/*" \
|
||||
! -name '*.pyc' \
|
||||
! -path '*/__pycache__/*' \
|
||||
-print0 \
|
||||
| sort -z \
|
||||
| xargs -0 sha256sum > "$FILE_TMP"
|
||||
|
||||
mv "$FILE_TMP" "$FILE_MANIFEST"
|
||||
sha256sum -c "$FILE_MANIFEST"
|
||||
@@ -7,6 +7,7 @@ All operator-facing scripts live in this directory.
|
||||
- `service.sh` — start, stop, restart, status, logs and health helper.
|
||||
- `dev.sh` — development build/run/check workflow.
|
||||
- `smoke.sh` — HTTP/API smoke test used by `dev.sh --check`.
|
||||
- `live_realtime_test.js` — Node-based regression test for WebSocket live-state updates, control-plan push/fallback and reconnect behavior.
|
||||
- `generate_ha_migration.py` — legacy/manual Home Assistant entity mapping helper.
|
||||
- `install-lxc.sh` — compatibility alias for `install.sh`.
|
||||
- `common.sh` — shared shell functions; normally not executed directly.
|
||||
|
||||
@@ -0,0 +1,708 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.15.17.
|
||||
|
||||
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
|
||||
exercise selected validation failures. This writes the same settings back and
|
||||
therefore creates settings/events and can trigger normal settings side effects.
|
||||
|
||||
No third-party dependencies are required.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
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.17"
|
||||
|
||||
SETTINGS_PATHS: dict[str, set[str]] = {
|
||||
"/api/settings/application": {"simulator_enabled"},
|
||||
"/api/settings/gree": {
|
||||
"controller_id",
|
||||
"poll_interval_seconds",
|
||||
"zone_interval_seconds",
|
||||
"discovery_timeout_ms",
|
||||
"discovery_broadcast",
|
||||
"ping_metrics_enabled",
|
||||
"ping_interval_seconds",
|
||||
"ping_sample_count",
|
||||
"suppress_device_beep",
|
||||
"compressor_protection_enabled",
|
||||
"compressor_protection_seconds",
|
||||
},
|
||||
"/api/settings/gree-cloud": {
|
||||
"enabled",
|
||||
"region",
|
||||
"username",
|
||||
"password_configured",
|
||||
"polling_interval_seconds",
|
||||
"connectivity_metrics_enabled",
|
||||
"connectivity_metrics_interval_seconds",
|
||||
"connectivity_metrics_sample_count",
|
||||
"installation_id",
|
||||
"account_id",
|
||||
"last_successful_contact",
|
||||
"last_rest_response_time_ms",
|
||||
},
|
||||
"/api/settings/history": {"retention_days", "compaction_enabled", "event_retention_days"},
|
||||
"/api/settings/influxdb": {
|
||||
"enabled",
|
||||
"version",
|
||||
"url",
|
||||
"database",
|
||||
"username",
|
||||
"password_configured",
|
||||
"org",
|
||||
"bucket",
|
||||
"token_configured",
|
||||
"history_threshold_days",
|
||||
},
|
||||
"/api/settings/notifications": {
|
||||
"enabled",
|
||||
"mode",
|
||||
"provider",
|
||||
"pushover_configured",
|
||||
"slack_configured",
|
||||
"discord_configured",
|
||||
"cooldown_seconds",
|
||||
"communication_failure_threshold",
|
||||
"target_timeout_minutes",
|
||||
"alert_types",
|
||||
},
|
||||
"/api/settings/night": {
|
||||
"enabled", "start_time", "end_time", "max_fan_speed", "force_quiet", "use_native_sleep"
|
||||
},
|
||||
"/api/settings/home-assistant": {
|
||||
"url",
|
||||
"token_configured",
|
||||
"outdoor_entity_id",
|
||||
"sensor_stale_after_seconds",
|
||||
"allow_invalid_tls",
|
||||
"sensor_aliases",
|
||||
"flow_inputs",
|
||||
"outdoor_assist_enabled",
|
||||
},
|
||||
"/api/settings/debug": {"overlay_enabled", "gree_frames"},
|
||||
}
|
||||
|
||||
REMOVED_0120_PATHS = [
|
||||
"/api/settings",
|
||||
"/api/debug",
|
||||
"/api/events/retention",
|
||||
"/api/settings/export",
|
||||
"/api/settings/import",
|
||||
]
|
||||
|
||||
SAFE_GET_PATHS = [
|
||||
"/api/bootstrap",
|
||||
"/api/system/info",
|
||||
"/api/devices",
|
||||
"/api/zones",
|
||||
"/api/groups",
|
||||
"/api/schedules",
|
||||
"/api/automations",
|
||||
"/api/flows",
|
||||
"/api/readings",
|
||||
"/api/history",
|
||||
"/api/history/network",
|
||||
"/api/control-plan",
|
||||
"/api/events",
|
||||
"/api/access-tokens",
|
||||
"/api/configuration/export",
|
||||
]
|
||||
|
||||
# These are administrator-authenticated restricted HA reads. They are safe but
|
||||
# can legitimately fail if the server is configured to forbid this surface.
|
||||
HA_SAFE_GET_PATHS = [
|
||||
"/api/integrations/home-assistant/devices",
|
||||
"/api/integrations/home-assistant/control-plan",
|
||||
"/api/integrations/home-assistant/groups",
|
||||
"/api/integrations/home-assistant/snapshot",
|
||||
]
|
||||
|
||||
DETAIL_COLLECTIONS = [
|
||||
("/api/devices", "/api/devices/{id}"),
|
||||
("/api/zones", "/api/zones/{id}"),
|
||||
("/api/groups", "/api/groups/{id}"),
|
||||
("/api/schedules", "/api/schedules/{id}"),
|
||||
("/api/automations", "/api/automations/{id}"),
|
||||
("/api/flows", "/api/flows/{id}"),
|
||||
]
|
||||
|
||||
EXPECTED_OPENAPI_PATHS = set(SETTINGS_PATHS) | {
|
||||
"/api/history/network",
|
||||
"/api/configuration/export",
|
||||
"/api/configuration/import",
|
||||
"/api/integrations/home-assistant/snapshot",
|
||||
}
|
||||
|
||||
BOOTSTRAP_SETTINGS_SECTIONS = {
|
||||
"application", "gree", "gree_cloud", "history", "influxdb", "notifications", "night", "home_assistant", "debug"
|
||||
}
|
||||
|
||||
|
||||
class TestFailure(AssertionError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class HttpResponse:
|
||||
status: int
|
||||
headers: dict[str, str]
|
||||
body: bytes
|
||||
elapsed_ms: float
|
||||
|
||||
def text(self) -> str:
|
||||
return self.body.decode("utf-8", errors="replace")
|
||||
|
||||
def json(self) -> Any:
|
||||
try:
|
||||
return json.loads(self.text())
|
||||
except json.JSONDecodeError as exc:
|
||||
raise TestFailure(f"response is not valid JSON: {exc}; body={self.text()[:300]!r}") from exc
|
||||
|
||||
|
||||
class ApiClient:
|
||||
def __init__(self, base_url: str, token: str, timeout: float, insecure: bool) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.token = token.strip()
|
||||
self.timeout = timeout
|
||||
self.ssl_context: Optional[ssl.SSLContext] = None
|
||||
if insecure:
|
||||
self.ssl_context = ssl._create_unverified_context() # noqa: SLF001 - explicit developer option
|
||||
|
||||
def _url(self, path: str) -> str:
|
||||
if not path.startswith("/"):
|
||||
path = "/" + path
|
||||
return self.base_url + path
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
payload: Any = None,
|
||||
*,
|
||||
auth: bool = True,
|
||||
headers: Optional[dict[str, str]] = None,
|
||||
) -> HttpResponse:
|
||||
req_headers = {"Accept": "application/json"}
|
||||
if auth and self.token:
|
||||
req_headers["Authorization"] = f"Bearer {self.token}"
|
||||
if headers:
|
||||
req_headers.update(headers)
|
||||
|
||||
data: Optional[bytes] = None
|
||||
if payload is not None:
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
req_headers["Content-Type"] = "application/json"
|
||||
|
||||
req = urllib.request.Request(
|
||||
self._url(path),
|
||||
data=data,
|
||||
headers=req_headers,
|
||||
method=method.upper(),
|
||||
)
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=self.timeout, context=self.ssl_context) as resp:
|
||||
body = resp.read()
|
||||
status = resp.getcode()
|
||||
response_headers = {k.lower(): v for k, v in resp.headers.items()}
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read()
|
||||
status = exc.code
|
||||
response_headers = {k.lower(): v for k, v in exc.headers.items()}
|
||||
except urllib.error.URLError as exc:
|
||||
raise TestFailure(f"cannot connect to {self._url(path)}: {exc.reason}") from exc
|
||||
elapsed_ms = (time.perf_counter() - started) * 1000.0
|
||||
return HttpResponse(status, response_headers, body, elapsed_ms)
|
||||
|
||||
def get(self, path: str, *, auth: bool = True) -> HttpResponse:
|
||||
return self.request("GET", path, auth=auth)
|
||||
|
||||
def put(self, path: str, payload: Any) -> HttpResponse:
|
||||
return self.request("PUT", path, payload)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
name: str
|
||||
status: str
|
||||
detail: str = ""
|
||||
elapsed_ms: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Runner:
|
||||
client: ApiClient
|
||||
verbose: bool = False
|
||||
results: list[Result] = field(default_factory=list)
|
||||
|
||||
def run(self, name: str, fn: Callable[[], Optional[str]]) -> None:
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
detail = fn() or ""
|
||||
status = "PASS"
|
||||
except SkipTest as exc:
|
||||
status = "SKIP"
|
||||
detail = str(exc)
|
||||
except Exception as exc: # deliberate: one failed test must not stop the suite
|
||||
status = "FAIL"
|
||||
detail = str(exc)
|
||||
elapsed_ms = (time.perf_counter() - started) * 1000.0
|
||||
self.results.append(Result(name, status, detail, elapsed_ms))
|
||||
marker = {"PASS": "+", "FAIL": "!", "SKIP": "-"}[status]
|
||||
suffix = f" - {detail}" if detail and (self.verbose or status != "PASS") else ""
|
||||
print(f"[{marker}] {status:<4} {name} ({elapsed_ms:.0f} ms){suffix}")
|
||||
|
||||
def summary(self) -> int:
|
||||
counts = {name: sum(r.status == name for r in self.results) for name in ("PASS", "FAIL", "SKIP")}
|
||||
print("\nSummary: " + ", ".join(f"{k}={v}" for k, v in counts.items()))
|
||||
if counts["FAIL"]:
|
||||
print("\nFailures:")
|
||||
for result in self.results:
|
||||
if result.status == "FAIL":
|
||||
print(f" - {result.name}: {result.detail}")
|
||||
return 1 if counts["FAIL"] else 0
|
||||
|
||||
|
||||
class SkipTest(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def assert_status(resp: HttpResponse, *expected: int) -> None:
|
||||
if resp.status not in expected:
|
||||
body = resp.text().strip().replace("\n", " ")[:500]
|
||||
raise TestFailure(f"HTTP {resp.status}, expected {expected}; body={body!r}")
|
||||
|
||||
|
||||
def assert_json_object(resp: HttpResponse) -> dict[str, Any]:
|
||||
value = resp.json()
|
||||
if not isinstance(value, dict):
|
||||
raise TestFailure(f"expected JSON object, got {type(value).__name__}")
|
||||
return value
|
||||
|
||||
|
||||
def assert_json_collection(resp: HttpResponse) -> Any:
|
||||
value = resp.json()
|
||||
if not isinstance(value, (list, dict)):
|
||||
raise TestFailure(f"expected JSON collection/object, got {type(value).__name__}")
|
||||
return value
|
||||
|
||||
|
||||
def first_item(value: Any) -> Optional[dict[str, Any]]:
|
||||
if isinstance(value, list):
|
||||
return value[0] if value and isinstance(value[0], dict) else None
|
||||
if isinstance(value, dict):
|
||||
# Handle APIs that wrap arrays in a named property.
|
||||
for candidate in value.values():
|
||||
if isinstance(candidate, list) and candidate and isinstance(candidate[0], dict):
|
||||
return candidate[0]
|
||||
return None
|
||||
|
||||
|
||||
def convert_settings_view_to_update(path: str, view: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Build a PUT payload from a GET view without modifying stored secrets."""
|
||||
payload = copy.deepcopy(view)
|
||||
if path == "/api/settings/influxdb":
|
||||
payload.pop("password_configured", None)
|
||||
payload.pop("token_configured", None)
|
||||
# Omitted Option fields preserve existing secrets.
|
||||
elif path == "/api/settings/gree-cloud":
|
||||
payload.pop("password_configured", None)
|
||||
payload.pop("installation_id", None)
|
||||
payload.pop("account_id", None)
|
||||
payload.pop("last_successful_contact", None)
|
||||
payload.pop("last_rest_response_time_ms", None)
|
||||
elif path == "/api/settings/notifications":
|
||||
payload.pop("pushover_configured", None)
|
||||
payload.pop("slack_configured", None)
|
||||
payload.pop("discord_configured", None)
|
||||
elif path == "/api/settings/home-assistant":
|
||||
payload.pop("token_configured", None)
|
||||
return payload
|
||||
|
||||
|
||||
def comparable_settings_view(path: str, view: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Normalize a settings view for equality checks after a no-op PUT."""
|
||||
return copy.deepcopy(view)
|
||||
|
||||
|
||||
def test_health(client: ApiClient, expected_version: str) -> str:
|
||||
resp = client.get("/api/health", auth=False)
|
||||
assert_status(resp, 200)
|
||||
data = assert_json_object(resp)
|
||||
if data.get("status") != "ok":
|
||||
raise TestFailure(f"health.status={data.get('status')!r}, expected 'ok'")
|
||||
if expected_version and data.get("version") != expected_version:
|
||||
raise TestFailure(f"health.version={data.get('version')!r}, expected {expected_version!r}")
|
||||
return f"version={data.get('version')}, control_ready={data.get('control_ready')}"
|
||||
|
||||
|
||||
def test_openapi(client: ApiClient, expected_version: str) -> str:
|
||||
resp = client.get("/api-docs/openapi.json", auth=False)
|
||||
assert_status(resp, 200)
|
||||
spec = assert_json_object(resp)
|
||||
paths = spec.get("paths")
|
||||
if not isinstance(paths, dict):
|
||||
raise TestFailure("OpenAPI has no paths object")
|
||||
missing = sorted(EXPECTED_OPENAPI_PATHS - set(paths))
|
||||
if missing:
|
||||
raise TestFailure(f"OpenAPI missing required paths: {missing}")
|
||||
forbidden = sorted(set(REMOVED_0120_PATHS) & set(paths))
|
||||
if forbidden:
|
||||
raise TestFailure(f"OpenAPI still exposes removed paths: {forbidden}")
|
||||
version = ((spec.get("info") or {}).get("version"))
|
||||
if expected_version and version != expected_version:
|
||||
raise TestFailure(f"OpenAPI info.version={version!r}, expected {expected_version!r}")
|
||||
|
||||
external_dependency_operations = [
|
||||
("/api/integrations/home-assistant/test", "post"),
|
||||
("/api/integrations/home-assistant/entity", "post"),
|
||||
("/api/integrations/notifications/test", "post"),
|
||||
("/api/integrations/gree-cloud/reconnect", "post"),
|
||||
]
|
||||
for path, method in external_dependency_operations:
|
||||
responses = (((paths.get(path) or {}).get(method) or {}).get("responses") or {})
|
||||
if "502" in responses:
|
||||
raise TestFailure(f"{method.upper()} {path} must not document external dependency failures as 502")
|
||||
if "424" not in responses:
|
||||
raise TestFailure(f"{method.upper()} {path} is missing 424 Failed Dependency")
|
||||
|
||||
return f"paths={len(paths)}, version={version}"
|
||||
|
||||
|
||||
def test_bootstrap_contract(client: ApiClient) -> str:
|
||||
resp = client.get("/api/bootstrap")
|
||||
assert_status(resp, 200)
|
||||
data = assert_json_object(resp)
|
||||
required = {
|
||||
"devices", "zones", "groups", "schedules", "automations", "flows",
|
||||
"access_tokens", "settings", "house", "outdoor_temperature",
|
||||
"control_plan", "control_plan_revision", "system",
|
||||
}
|
||||
missing = sorted(required - set(data))
|
||||
if missing:
|
||||
raise TestFailure(f"bootstrap missing fields: {missing}")
|
||||
settings = data.get("settings")
|
||||
if not isinstance(settings, dict):
|
||||
raise TestFailure("bootstrap.settings is not an object")
|
||||
missing_sections = sorted(BOOTSTRAP_SETTINGS_SECTIONS - set(settings))
|
||||
if missing_sections:
|
||||
raise TestFailure(f"bootstrap.settings missing sections: {missing_sections}")
|
||||
forbidden = {
|
||||
"influxdb": {"password", "token"},
|
||||
"gree_cloud": {"password"},
|
||||
"home_assistant": {"token"},
|
||||
"notifications": {"pushover_app_token", "pushover_user_key", "slack_webhook_url", "discord_webhook_url"},
|
||||
}
|
||||
for section, fields in forbidden.items():
|
||||
leaked = sorted(fields & set(settings.get(section, {})))
|
||||
if leaked:
|
||||
raise TestFailure(f"bootstrap.settings.{section} leaks secret fields: {leaked}")
|
||||
return f"settings_sections={len(BOOTSTRAP_SETTINGS_SECTIONS)}, control_plan_revision={data.get('control_plan_revision')}"
|
||||
|
||||
|
||||
def test_protected_auth(client: ApiClient) -> str:
|
||||
if not client.token:
|
||||
raise SkipTest("no token supplied; controller may be in trusted-LAN mode")
|
||||
resp = client.get("/api/bootstrap", auth=False)
|
||||
assert_status(resp, 401)
|
||||
return "protected endpoint rejects missing token"
|
||||
|
||||
|
||||
def test_safe_get(client: ApiClient, path: str) -> str:
|
||||
resp = client.get(path)
|
||||
assert_status(resp, 200)
|
||||
content_type = resp.headers.get("content-type", "")
|
||||
if "json" not in content_type.lower():
|
||||
# Export is still expected to be JSON, but tolerate servers that return octet-stream attachment.
|
||||
if path != "/api/configuration/export":
|
||||
raise TestFailure(f"unexpected content-type {content_type!r}")
|
||||
data = resp.json()
|
||||
if path == "/api/configuration/export":
|
||||
if not isinstance(data, dict):
|
||||
raise TestFailure("configuration export is not a JSON object")
|
||||
fmt = data.get("format_version")
|
||||
if fmt != 3:
|
||||
raise TestFailure(f"configuration export format_version={fmt!r}, expected 3")
|
||||
return f"backup format_version={fmt}"
|
||||
if not isinstance(data, (list, dict)):
|
||||
raise TestFailure(f"unexpected JSON type {type(data).__name__}")
|
||||
if isinstance(data, list):
|
||||
return f"items={len(data)}"
|
||||
return f"keys={len(data)}"
|
||||
|
||||
|
||||
def test_ha_safe_get(client: ApiClient, path: str) -> str:
|
||||
resp = client.get(path)
|
||||
if not client.token:
|
||||
# Restricted HA API always requires a token, even when the normal admin
|
||||
# API runs in trusted-LAN mode.
|
||||
assert_status(resp, 401)
|
||||
return "restricted HA auth enforced (no token supplied)"
|
||||
assert_status(resp, 200)
|
||||
data = resp.json()
|
||||
if path == "/api/integrations/home-assistant/snapshot":
|
||||
if not isinstance(data, dict):
|
||||
raise TestFailure(f"snapshot is not an object: {type(data).__name__}")
|
||||
required = {"devices", "control_plan", "control_plan_revision", "groups"}
|
||||
missing = sorted(required - set(data))
|
||||
if missing:
|
||||
raise TestFailure(f"snapshot missing keys: {missing}")
|
||||
if not isinstance(data["devices"], list) or not isinstance(data["groups"], list):
|
||||
raise TestFailure("snapshot devices/groups must be arrays")
|
||||
if not isinstance(data["control_plan"], dict):
|
||||
raise TestFailure("snapshot control_plan must be an object")
|
||||
if not isinstance(data["control_plan_revision"], int):
|
||||
raise TestFailure("snapshot control_plan_revision must be an integer")
|
||||
return f"devices={len(data['devices'])}, groups={len(data['groups'])}, revision={data['control_plan_revision']}"
|
||||
if not isinstance(data, (list, dict)):
|
||||
raise TestFailure(f"unexpected JSON type {type(data).__name__}")
|
||||
if isinstance(data, list):
|
||||
return f"items={len(data)}"
|
||||
return f"keys={len(data)}"
|
||||
|
||||
|
||||
def test_settings_get(client: ApiClient, path: str, required: set[str]) -> str:
|
||||
resp = client.get(path)
|
||||
assert_status(resp, 200)
|
||||
data = assert_json_object(resp)
|
||||
missing = sorted(required - set(data))
|
||||
if missing:
|
||||
raise TestFailure(f"missing keys: {missing}")
|
||||
# Secret values must never be returned by split GETs.
|
||||
forbidden_secret_keys = {
|
||||
"/api/settings/influxdb": {"password", "token"},
|
||||
"/api/settings/gree-cloud": {"password"},
|
||||
"/api/settings/notifications": {
|
||||
"pushover_app_token", "pushover_user_key", "slack_webhook_url", "discord_webhook_url"
|
||||
},
|
||||
"/api/settings/home-assistant": {"token"},
|
||||
}.get(path, set())
|
||||
leaked = sorted(forbidden_secret_keys & set(data))
|
||||
if leaked:
|
||||
raise TestFailure(f"secret fields leaked in GET response: {leaked}")
|
||||
return f"keys={len(data)}"
|
||||
|
||||
|
||||
def test_removed_path(client: ApiClient, path: str) -> str:
|
||||
# GET is enough to prove there is no compatibility alias. Some removed write-only
|
||||
# paths may return 404 or 405 depending on router fallback/method handling.
|
||||
resp = client.get(path)
|
||||
if resp.status not in (404, 405):
|
||||
raise TestFailure(f"removed endpoint still responds with HTTP {resp.status}")
|
||||
return f"HTTP {resp.status}"
|
||||
|
||||
|
||||
def test_detail_endpoint(client: ApiClient, collection_path: str, detail_template: str) -> str:
|
||||
resp = client.get(collection_path)
|
||||
assert_status(resp, 200)
|
||||
collection = assert_json_collection(resp)
|
||||
item = first_item(collection)
|
||||
if item is None:
|
||||
raise SkipTest("collection is empty")
|
||||
item_id = item.get("id")
|
||||
if not isinstance(item_id, str) or not item_id:
|
||||
raise SkipTest("first item has no string id")
|
||||
path = detail_template.replace("{id}", urllib.parse.quote(item_id, safe=""))
|
||||
detail = client.get(path)
|
||||
assert_status(detail, 200)
|
||||
assert_json_object(detail)
|
||||
return f"id={item_id}"
|
||||
|
||||
|
||||
def test_flow_read_subresources(client: ApiClient) -> str:
|
||||
resp = client.get("/api/flows")
|
||||
assert_status(resp, 200)
|
||||
item = first_item(assert_json_collection(resp))
|
||||
if item is None or not isinstance(item.get("id"), str):
|
||||
raise SkipTest("no Flow available")
|
||||
flow_id = urllib.parse.quote(item["id"], safe="")
|
||||
export_resp = client.get(f"/api/flows/{flow_id}/export")
|
||||
assert_status(export_resp, 200)
|
||||
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()
|
||||
return f"id={item['id']}"
|
||||
|
||||
|
||||
def test_settings_roundtrip(client: ApiClient, path: str) -> str:
|
||||
before_resp = client.get(path)
|
||||
assert_status(before_resp, 200)
|
||||
before = assert_json_object(before_resp)
|
||||
payload = convert_settings_view_to_update(path, before)
|
||||
|
||||
put_resp = client.put(path, payload)
|
||||
assert_status(put_resp, 200)
|
||||
put_view = assert_json_object(put_resp)
|
||||
|
||||
after_resp = client.get(path)
|
||||
assert_status(after_resp, 200)
|
||||
after = assert_json_object(after_resp)
|
||||
|
||||
if comparable_settings_view(path, put_view) != comparable_settings_view(path, after):
|
||||
raise TestFailure(f"PUT response differs from following GET: put={put_view!r}, get={after!r}")
|
||||
|
||||
# No-op write should preserve the observable GET view, including *_configured flags.
|
||||
if comparable_settings_view(path, before) != comparable_settings_view(path, after):
|
||||
raise TestFailure(f"settings changed after no-op round-trip: before={before!r}, after={after!r}")
|
||||
return "GET -> PUT(no-op) -> GET preserved view"
|
||||
|
||||
|
||||
def expect_validation_error(client: ApiClient, path: str, mutate: Callable[[dict[str, Any]], None]) -> str:
|
||||
before_resp = client.get(path)
|
||||
assert_status(before_resp, 200)
|
||||
before = assert_json_object(before_resp)
|
||||
payload = convert_settings_view_to_update(path, before)
|
||||
mutate(payload)
|
||||
resp = client.put(path, payload)
|
||||
assert_status(resp, 400)
|
||||
error = assert_json_object(resp)
|
||||
if not isinstance(error.get("error"), str) or not error["error"]:
|
||||
raise TestFailure(f"400 response missing string error: {error!r}")
|
||||
after_resp = client.get(path)
|
||||
assert_status(after_resp, 200)
|
||||
after = assert_json_object(after_resp)
|
||||
if before != after:
|
||||
raise TestFailure("settings changed despite rejected request")
|
||||
return error["error"][:120]
|
||||
|
||||
|
||||
def run_suite(args: argparse.Namespace) -> int:
|
||||
client = ApiClient(args.base_url, args.token, args.timeout, args.insecure)
|
||||
runner = Runner(client, verbose=args.verbose)
|
||||
|
||||
print(f"GREE Controller API test: {client.base_url}")
|
||||
print(f"Expected version: {args.expected_version or '(any)'}")
|
||||
print(f"Auth token: {'yes' if args.token else 'no'}")
|
||||
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.17 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))
|
||||
|
||||
for path in SAFE_GET_PATHS:
|
||||
runner.run(f"GET {path}", lambda path=path: test_safe_get(client, path))
|
||||
|
||||
for path in HA_SAFE_GET_PATHS:
|
||||
runner.run(f"GET {path}", lambda path=path: test_ha_safe_get(client, path))
|
||||
|
||||
for path, required in SETTINGS_PATHS.items():
|
||||
runner.run(f"split settings GET {path}", lambda path=path, required=required: test_settings_get(client, path, required))
|
||||
|
||||
for path in REMOVED_0120_PATHS:
|
||||
runner.run(f"removed route {path}", lambda path=path: test_removed_path(client, path))
|
||||
|
||||
for collection, detail in DETAIL_COLLECTIONS:
|
||||
runner.run(
|
||||
f"detail read {detail}",
|
||||
lambda collection=collection, detail=detail: test_detail_endpoint(client, collection, detail),
|
||||
)
|
||||
runner.run("Flow export/logs read", lambda: test_flow_read_subresources(client))
|
||||
|
||||
if args.settings_write:
|
||||
print("\n--- settings write/validation tests ---")
|
||||
for path in SETTINGS_PATHS:
|
||||
runner.run(f"round-trip PUT {path}", lambda path=path: test_settings_roundtrip(client, path))
|
||||
|
||||
runner.run(
|
||||
"validation: empty GREE controller_id -> 400",
|
||||
lambda: expect_validation_error(client, "/api/settings/gree", lambda p: p.__setitem__("controller_id", "")),
|
||||
)
|
||||
runner.run(
|
||||
"validation: invalid night start_time -> 400",
|
||||
lambda: expect_validation_error(client, "/api/settings/night", lambda p: p.__setitem__("start_time", "99:99")),
|
||||
)
|
||||
runner.run(
|
||||
"validation: invalid notification provider -> 400",
|
||||
lambda: expect_validation_error(
|
||||
client, "/api/settings/notifications", lambda p: p.__setitem__("provider", "invalid-provider")
|
||||
),
|
||||
)
|
||||
runner.run(
|
||||
"validation: invalid HA URL scheme -> 400",
|
||||
lambda: expect_validation_error(
|
||||
client, "/api/settings/home-assistant", lambda p: p.__setitem__("url", "ftp://invalid.local")
|
||||
),
|
||||
)
|
||||
|
||||
exit_code = runner.summary()
|
||||
|
||||
if args.report_json:
|
||||
report_path = Path(args.report_json)
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report = {
|
||||
"base_url": client.base_url,
|
||||
"expected_version": args.expected_version,
|
||||
"settings_write": bool(args.settings_write),
|
||||
"exit_code": exit_code,
|
||||
"results": [r.__dict__ for r in runner.results],
|
||||
}
|
||||
report_path.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||
print(f"JSON report: {report_path}")
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Developer smoke/integration tests for GREE Controller API 0.14.18.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""Examples:
|
||||
python3 scripts/api_dev_test.py
|
||||
python3 scripts/api_dev_test.py --base-url http://192.168.1.50:8787
|
||||
GREE_API_TOKEN=secret python3 scripts/api_dev_test.py --settings-write
|
||||
python3 scripts/api_dev_test.py --settings-write --report-json /tmp/api-report.json
|
||||
|
||||
Default mode performs only reads. --settings-write re-saves the current settings
|
||||
and intentionally generates normal settings events/side effects. It does NOT send
|
||||
house/device/zone control commands.
|
||||
""",
|
||||
)
|
||||
parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help=f"API base URL (default: {DEFAULT_BASE_URL})")
|
||||
parser.add_argument("--token", default=DEFAULT_TOKEN, help="administrator app token; or set GREE_API_TOKEN")
|
||||
parser.add_argument(
|
||||
"--expected-version",
|
||||
default=DEFAULT_EXPECTED_VERSION,
|
||||
help=f"expected health/OpenAPI version; use empty string to disable (default: {DEFAULT_EXPECTED_VERSION})",
|
||||
)
|
||||
parser.add_argument("--timeout", type=float, default=8.0, help="HTTP timeout in seconds (default: 8)")
|
||||
parser.add_argument("--insecure", action="store_true", help="disable TLS certificate verification for HTTPS dev instances")
|
||||
parser.add_argument(
|
||||
"--settings-write",
|
||||
action="store_true",
|
||||
help="round-trip all 8 settings PUT endpoints and test selected 400 validation paths",
|
||||
)
|
||||
parser.add_argument("--report-json", help="write machine-readable test results to this JSON file")
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help="show PASS details")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
return run_suite(parse_args())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -98,3 +98,6 @@ display_host="${bind%:*}"
|
||||
say "Panel: http://${display_host}:${bind##*:}"
|
||||
say "Stop: Ctrl+C"
|
||||
exec "$BINARY"
|
||||
Wspierane przez Gitea
|
||||
Wersja: 1.27.2
|
||||
Strona: 30ms Szablon: 4ms
|
||||
|
||||
@@ -79,7 +79,7 @@ def main() -> int:
|
||||
parser.add_argument("--controller-token", default="", help="GREE Controller Home Assistant access token")
|
||||
parser.add_argument("--ha-url", help="Optional Home Assistant URL used to validate source entities")
|
||||
parser.add_argument("--ha-token", default="", help="Optional Home Assistant Long-Lived Access Token")
|
||||
parser.add_argument("--output", default="home-assistant/generated/gree_controller_entities.json", help="Output mapping file")
|
||||
parser.add_argument("--output", default="ha-addon/home-assistant/generated/gree_controller_entities.json", help="Output mapping file")
|
||||
args = parser.parse_args()
|
||||
|
||||
mappings: list[tuple[str, str]] = list(args.map)
|
||||
|
||||
Executable
+289
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const timers = [];
|
||||
let timerId = 0;
|
||||
let apiCalls = [];
|
||||
let apiResponder = async requestPath => requestPath === '/api/schedules' ? [] : {};
|
||||
const calls = {};
|
||||
const hit = name => () => { calls[name] = (calls[name] || 0) + 1; };
|
||||
|
||||
class FakeWebSocket {
|
||||
static OPEN = 1;
|
||||
static CONNECTING = 0;
|
||||
static CLOSED = 3;
|
||||
static instances = [];
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.readyState = FakeWebSocket.CONNECTING;
|
||||
FakeWebSocket.instances.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
const app = {
|
||||
devices: [], zones: [], groups: [], schedules: [], automations: [], flows: [], accessTokens: [],
|
||||
settings: { house_mode: 'cool', debug: { overlay_enabled: false }, home_assistant: {} },
|
||||
system: {}, outdoorTemperature: null, controlPlan: null, controlPlanRevision: null,
|
||||
controlPlanPushReady: false, controlPlanTimer: null, currentView: 'dashboard',
|
||||
sensorAliases: {}, flowSharedInputs: [], flowDraft: null, ws: null, wsTimer: null,
|
||||
token: '', debugBacklogLoaded: false,
|
||||
};
|
||||
|
||||
const context = {
|
||||
console, JSON, Number, Date, Intl, Promise, app, WebSocket: FakeWebSocket,
|
||||
location: { protocol: 'http:', host: 'controller.test' },
|
||||
setTimeout: (fn, delay) => {
|
||||
const id = ++timerId;
|
||||
timers.push({ id, fn, delay, cleared: false });
|
||||
return id;
|
||||
},
|
||||
clearTimeout: id => {
|
||||
const timer = timers.find(item => item.id === id);
|
||||
if (timer) timer.cleared = true;
|
||||
},
|
||||
api: async (...args) => {
|
||||
apiCalls.push(args);
|
||||
return apiResponder(...args);
|
||||
},
|
||||
withBase: value => value,
|
||||
$: () => null,
|
||||
isFormDirty: () => false,
|
||||
applySettingsSection: (name, data) => { app.settings[name] = data; },
|
||||
loadDebugBacklog: hit('loadDebugBacklog'),
|
||||
loadBootstrap: async () => { calls.loadBootstrap = (calls.loadBootstrap || 0) + 1; },
|
||||
updateConnectionIndicator: status => { calls[`connection:${status}`] = (calls[`connection:${status}`] || 0) + 1; },
|
||||
updateDevice: device => {
|
||||
const index = app.devices.findIndex(item => item.id === device.id);
|
||||
if (index >= 0) app.devices[index] = device; else app.devices.push(device);
|
||||
},
|
||||
toast: () => {}, tr: key => key, esc: value => String(value), locale: () => 'en-GB',
|
||||
logCategory: () => 'test', debugLine: hit('debugLine'),
|
||||
};
|
||||
|
||||
const renderNames = [
|
||||
'renderAll', 'renderSummary', 'renderDevices', 'renderGroups', 'renderHouseClimate',
|
||||
'renderZones', 'renderFlows', 'renderSchedules', 'renderAutomations', 'renderSettings',
|
||||
'renderSimulationModeBanner', 'renderSystemInfo', 'renderLogRetention', 'renderNightSettings',
|
||||
'renderHomeAssistantSettings', 'renderDebugOverlay', 'renderFlowEditor', 'renderGreeFrameStats',
|
||||
'renderControlPlan', 'renderSimulationPage', 'fillSelects',
|
||||
];
|
||||
for (const name of renderNames) context[name] = hit(name);
|
||||
|
||||
vm.createContext(context);
|
||||
vm.runInContext(fs.readFileSync(path.join(root, 'web/js-dynamic/dashboard.js'), 'utf8'), context, { filename: 'dashboard.js' });
|
||||
vm.runInContext(fs.readFileSync(path.join(root, 'web/js-dynamic/bootstrap.js'), 'utf8'), context, { filename: 'bootstrap.js' });
|
||||
vm.runInContext(fs.readFileSync(path.join(root, 'web/js-dynamic/realtime.js'), 'utf8'), context, { filename: 'realtime.js' });
|
||||
// Source files define DOM-heavy renderers. Replace those implementations for this state-machine test.
|
||||
for (const name of renderNames) context[name] = hit(name);
|
||||
context.updateDevice = device => {
|
||||
const index = app.devices.findIndex(item => item.id === device.id);
|
||||
if (index >= 0) app.devices[index] = device; else app.devices.push(device);
|
||||
};
|
||||
context.applySettingsSection = (name, data) => { app.settings[name] = data; };
|
||||
context.loadDebugBacklog = hit('loadDebugBacklog');
|
||||
context.loadBootstrap = async () => { calls.loadBootstrap = (calls.loadBootstrap || 0) + 1; };
|
||||
context.isFormDirty = () => false;
|
||||
context.updateConnectionIndicator = status => { calls[`connection:${status}`] = (calls[`connection:${status}`] || 0) + 1; };
|
||||
context.withBase = value => value;
|
||||
|
||||
async function send(event, data) {
|
||||
await context.handleWebSocketMessage({
|
||||
data: JSON.stringify({ event, timestamp: new Date().toISOString(), data }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function testBootstrapReloadQueue() {
|
||||
const queuedTimers = [];
|
||||
let firstResolve;
|
||||
let bootstrapCalls = 0;
|
||||
const firstResponse = new Promise(resolve => { firstResolve = resolve; });
|
||||
const c = {
|
||||
console, Promise, Date,
|
||||
app: { loading: false, bootstrapReloadPending: false, controlPlan: null, controlPlanRevision: null, settings: {}, sensorAliases: {}, flowSharedInputs: {}, system: {}, debugBacklogLoaded: false },
|
||||
api: async requestPath => {
|
||||
assert.equal(requestPath, '/api/bootstrap');
|
||||
bootstrapCalls += 1;
|
||||
if (bootstrapCalls === 1) return firstResponse;
|
||||
return { devices: [], zones: [], groups: [], schedules: [], automations: [], flows: [], access_tokens: [], settings: {}, house: { mode: 'cool' }, system: {}, control_plan: { marker: 'second' }, control_plan_revision: 2 };
|
||||
},
|
||||
renderAll: () => {}, scheduleControlPlanLoad: () => {}, loadDebugBacklog: () => {}, toast: () => {}, tr: key => key,
|
||||
$: () => ({ open: false }), connectWebSocket: () => {},
|
||||
setTimeout: (fn, delay) => { queuedTimers.push({ fn, delay }); return queuedTimers.length; },
|
||||
};
|
||||
vm.createContext(c);
|
||||
vm.runInContext(fs.readFileSync(path.join(root, 'web/js-dynamic/bootstrap.js'), 'utf8'), c, { filename: 'bootstrap.js' });
|
||||
const first = c.loadBootstrap();
|
||||
assert.equal(c.app.loading, true);
|
||||
await c.loadBootstrap();
|
||||
assert.equal(c.app.bootstrapReloadPending, true, 'a bootstrap request during loading must be queued');
|
||||
firstResolve({ devices: [], zones: [], groups: [], schedules: [], automations: [], flows: [], access_tokens: [], settings: {}, house: { mode: 'cool' }, system: {}, control_plan: { marker: 'first' }, control_plan_revision: 1 });
|
||||
await first;
|
||||
assert.equal(c.app.outdoorTemperature, null, 'null/missing outdoor temperature must stay unknown, not become 0 C');
|
||||
assert.equal(c.app.bootstrapReloadPending, false);
|
||||
assert.equal(queuedTimers.length, 1);
|
||||
assert.equal(queuedTimers[0].delay, 0);
|
||||
queuedTimers[0].fn();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(bootstrapCalls, 2, 'queued bootstrap reload must run after the first request finishes');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await testBootstrapReloadQueue();
|
||||
const apiCallsBeforeBootstrap = apiCalls.length;
|
||||
await send('bootstrap', {
|
||||
devices: [{ id: 'd1', power: false }],
|
||||
zones: [{ id: 'z1', demand: false }],
|
||||
groups: [{ id: 'g1', name: 'G1' }],
|
||||
schedules: [{ id: 's1', name: 'S1' }],
|
||||
automations: [{ id: 'a1', name: 'A1' }],
|
||||
flows: [{ id: 'f1', name: 'F1' }],
|
||||
access_tokens: [{ id: 't1', name: 'HA', token_prefix: 'abc', created_at: '2026-09-07T08:00:00Z' }],
|
||||
settings: {
|
||||
application: { simulator_enabled: true },
|
||||
gree: { controller_id: 'test-controller' },
|
||||
history: { retention_days: 30 }, influxdb: {}, notifications: {}, night: {},
|
||||
home_assistant: { sensor_aliases: { 'sensor.bootstrap': 'Bootstrap' }, flow_inputs: [], outdoor_assist_enabled: true },
|
||||
debug: { overlay_enabled: false },
|
||||
},
|
||||
house: { mode: 'heat' }, system: { ok: true }, outdoor_temperature: 10,
|
||||
control_plan: { marker: 'p1' }, control_plan_revision: 1,
|
||||
});
|
||||
assert.equal(apiCalls.length, apiCallsBeforeBootstrap, 'WebSocket bootstrap must not fetch split settings endpoints');
|
||||
assert.equal(app.accessTokens[0].id, 't1', 'WebSocket bootstrap must refresh access-token metadata');
|
||||
assert.equal(app.settings.controller_id, 'test-controller');
|
||||
assert.equal(app.sensorAliases['sensor.bootstrap'], 'Bootstrap');
|
||||
assert.equal(app.controlPlan.marker, 'p1');
|
||||
assert.equal(app.controlPlanRevision, 1);
|
||||
assert.equal(app.controlPlanPushReady, true);
|
||||
|
||||
await send('control_plan.updated', { revision: 2, plan: { marker: 'p2' } });
|
||||
await send('control_plan.updated', { revision: 1, plan: { marker: 'stale' } });
|
||||
assert.equal(app.controlPlan.marker, 'p2', 'stale control-plan revision must be ignored');
|
||||
|
||||
app.ws = { readyState: FakeWebSocket.OPEN };
|
||||
const timerCountWhilePushReady = timers.length;
|
||||
|
||||
await send('device.updated', { id: 'd1', power: true, online: true });
|
||||
assert.equal(app.devices[0].power, true);
|
||||
await send('device.created', { id: 'd2', power: false });
|
||||
assert(app.devices.some(item => item.id === 'd2'));
|
||||
await send('devices.discovered', { devices: [{ id: 'd3', power: false }] });
|
||||
assert(app.devices.some(item => item.id === 'd3'));
|
||||
await send('device.deleted', { id: 'd2' });
|
||||
assert(!app.devices.some(item => item.id === 'd2'));
|
||||
|
||||
await send('zone.updated', { id: 'z1', demand: true, current_temperature: 22.1 });
|
||||
assert.equal(app.zones[0].demand, true);
|
||||
await send('zone.created', { id: 'z2', demand: false });
|
||||
assert(app.zones.some(item => item.id === 'z2'));
|
||||
await send('zone.deleted', { id: 'z2' });
|
||||
assert(!app.zones.some(item => item.id === 'z2'));
|
||||
|
||||
await send('group.updated', { id: 'g1', name: 'G1 updated' });
|
||||
assert.equal(app.groups[0].name, 'G1 updated');
|
||||
await send('group.created', { id: 'g2', name: 'G2' });
|
||||
assert(app.groups.some(item => item.id === 'g2'));
|
||||
await send('group.deleted', { id: 'g2' });
|
||||
assert(!app.groups.some(item => item.id === 'g2'));
|
||||
|
||||
await send('schedule.updated', { id: 's1', name: 'S1 updated' });
|
||||
assert.equal(app.schedules[0].name, 'S1 updated');
|
||||
await send('schedule.created', { id: 's2', name: 'S2' });
|
||||
assert(app.schedules.some(item => item.id === 's2'));
|
||||
await send('schedule.deleted', { id: 's2' });
|
||||
assert(!app.schedules.some(item => item.id === 's2'));
|
||||
apiResponder = async requestPath => requestPath === '/api/schedules' ? [{ id: 's3', name: 'Template' }] : {};
|
||||
await send('schedule.template_applied', { zone_id: 'z1', template: 'family', count: 2 });
|
||||
assert.equal(app.schedules[0].id, 's3');
|
||||
|
||||
await send('automation.updated', { id: 'a1', name: 'A1 updated', last_fired_at: '2026-09-04T09:00:00Z' });
|
||||
assert.equal(app.automations[0].name, 'A1 updated');
|
||||
await send('automation.created', { id: 'a2', name: 'A2' });
|
||||
assert(app.automations.some(item => item.id === 'a2'));
|
||||
await send('automation.deleted', { id: 'a2' });
|
||||
assert(!app.automations.some(item => item.id === 'a2'));
|
||||
|
||||
await send('flow.updated', { id: 'f1', name: 'F1 updated' });
|
||||
assert.equal(app.flows[0].name, 'F1 updated');
|
||||
await send('flow.created', { id: 'f2', name: 'F2' });
|
||||
assert(app.flows.some(item => item.id === 'f2'));
|
||||
await send('flow.deleted', { id: 'f2' });
|
||||
assert(!app.flows.some(item => item.id === 'f2'));
|
||||
|
||||
await send('settings.application.updated', { poll_interval_seconds: 15 });
|
||||
await send('settings.gree.updated', { command_timeout_ms: 1000 });
|
||||
await send('settings.history.updated', { retention_days: 30 });
|
||||
await send('settings.influxdb.updated', { enabled: false });
|
||||
await send('settings.notifications.updated', { enabled: false });
|
||||
await send('settings.debug.updated', { overlay_enabled: false });
|
||||
await send('settings.night.updated', { enabled: true, start_time: '22:00', end_time: '06:00' });
|
||||
await send('settings.home_assistant.updated', { sensor_aliases: { 'sensor.room': 'Room' }, flow_inputs: [] });
|
||||
assert.equal(app.settings.night.enabled, true);
|
||||
assert.equal(app.sensorAliases['sensor.room'], 'Room');
|
||||
|
||||
await send('house.mode_changed', { mode: 'off' });
|
||||
assert.equal(app.settings.house_mode, 'off');
|
||||
await send('outdoor.updated', { temperature: 12.3 });
|
||||
assert.equal(app.outdoorTemperature, 12.3);
|
||||
|
||||
await send('gree.frame_received', { total: 5, device_id: 'd1', device_count: 3 });
|
||||
assert.equal(app.system.gree_received_frames, 5);
|
||||
assert.equal(app.system.gree_received_frames_by_device.d1, 3);
|
||||
app.settings.debug = { overlay_enabled: true };
|
||||
await send('gree.frame', { direction: 'rx', payload: {} });
|
||||
await send('api.request', { method: 'GET', status: 200, path: '/api/health', duration_ms: 1 });
|
||||
await send('log.created', { level: 'info', kind: 'test', message: 'ok', metadata: {} });
|
||||
|
||||
await send('configuration.imported', { at: new Date().toISOString() });
|
||||
assert.equal(calls.loadBootstrap, 1, 'configuration import must full-resync connected UIs');
|
||||
|
||||
// With a valid pushed control plan, ordinary state events must not re-enable HTTP control-plan polling.
|
||||
assert.equal(timers.length, timerCountWhilePushReady, 'push-ready live events must not schedule control-plan HTTP fallback');
|
||||
|
||||
// When push is unavailable, state changes must schedule the existing HTTP fallback.
|
||||
app.controlPlanPushReady = false;
|
||||
app.ws = { readyState: FakeWebSocket.CLOSED };
|
||||
const fallbackTimerCount = timers.length;
|
||||
await send('zone.updated', { id: 'z1', demand: false });
|
||||
assert(timers.length > fallbackTimerCount);
|
||||
assert.equal(timers[timers.length - 1].delay, 180);
|
||||
|
||||
// An HTTP fallback started while disconnected must never overwrite a newer pushed revision after reconnect.
|
||||
let resolveApi;
|
||||
apiResponder = () => new Promise(resolve => { resolveApi = resolve; });
|
||||
const pendingHttp = context.loadControlPlan();
|
||||
app.ws = { readyState: FakeWebSocket.OPEN };
|
||||
await send('control_plan.updated', { revision: 3, plan: { marker: 'p3' } });
|
||||
resolveApi({ marker: 'old-http' });
|
||||
await pendingHttp;
|
||||
assert.equal(app.controlPlan.marker, 'p3');
|
||||
|
||||
// Connection lifecycle still switches to fallback and schedules reconnect.
|
||||
app.ws = null;
|
||||
app.controlPlanPushReady = true;
|
||||
context.connectWebSocket();
|
||||
const socket = FakeWebSocket.instances[FakeWebSocket.instances.length - 1];
|
||||
assert.equal(socket.url, 'ws://controller.test/ws');
|
||||
socket.readyState = FakeWebSocket.OPEN;
|
||||
socket.onopen();
|
||||
assert.equal(calls['connection:connected'], 1);
|
||||
const beforeCloseTimers = timers.length;
|
||||
socket.readyState = FakeWebSocket.CLOSED;
|
||||
socket.onclose();
|
||||
assert.equal(app.controlPlanPushReady, false);
|
||||
assert(timers.length >= beforeCloseTimers + 2, 'close must schedule fallback and reconnect');
|
||||
|
||||
console.log(`Live realtime test OK (${apiCalls.length} API fallback/resync calls observed)`);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error.stack || error);
|
||||
process.exit(1);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user