v0.13.2
This commit is contained in:
Generated
+1
-1
@@ -700,7 +700,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.13.1"
|
||||
version = "0.13.2"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.13.1"
|
||||
version = "0.13.2"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+15
-14
@@ -1,13 +1,13 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
66ab5e9fc9c23c506c2cde7810ead25b63ea9831dab56bda09194af9cea378c0 ./Cargo.lock
|
||||
1bda71918037a7f726cab6d384f3eec3ee5e34ba7377ba21c7b8833c599682e3 ./Cargo.toml
|
||||
3ddb42eef37a20f8ace4cfca42bcb0a96b8e6de94f82fcf7f7a0fffc2c31b34a ./Cargo.lock
|
||||
71ee004b46883be6d12098c1a6cde9caf8076e8beb041e2cafd38c7108859ba1 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
31dd042f3566a9927d9cde6b603f77444403a1e8055539c5f4c1245f94704507 ./README.md
|
||||
97d2ed6c330e71b243de9816b50c191361ede2968ce9c0ebd31efbee35eec003 ./README.md
|
||||
d56cfff4179a6511aa764a91c6ddebe21fcd35f36b7298e62d012e11f62bfeb9 ./build.rs
|
||||
fc63a26f2c9541ffe1c8b21a5ce29a5585b5e1d9ab1fdc1e57db3cd019c78171 ./docs/API.md
|
||||
ad559c9b776cb4dcc1febc06879d8c9154f7b8ff3e0e0b4b9b394f87ec571131 ./docs/API.md
|
||||
2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md
|
||||
6ca0bf3639a3133f7a67a1744269d85752d324142cf49c93e387bc02b40a5639 ./docs/openapi.json
|
||||
ceeef0504298dc11b5964aa898e2425fc0847e0d9c1c1436d113161431a75917 ./docs/openapi.json
|
||||
b11e581dd916676e24402112fe6896bc681abf3c94cafc85c4d3ded3f3a272a3 ./home-assistant/README.md
|
||||
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
|
||||
3f6ef15ef58456376ac53fde7cace1ef359d6a6f7a64c5b575ad77ea6e55ccc4 ./home-assistant/custom_components/gree_controller/api.py
|
||||
@@ -18,7 +18,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./home-assista
|
||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
|
||||
412a887b4349c3cbdaededff76ec5ffe5a0c7dd4374b2de4ca60de02ee3f3d2e ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
2cc326a9a839908eca36bc3f0c51f38c53c05c6093c678b80ac7b2ad3b323e9a ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
|
||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
|
||||
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./home-assistant/custom_components/gree_controller/sensor.py
|
||||
@@ -68,14 +68,15 @@ ae21459a261712bcb8d57594528b1648432e8d03a8a38502234829c0dbfef774 ./presets/week
|
||||
fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/window_available_guard.json
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
|
||||
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
|
||||
9c76cf597224817576be543088e7aa0af8e5961e7045645cbca4e878bbffae2d ./scripts/api_dev_test.py
|
||||
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
|
||||
93d28fcfb77b59548ac4f4f0e03372cae8fb286142e7eb3130b438fd180caf6e ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
14076104c042fba1284ebb07531a6c3ff972df1f9f5b18f70da18ab774efed27 ./scripts/generate_ha_migration.py
|
||||
e4849261fd9ed1f01df96c0637c439c0c4eff8fa317b2918026167bba343af79 ./scripts/install-lxc.sh
|
||||
bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./scripts/install.sh
|
||||
2af0a1390e69330acb6d62bafefafd3f33088b712867f9b1fc2f8604e2f253cd ./scripts/live_realtime_test.js
|
||||
e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
@@ -87,7 +88,7 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda
|
||||
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
|
||||
368cd843dcac970f1aa6800ba132a4bbc3429e8379ed7abf9878dcb0c633af40 ./src/api/configuration.rs
|
||||
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
|
||||
0b87a7f0caef2a9c9cdea34d560095a3367e3c9d4322d6bcabc5b8ade8d55d06 ./src/api/devices.rs
|
||||
a58fb7dcf3f37e3f9d90363623ec91edb667bca97f5faa6405c8b8f539c3d998 ./src/api/devices.rs
|
||||
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
|
||||
44b74965f3256c2972d6b3cd384a63c68e3b422f414a0902c085bc92e17369bf ./src/api/flows.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
@@ -115,7 +116,7 @@ c0a7fa3eac198737d7261ad00c542a614e6d7291f6581af8e7f7aef9dec1a049 ./src/db/sched
|
||||
1a209dc6d8df864778bb03c980f309a2ec86fc62df274880fbb95eff8470174e ./src/db/tests.rs
|
||||
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
|
||||
4d2c6053586558afbca6b60fbbc9b1cd397f74f8284d7c01f3337eb42bf6d4f7 ./src/engine.rs
|
||||
d834dab18fb8970a9e44e3d2fb6e5cee4407eea15f2ccbd50dc6fceb0526db3f ./src/engine/automations.rs
|
||||
31ed13a143a80f165982662667c1248213a4db7b7e60c4b4bc25c4a96163c184 ./src/engine/automations.rs
|
||||
88f85acf692e12c7061e039dd7c54f101fe0b014c4a3d799150ad92eea9e2894 ./src/engine/commands.rs
|
||||
8ee70ad21d8ee32914dd90952b04dfa8355ee868252ef4fc682ec40503590a7a ./src/engine/control_plan.rs
|
||||
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
|
||||
@@ -123,7 +124,7 @@ e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/g
|
||||
f0c8f3384b6174771aeb5640ee1eec87f3d6d9ff79a4fbd6a35554a4eec00cfa ./src/engine/history.rs
|
||||
41b7045877771b7c352328998cb45ce2433e7f9631fad3eb1a882ab4897dc31f ./src/engine/local_thermostat.rs
|
||||
6280a7f5059d9737987bf2d72b19a26b3a6709ce578af1108c4215f3f47d7763 ./src/engine/ownership.rs
|
||||
a5250a4220deb0a9d5cdaee0cf38343a0e26ba37003f6d47e63402a9eaa1e4b2 ./src/engine/polling.rs
|
||||
3d7e74a8180e14f398363af1a45b32a5e4c89f749d631afce556a1ecd5fe4e80 ./src/engine/polling.rs
|
||||
f0bcd3a56b600944ccc62fa24ce20838033eeb4354f937cdb534630c1a1f5a78 ./src/engine/runtime.rs
|
||||
c1e8de5392681ba1255e3e2d871a6ee860fb8ca987a0a8f973d9ded3977a68fb ./src/engine/schedules.rs
|
||||
de80d9ff69f1c843fdac23fdc3e3a350fc1082a186261b3a38617af189132356 ./src/engine/targets.rs
|
||||
@@ -177,9 +178,9 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
d0388bf22f34ae28fb63074b0bc15c087a3be298bb702d006d4814ebb786b36e ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
||||
7a0baeaa8703ba8654aa402e320ced127ae12327403f339f8fefdc0cd792af0d ./web/js/bootstrap.js
|
||||
45fb54541a117a8ee0dc484cd263b6af4aafab75d94d269b126bb603b5f0392c ./web/js/bootstrap.js
|
||||
badde8a5214a0322b2c45803c02a1cb723d53b25e57c687369cd804f509d9744 ./web/js/charts.js
|
||||
13def8de3450b612bc669f441782237e992a7a27aa4784b0dda4d48dcf3482b2 ./web/js/core.js
|
||||
dd596b3362643b96be8537b9415e8c1be90d7cf458124b61121016a6742951c7 ./web/js/core.js
|
||||
d880ffdac6aa07cd10d79fbacf12f87eb4c69b48a80564bbfdd0248abe8884d5 ./web/js/dashboard.js
|
||||
d6fbd8ef4a67269afadb2f59f19fe9a359f7ec4d7bd1473ecff77df117b9558d ./web/js/entities.js
|
||||
e332b3f811958a938105272c1e8e28d6ca8ba69303001da165b4b0b85727695c ./web/js/events.js
|
||||
@@ -188,7 +189,7 @@ f06557c6d338259059b990f8f79c91d68ac8ec1f245c302468f08929b5506901 ./web/js/forms
|
||||
dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js
|
||||
7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js
|
||||
733fcc595ed9c03c9639acc6caba46f0588f29e0b95bf72891e34443e7aa9db8 ./web/js/navigation.js
|
||||
90dccce563b1cbd6119576d575ea72121a438107c4dd16ea44f8a591f4e6db90 ./web/js/realtime.js
|
||||
e3e01511a34175b88eb01dac2c557e8b112fcad5826f02a0bc3cd1e8dfb7ff91 ./web/js/realtime.js
|
||||
c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/router.js
|
||||
427376a35832cb3644206c6e89e1654bd807eec78352f80bd0dd43f74b4ec810 ./web/js/settings-ui.js
|
||||
9dd9fbcc5f00584562bae6a840189495f830d1086d9679763dbad6e9c18c4fdb ./web/js/settings.js
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
|
||||
|
||||
**Current release: 0.13.1**
|
||||
**Current release: 0.13.2**
|
||||
|
||||
Release 0.13.1 keeps all 0.13.0 API/UI contracts and reduces real-time churn: regulator-only `updated_at` heartbeats no longer emit full `zone.updated` frames, device heartbeat polls no longer invalidate the materialized control plan unless plan-relevant device state changed, and runtime log traffic no longer triggers control-plan rebuilds. Real zone/device/control changes remain live.
|
||||
Release 0.13.2 keeps the 0.13.x HTTP/UI contracts, retains the reduced WebSocket/control-plan churn from 0.13.1, and completes live synchronization for zone creation, schedules, automations, configuration imports, device bind/communication-health changes and automation runtime metadata. Real state changes remain pushed immediately while no-op regulator heartbeats stay deduplicated.
|
||||
|
||||
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
|
||||
|
||||
|
||||
+10
-3
@@ -1,6 +1,6 @@
|
||||
# GREE Controller API reference
|
||||
|
||||
HTTP and WebSocket API for GREE Controller **0.13.1**.
|
||||
HTTP and WebSocket API for GREE Controller **0.13.2**.
|
||||
|
||||
[← Main documentation](../README.md)
|
||||
|
||||
@@ -231,7 +231,7 @@ Response:
|
||||
{
|
||||
"status": "ok",
|
||||
"name": "gree-controller",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.2",
|
||||
"uptime_seconds": 1234,
|
||||
"control_ready": true,
|
||||
"time": "2026-08-30T06:54:00Z"
|
||||
@@ -257,7 +257,7 @@ Returns the initial Web UI snapshot:
|
||||
"control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []},
|
||||
"control_plan_revision": 42,
|
||||
"system": {
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.2",
|
||||
"uptime_seconds": 1234,
|
||||
"auth_required": false,
|
||||
"control_ready": true,
|
||||
@@ -1433,6 +1433,9 @@ schedule.template_applied
|
||||
automation.created
|
||||
automation.updated
|
||||
automation.deleted
|
||||
flow.created
|
||||
flow.updated
|
||||
flow.deleted
|
||||
settings.application.updated
|
||||
settings.gree.updated
|
||||
settings.history.updated
|
||||
@@ -1442,8 +1445,10 @@ settings.night.updated
|
||||
settings.home_assistant.updated
|
||||
settings.debug.updated
|
||||
house.mode_changed
|
||||
outdoor.updated
|
||||
configuration.imported
|
||||
api.request
|
||||
gree.frame_received
|
||||
gree.frame
|
||||
log.created
|
||||
```
|
||||
@@ -1452,6 +1457,8 @@ log.created
|
||||
|
||||
`zone.updated` keeps the existing full-zone payload. Regulator passes that only advance the internal `updated_at` heartbeat are deduplicated and do not emit a frame; any actual zone state/sensor/control change still emits the full snapshot. Device polling keeps `device.updated` heartbeats for live `last_seen` UI, but heartbeat-only fields do not invalidate `control-plan`.
|
||||
|
||||
In `0.13.2`, the bundled Web UI mirrors entity CRUD events directly into its local state. Schedule-template replacement performs a targeted schedule resync, configuration import performs a full bootstrap resync, and runtime automation/device-health changes publish their updated snapshots immediately.
|
||||
|
||||
Additional engine/integration events may be introduced without changing the envelope.
|
||||
|
||||
`api.request` data:
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "GREE Controller API",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.2",
|
||||
"summary": "Local HTTP/WebSocket API for GREE HVAC control",
|
||||
"description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nNormal administrator endpoints accept `Authorization: Bearer <APP_TOKEN>` or `x-api-token: <APP_TOKEN>` when `GREE_CONTROLLER_APP_TOKEN` is configured. If the app token is empty, those endpoints intentionally run in trusted-LAN mode without authentication.\n\nThe restricted `/api/integrations/home-assistant/*` control surface always requires either a generated access token or the administrator app token. Generated tokens do **not** grant normal administrator API access.\n\nIn Swagger UI use **Authorize** and enter the token in either the bearer field or `x-api-token` field.\n\n### Control ownership\nThermostat zones, groups, house rules, direct device control, schedules and visual Flows share an ownership/arbitration model. Prefer zone/group/house endpoints for thermostat intent; direct device commands are technical/manual control and can deliberately take ownership.\n\n### 0.12.0 breaking API\nRuntime settings are split into functional `/api/settings/*` resources. The former `/api/settings`, `/api/debug`, `/api/events/retention`, `/api/settings/export` and `/api/settings/import` endpoints were removed without compatibility aliases.\n\n### 0.13.0\n`control-plan` keeps its existing HTTP response shape but is materialized in memory and pushed to WebSocket clients as `control_plan.updated`; HTTP remains the fallback/resync path. A restricted Home Assistant snapshot endpoint combines devices, groups and the control plan.\n\n### 0.13.1\nRuntime WebSocket churn is reduced without changing contracts: no-op zone regulator heartbeats are deduplicated and device heartbeat polls only invalidate the materialized control plan when plan-relevant device state changes.",
|
||||
"description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nNormal administrator endpoints accept `Authorization: Bearer <APP_TOKEN>` or `x-api-token: <APP_TOKEN>` when `GREE_CONTROLLER_APP_TOKEN` is configured. If the app token is empty, those endpoints intentionally run in trusted-LAN mode without authentication.\n\nThe restricted `/api/integrations/home-assistant/*` control surface always requires either a generated access token or the administrator app token. Generated tokens do **not** grant normal administrator API access.\n\nIn Swagger UI use **Authorize** and enter the token in either the bearer field or `x-api-token` field.\n\n### Control ownership\nThermostat zones, groups, house rules, direct device control, schedules and visual Flows share an ownership/arbitration model. Prefer zone/group/house endpoints for thermostat intent; direct device commands are technical/manual control and can deliberately take ownership.\n\n### 0.12.0 breaking API\nRuntime settings are split into functional `/api/settings/*` resources. The former `/api/settings`, `/api/debug`, `/api/events/retention`, `/api/settings/export` and `/api/settings/import` endpoints were removed without compatibility aliases.\n\n### 0.13.0\n`control-plan` keeps its existing HTTP response shape but is materialized in memory and pushed to WebSocket clients as `control_plan.updated`; HTTP remains the fallback/resync path. A restricted Home Assistant snapshot endpoint combines devices, groups and the control plan.\n\n### 0.13.1\nRuntime WebSocket churn is reduced without changing contracts: no-op zone regulator heartbeats are deduplicated and device heartbeat polls only invalidate the materialized control plan when plan-relevant device state changes.\n\n### 0.13.2\nCompletes live WebSocket synchronization for entity creation, schedules, automations, configuration imports and runtime device/automation state while preserving the existing HTTP API contracts and control-plan fallback behavior.",
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
}
|
||||
@@ -4891,7 +4891,7 @@
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "0.13.1"
|
||||
"example": "0.13.2"
|
||||
},
|
||||
"uptime_seconds": {
|
||||
"type": "integer",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.2",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.13.1.
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.13.2.
|
||||
|
||||
Default mode is read-only and safe to run against a real controller.
|
||||
Use --settings-write to additionally round-trip all split settings resources and
|
||||
@@ -26,7 +26,7 @@ from typing import Any, Callable, Iterable, Optional
|
||||
|
||||
DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787")
|
||||
DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "")
|
||||
DEFAULT_EXPECTED_VERSION = "0.13.1"
|
||||
DEFAULT_EXPECTED_VERSION = "0.13.2"
|
||||
|
||||
SETTINGS_PATHS: dict[str, set[str]] = {
|
||||
"/api/settings/application": {"simulator_enabled"},
|
||||
@@ -519,7 +519,7 @@ def run_suite(args: argparse.Namespace) -> int:
|
||||
print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n")
|
||||
|
||||
runner.run("public health", lambda: test_health(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.13.1 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.13.2 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("protected API requires auth", lambda: test_protected_auth(client))
|
||||
|
||||
for path in SAFE_GET_PATHS:
|
||||
@@ -587,7 +587,7 @@ def run_suite(args: argparse.Namespace) -> int:
|
||||
|
||||
def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Developer smoke/integration tests for GREE Controller API 0.13.1.",
|
||||
description="Developer smoke/integration tests for GREE Controller API 0.13.2.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""Examples:
|
||||
python3 scripts/api_dev_test.py
|
||||
|
||||
Executable
+278
@@ -0,0 +1,278 @@
|
||||
#!/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: [],
|
||||
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,
|
||||
loadSettingsSections: async () => {},
|
||||
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/dashboard.js'), 'utf8'), context, { filename: 'dashboard.js' });
|
||||
vm.runInContext(fs.readFileSync(path.join(root, 'web/js/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.loadSettingsSections = async () => {};
|
||||
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: [], house: { mode: 'cool' }, system: {}, control_plan: { marker: 'second' }, control_plan_revision: 2 };
|
||||
},
|
||||
fetchSettingsSections: async () => ({ application: {}, gree: {}, history: {}, influxdb: {}, notifications: {}, night: {}, home_assistant: {}, debug: {} }),
|
||||
aggregateSettingsSections: () => ({ home_assistant: {}, debug: { overlay_enabled: false } }),
|
||||
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/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: [], house: { mode: 'cool' }, system: {}, control_plan: { marker: 'first' }, control_plan_revision: 1 });
|
||||
await first;
|
||||
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();
|
||||
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' }],
|
||||
house: { mode: 'heat' }, system: { ok: true }, outdoor_temperature: 10,
|
||||
control_plan: { marker: 'p1' }, control_plan_revision: 1,
|
||||
});
|
||||
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);
|
||||
});
|
||||
@@ -293,6 +293,7 @@ async fn bind_device(
|
||||
device.last_error = None;
|
||||
device.updated_at = Utc::now();
|
||||
state.db.save_device(&device)?;
|
||||
state.broadcast("device.updated", serde_json::to_value(&device)?);
|
||||
state.log(
|
||||
"info",
|
||||
"device.bound",
|
||||
|
||||
@@ -161,9 +161,9 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
item.last_fired_at = Some(fired_at);
|
||||
item.updated_at = Utc::now();
|
||||
state.db.save_automation(&item)?;
|
||||
// last_fired_at/next_ready_at are part of ControlPlan.rules. Runtime execution
|
||||
// does not emit automation.updated, so invalidate the plan directly.
|
||||
state.invalidate_control_plan();
|
||||
// Keep connected clients in sync with runtime metadata such as last_fired_at.
|
||||
// automation.updated also invalidates the materialized control plan.
|
||||
state.broadcast("automation.updated", serde_json::to_value(&item)?);
|
||||
state.log("info", "automation.fired", &format!("Automation {} fired", item.name), json!({
|
||||
"automation_id": item.id, "flow_id": item.flow_id, "flow_node_id": item.flow_node_id, "group_id": item.action_group_id, "device_id": item.action_device_id
|
||||
}));
|
||||
@@ -184,7 +184,7 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
item.last_fired_at = Some(Utc::now());
|
||||
item.updated_at = Utc::now();
|
||||
state.db.save_automation(&item)?;
|
||||
state.invalidate_control_plan();
|
||||
state.broadcast("automation.updated", serde_json::to_value(&item)?);
|
||||
state.log("error", "automation.error", &err.to_string(), json!({"automation_id": item.id, "flow_id": item.flow_id, "flow_node_id": item.flow_node_id}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,9 +198,8 @@ fn record_poll_failure(device: &mut Device, error: &str) {
|
||||
fn register_device_failure(state: &AppState, device: &mut Device, error: &str) -> Result<(), AppError> {
|
||||
record_poll_failure(device, error);
|
||||
state.db.save_device(device)?;
|
||||
// Command failures can change online/communication health without a device.updated frame.
|
||||
// Keep the materialized plan current explicitly instead of relying on log-event prefixes.
|
||||
state.invalidate_control_plan();
|
||||
// Command failures change live communication health; publish the updated snapshot immediately.
|
||||
state.broadcast("device.updated", serde_json::to_value(&*device)?);
|
||||
state.log("warn", "device.communication_error", &format!("{}: {error}", device.name), json!({
|
||||
"device_id": device.id,
|
||||
"consecutive_failures": device.communication_failures,
|
||||
|
||||
Vendored
+5
-1
@@ -1,5 +1,5 @@
|
||||
async function loadBootstrap(showMessage = false) {
|
||||
if (app.loading) return;
|
||||
if (app.loading) { app.bootstrapReloadPending = true; return; }
|
||||
app.loading = true;
|
||||
try {
|
||||
const [data, sections] = await Promise.all([api('/api/bootstrap'), fetchSettingsSections()]);
|
||||
@@ -28,6 +28,10 @@ async function loadBootstrap(showMessage = false) {
|
||||
if (!String(error.message).toLowerCase().includes('token')) toast(error.message, true);
|
||||
} finally {
|
||||
app.loading = false;
|
||||
if (app.bootstrapReloadPending) {
|
||||
app.bootstrapReloadPending = false;
|
||||
setTimeout(() => loadBootstrap(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ const preferredTheme = ['system', 'light', 'dark'].includes(getCookie('gree_cont
|
||||
const app = {
|
||||
devices: [], zones: [], groups: [], schedules: [], automations: [], flows: [], accessTokens: [], settings: null, system: {}, outdoorTemperature: null,
|
||||
token: localStorage.getItem('gree_controller_token') || '', ws: null, wsTimer: null,
|
||||
currentView: 'dashboard', loading: false, language: preferredLanguage, theme: preferredTheme, connectionStatus: 'connecting',
|
||||
currentView: 'dashboard', loading: false, bootstrapReloadPending: false, language: preferredLanguage, theme: preferredTheme, connectionStatus: 'connecting',
|
||||
languages: [], translations: {}, locales: {},
|
||||
historyTab: 'overview', historyData: { zones: [], devices: [], sensors: [] }, historyCounts: {},
|
||||
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyLoading: false,
|
||||
|
||||
+9
-2
@@ -28,13 +28,20 @@ async function handleWebSocketMessage(event) {
|
||||
}
|
||||
const data = message.data || {};
|
||||
if (message.event === 'control_plan.updated') { app.controlPlanPushReady = true; stopControlPlanFallback(); applyControlPlan(data.plan, data.revision); }
|
||||
else if (['device.updated', 'device.created'].includes(message.event)) { updateDevice(data); renderSummary(); renderDevices(); renderGroups(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'device.updated') { updateDevice(data); renderSummary(); renderDevices(); renderGroups(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'device.created') { updateDevice(data); renderSummary(); renderDevices(); renderGroups(); fillSelects(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'device.deleted') { app.devices = app.devices.filter(v => v.id !== data.id); renderAll(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'devices.discovered') { (data.devices || []).forEach(updateDevice); renderAll(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'zone.updated') { const i = app.zones.findIndex(v => v.id === data.id); if (i >= 0) app.zones[i] = data; else app.zones.push(data); renderSummary(); renderHouseClimate(); renderZones(); renderDevices(); renderGroups(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'zone.created') { const i = app.zones.findIndex(v => v.id === data.id); if (i >= 0) app.zones[i] = data; else app.zones.push(data); renderSummary(); renderHouseClimate(); renderZones(); renderDevices(); renderGroups(); fillSelects(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'zone.deleted') { app.zones = app.zones.filter(v => v.id !== data.id); renderAll(); scheduleControlPlanLoad(); }
|
||||
else if (['group.updated', 'group.created'].includes(message.event)) { const i = app.groups.findIndex(v => v.id === data.id); if (i >= 0) app.groups[i] = data; else app.groups.push(data); renderGroups(); fillSelects(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'group.deleted') { app.groups = app.groups.filter(v => v.id !== data.id); renderGroups(); fillSelects(); scheduleControlPlanLoad(); }
|
||||
else if (['schedule.updated', 'schedule.created'].includes(message.event)) { const i = app.schedules.findIndex(v => v.id === data.id); if (i >= 0) app.schedules[i] = data; else app.schedules.push(data); renderSchedules(); fillSelects(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'schedule.deleted') { app.schedules = app.schedules.filter(v => v.id !== data.id); renderSchedules(); fillSelects(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'schedule.template_applied') { try { app.schedules = await api('/api/schedules'); renderSchedules(); fillSelects(); } catch (_) { } scheduleControlPlanLoad(); }
|
||||
else if (['automation.updated', 'automation.created'].includes(message.event)) { const i = app.automations.findIndex(v => v.id === data.id); if (i >= 0) app.automations[i] = data; else app.automations.push(data); renderAutomations(); fillSelects(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'automation.deleted') { app.automations = app.automations.filter(v => v.id !== data.id); renderAutomations(); fillSelects(); scheduleControlPlanLoad(); }
|
||||
else if (['flow.updated', 'flow.created'].includes(message.event)) { const i = app.flows.findIndex(v => v.id === data.id); if (i >= 0) app.flows[i] = data; else app.flows.push(data); renderFlows(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'flow.deleted') { app.flows = app.flows.filter(v => v.id !== data.id); renderFlows(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'settings.application.updated') { applySettingsSection('application', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); renderSimulationModeBanner(); renderSystemInfo(); }
|
||||
@@ -51,6 +58,7 @@ async function handleWebSocketMessage(event) {
|
||||
}
|
||||
else if (message.event === 'house.mode_changed') { app.settings = app.settings || {}; app.settings.house_mode = data.mode || 'cool'; renderHouseClimate(); renderGroups(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'outdoor.updated') { app.outdoorTemperature = Number.isFinite(Number(data.temperature)) ? Number(data.temperature) : null; renderHouseClimate(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'configuration.imported') { await loadBootstrap(); return; }
|
||||
else if (message.event === 'gree.frame_received') {
|
||||
app.system = app.system || {};
|
||||
app.system.gree_received_frames = Number(data.total || 0);
|
||||
@@ -62,7 +70,6 @@ async function handleWebSocketMessage(event) {
|
||||
else if (message.event === 'gree.frame') { if (app.settings?.debug?.overlay_enabled) debugLine('GREE', `${data.direction || '?'} ${data.protocol_version || ''}`, data.device_name || data.device_id || data.target || '', message.timestamp, data.payload); }
|
||||
else if (message.event === 'api.request') { if (app.settings?.debug?.overlay_enabled) debugLine('HTTP', `${data.method || '?'} ${data.status || ''}`, `${data.path || ''} · ${data.duration_ms ?? '?'} ms`, message.timestamp); }
|
||||
else if (message.event === 'log.created') { if (app.settings?.debug?.overlay_enabled) debugLine('API', data.kind || data.level || 'log', data.message || '', message.timestamp, data.metadata); if (app.currentView === 'logs') loadLogs(); }
|
||||
else if (message.event.startsWith('schedule.') || message.event.startsWith('automation.') || message.event.startsWith('flow.')) scheduleControlPlanLoad();
|
||||
} catch (_) { }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user