This commit is contained in:
Mateusz Gruszczyński
2026-09-07 10:01:52 +02:00
parent 11da46c4d6
commit 7772e1e339
18 changed files with 347 additions and 170 deletions
Generated
+1 -1
View File
@@ -700,7 +700,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.13.6"
version = "0.13.7"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
version = "0.13.6"
version = "0.13.7"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+17 -18
View File
@@ -1,13 +1,13 @@
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
174b5c57813e07375723f80f92772d965b1b434c02881d916c89efac0b61ee7e ./Cargo.lock
dc8d8412b74e19f2d32570ca0ef176c0036a140e47e41d020cb4d140ad9bfe39 ./Cargo.toml
d4330d1b80dd2bf948a5157c97581c2ca10dc69b7d6740fd55c48a699c2e3bc6 ./Cargo.lock
71d0ce719983e6d41bd89b592a6eaf66658479c0c30424b88d1133d80936d4aa ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
079ddac7ba983a3d679b8c748bd43c731eb078e91a40c5d8a683bd76e14f06c5 ./README.md
a7acf66c0bbae0c6ca3faa2993b4b8ba80fec935902d0102f34693ffc96b7a19 ./README.md
d56cfff4179a6511aa764a91c6ddebe21fcd35f36b7298e62d012e11f62bfeb9 ./build.rs
ed543caace807645561db525c19ae2c3745600af146e1c1ea9b89a39a7f597a3 ./docs/API.md
6d42d846df9e6115acedd9ac0ba84d05689046dc3f71fb2311c074638488d8f6 ./docs/API.md
2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md
cb62e2ea377bdf4785c139ee169eec4914e04ceac6883c0afcb4c96fe60427b0 ./docs/openapi.json
6da82ee5007330b210a1645aed7df7951f8c858e04009631e89de51f32815ecf ./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
5d8a5f7be7e11b385d7c22a10dedce418c809e5d737e3a402ee21c7e2a5ae316 ./home-assistant/custom_components/gree_controller/manifest.json
542d58cc7f47b55c786619c831f7bcdecaa3699940f29d9a7f26f01aaa0f0547 ./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
@@ -69,20 +69,20 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
0e734dd7cdb8fa6226e31a96927861b44a41e75dea27f27ff4a539002d5bcf35 ./scripts/api_dev_test.py
b1e6a729a28eec03b38f013377d26b95669b58c035802c96ea9201cb633d87b2 ./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
1a84c91a89bf7f60aa4816a7650640f315efadf39bf7b54dc20f1259b2e2d5ce ./scripts/live_realtime_test.js
e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
7e8f558b284362f55f50bd7223a89754a2a4960e746a27035cc61180f4a2da6f ./scripts/smoke.sh
472db31959be29ba99247f7e9cc07a44bc090c538cd654ad8d16e094726037a3 ./scripts/smoke.sh
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
4b795bcb2e6b73c0dc9687325f6ffb0e6f008c6694b2a9fb8190a45a563451b2 ./src/api.rs
6b6cab48685c56893fbe31174460254347d73a354598d0bb1f22b195df91da3e ./src/api.rs
5027797b9cefb9f0aa04c792e2ac9a38745c6bf1ee822f50eb70574fa6f997e2 ./src/api/assets.rs
05c255919f7b3d81188fc5f1410be6a0037471dfc44e93bffb009f29a1e6a3fc ./src/api/auth.rs
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
@@ -97,11 +97,10 @@ e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/hous
21636a6beb83e7bda961ef3ee383f6da317ffaa90b89fbcefa1547beb888de2d ./src/api/integrations.rs
6bfbe5135d4f93bd8d19a707877b53ed2029fd8490b4b1954a7520888fb1d664 ./src/api/middleware.rs
45e34a447ff384d99a3c42c489ca9fa75849c74d4d246caa91c63d927de17396 ./src/api/openapi.rs
744ce18aa9e0227c6cdfb6980063d0d738d74c5865d45c20fd20026d4742bb3e ./src/api/public_settings.rs
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
9d157e1901081c077e32430d5d4d7f328ffb4933996579f584ce2959d5d10f47 ./src/api/settings.rs
df3297f36f0d0fa071b872967c5c124ed3887805d4f0f1031761cfba25331e97 ./src/api/system.rs
badd52159fa5d19c236cf9022b623d15c6fbfc7f0b7fc59e269717eb5ee3d10b ./src/api/websocket.rs
95db93b7bf6d3d93723f1ebadbf7cd9b922fbe3d98966963df1dc94001c0c0e2 ./src/api/settings.rs
45d6f9d62dddb677b382c40d529aab9b0a4f30cef98c161d76d0492b7a0531cb ./src/api/system.rs
9118a5a3a381b010e22d646498ff56d2e05f51656789682cc4371b09c384aa98 ./src/api/websocket.rs
455c7238c9788d378e909a4fbc94d5390b67edd92e127f48b4b913ddcbe82fb0 ./src/api/zones.rs
f06014454d5062b1be17ac50d875a42c02fc778b59702ec455611265ab7de934 ./src/config.rs
e0340b0e5945d5a38a1054e549c819566d5c1ef6034ab0e4d44c68a3b9a249cf ./src/db.rs
@@ -149,7 +148,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f
cf63af00d98410a1452f205ce00b8a6c5f0f3525b994cba3e971d631db288396 ./src/models/history.rs
e1621255217ee6128151cc898373dd404123cb6d54017beec6c7e3e5c3030486 ./src/models/integrations.rs
8294749dd3af524925309c51ad641ffd32ff1fd9c164cfa71660aa88c2d13d6e ./src/models/runtime.rs
887eb80a58998e90023e952c1fde67435eb843493387ae64536613b74c084534 ./src/models/settings_api.rs
41eafa63499dd800b499167ddaedce7a6fc43751734aaca789fcbe2fb992c668 ./src/models/settings_api.rs
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
7d798228a8d4d4e60a112167c001535f4b8a5fa32ec7a33554c73125c42256c2 ./src/models/zone.rs
62e696df8b86835df7ad127f03c257a78580769dc82bc66daa5041f71e8440a5 ./src/notifications.rs
@@ -178,7 +177,7 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
9e98df7a759bd76bc7682b23c3c4b7e29f7bde972f03ae316ba7f8159301e2c6 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
45fb54541a117a8ee0dc484cd263b6af4aafab75d94d269b126bb603b5f0392c ./web/js/bootstrap.js
f272912f6e5d8b34786827d78ba592513e3ca1e85e07e4c073089c5bbb75f282 ./web/js/bootstrap.js
badde8a5214a0322b2c45803c02a1cb723d53b25e57c687369cd804f509d9744 ./web/js/charts.js
dd596b3362643b96be8537b9415e8c1be90d7cf458124b61121016a6742951c7 ./web/js/core.js
d880ffdac6aa07cd10d79fbacf12f87eb4c69b48a80564bbfdd0248abe8884d5 ./web/js/dashboard.js
@@ -189,10 +188,10 @@ cf1ebf3b9d666f309a1add070b4da86be86d990b02c8d6474f6d121c6d332746 ./web/js/forms
dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js
7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js
733fcc595ed9c03c9639acc6caba46f0588f29e0b95bf72891e34443e7aa9db8 ./web/js/navigation.js
0f0ecb9fcc2b47aece715de4ededdc1cf512daf404c37efe33bf9069027221e3 ./web/js/realtime.js
e0a6b5d2900f5524dba395b97288f8d75976e31b27e26810d42febbfd723085c ./web/js/realtime.js
c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/router.js
25f5c9a0aa8622f419b05f64712c9eca4896f0e79818616904b83051fd77b07b ./web/js/settings-ui.js
7399a951ae585d7c9ee55cc57064c64a2806ec725e2456cfe6b09bc61f8f4166 ./web/js/settings.js
4aadfdf107593d32ab73514688f225bf559fd5ce6bbf9d9459a8098daf1fbe3d ./web/js/settings.js
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
0b9774bb388158e23f8c90297d70659a015e0db58d25eadcae316a49e3c9fcb7 ./web/styles.css
ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js
+3 -1
View File
@@ -2,7 +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.6**
**Current release: 0.13.7**
Release 0.13.7 consolidates the Web UI startup snapshot: `/api/bootstrap` now includes the same redacted settings views exposed by the split settings endpoints, eliminating redundant startup/resync requests while keeping the functional settings API unchanged. Bootstrap/system response construction and WebSocket state application are also deduplicated.
Release 0.13.6 adds a dedicated notification toggle for GREE vs Home Assistant room-temperature discrepancies. Disabling this alert suppresses `zone.sensor_discrepancy` notifications only; sensor fallback and thermostat safety behavior are unchanged.
+44 -3
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.13.6**.
HTTP and WebSocket API for GREE Controller **0.13.7**.
[← Main documentation](../README.md)
@@ -231,7 +231,7 @@ Response:
{
"status": "ok",
"name": "gree-controller",
"version": "0.13.6",
"version": "0.13.7",
"uptime_seconds": 1234,
"control_ready": true,
"time": "2026-08-30T06:54:00Z"
@@ -251,13 +251,50 @@ Returns the initial Web UI snapshot:
"groups": [],
"schedules": [],
"automations": [],
"flows": [],
"access_tokens": [],
"settings": {
"application": {"simulator_enabled": false},
"gree": {
"controller_id": "gree-controller",
"poll_interval_seconds": 10,
"zone_interval_seconds": 10,
"discovery_timeout_ms": 3000,
"discovery_broadcast": "auto",
"suppress_device_beep": false,
"compressor_protection_enabled": true,
"compressor_protection_seconds": 180
},
"history": {"retention_days": 30, "compaction_enabled": true, "event_retention_days": 30},
"influxdb": {
"enabled": false, "version": "2", "url": "", "database": "gree_controller",
"username": "", "password_configured": false, "org": "", "bucket": "",
"token_configured": false, "history_threshold_days": 30
},
"notifications": {
"enabled": false, "mode": "problems", "provider": "pushover",
"pushover_configured": false, "slack_configured": false, "discord_configured": false,
"cooldown_seconds": 300, "communication_failure_threshold": 3,
"target_timeout_minutes": 60, "alert_types": {}
},
"night": {
"enabled": false, "start_time": "22:00", "end_time": "06:00",
"max_fan_speed": 1, "force_quiet": true, "use_native_sleep": true
},
"home_assistant": {
"url": "", "token_configured": false, "default_entity_id": "",
"outdoor_entity_id": "", "sensor_stale_after_seconds": 300,
"allow_invalid_tls": false, "sensor_aliases": {}, "flow_inputs": [],
"outdoor_assist_enabled": true
},
"debug": {"overlay_enabled": false, "gree_frames": false}
},
"house": {"mode": "cool"},
"outdoor_temperature": null,
"control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []},
"control_plan_revision": 42,
"system": {
"version": "0.13.6",
"version": "0.13.7",
"uptime_seconds": 1234,
"auth_required": false,
"control_ready": true,
@@ -274,6 +311,8 @@ Returns the initial Web UI snapshot:
}
```
`settings` is a single startup snapshot composed from the same response models as the eight `/api/settings/*` GET endpoints. Secret values are never included; only `*_configured` flags are exposed for stored credentials. The split settings endpoints remain the canonical resources for independent reads and updates.
### `GET /api/system/info`
Returns the `system` diagnostic object independently of the full bootstrap. Useful for monitoring and **Settings → System status**.
@@ -1472,6 +1511,8 @@ In `0.13.5`, the same disabled-zone behavior is explained directly beside the zo
In `0.13.6`, notification settings add `alert_types.sensor_discrepancy`, allowing GREE vs Home Assistant temperature-difference notifications to be disabled independently from thermostat/group control errors. The sensor fallback behavior itself is unchanged.
In `0.13.7`, the bootstrap payload also contains all eight redacted settings views. The bundled Web UI applies the same bootstrap mapper for initial HTTP load and WebSocket resynchronization, so a bootstrap frame no longer triggers eight additional settings GET requests.
Additional engine/integration events may be introduced without changing the envelope.
`api.request` data:
+4 -4
View File
@@ -2,9 +2,9 @@
"openapi": "3.1.0",
"info": {
"title": "GREE Controller API",
"version": "0.13.6",
"version": "0.13.7",
"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.\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.\n\n### 0.13.3\nLogical thermostat targets are normalized to 0.1 C consistently, including Temporary Quick Thermostat and thermostat-managed automation/Flow actions. The bundled Web UI accepts comma or dot decimal input for the temporary thermostat target; physical GREE setpoint rounding is unchanged.\n\n### 0.13.4\nDisabled-zone thermostat controls remain visually active in the bundled Web UI because explicit temporary/manual thermostat and direct manual control remain available while zone automation is disabled. HTTP and WebSocket contracts are unchanged.\n\n### 0.13.5\nThe zone settings dialog now explains directly beside the enabled/disabled option that disabling a zone stops automation but does not block manual thermostat, temporary thermostat or direct manual control. HTTP and WebSocket contracts remain unchanged.\n\n### 0.13.6\nNotification settings add a dedicated sensor_discrepancy alert type for GREE vs Home Assistant room-temperature divergence. Disabling it suppresses only that notification; the GREE safety fallback remains active.",
"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.\n\n### 0.13.3\nLogical thermostat targets are normalized to 0.1 C consistently, including Temporary Quick Thermostat and thermostat-managed automation/Flow actions. The bundled Web UI accepts comma or dot decimal input for the temporary thermostat target; physical GREE setpoint rounding is unchanged.\n\n### 0.13.4\nDisabled-zone thermostat controls remain visually active in the bundled Web UI because explicit temporary/manual thermostat and direct manual control remain available while zone automation is disabled. HTTP and WebSocket contracts are unchanged.\n\n### 0.13.5\nThe zone settings dialog now explains directly beside the enabled/disabled option that disabling a zone stops automation but does not block manual thermostat, temporary thermostat or direct manual control. HTTP and WebSocket contracts remain unchanged.\n\n### 0.13.6\nNotification settings add a dedicated sensor_discrepancy alert type for GREE vs Home Assistant room-temperature divergence. Disabling it suppresses only that notification; the GREE safety fallback remains active.\n\n### 0.13.7\nThe bootstrap snapshot now embeds all eight redacted runtime settings views. The bundled Web UI consumes those settings directly for HTTP startup and WebSocket resynchronization, removing the previous settings-request fan-out while preserving the split settings endpoints for direct reads and writes.",
"license": {
"name": "MIT"
}
@@ -122,7 +122,7 @@
"System"
],
"summary": "Get application bootstrap",
"description": "Returns the initial Web UI entity snapshot: devices, zones, groups, schedules, automations, Flows, access-token metadata, house mode, outdoor temperature, the latest materialized control plan with its revision, and system diagnostics. Runtime settings are loaded from the functional `/api/settings/*` resources.",
"description": "Returns the initial Web UI snapshot: devices, zones, groups, schedules, automations, Flows, access-token metadata, all eight redacted runtime settings views, house mode, outdoor temperature, the latest materialized control plan with its revision, and system diagnostics. The split `/api/settings/*` resources remain available for independent reads and writes.",
"operationId": "bootstrap",
"security": [
{
@@ -4891,7 +4891,7 @@
},
"version": {
"type": "string",
"example": "0.13.6"
"example": "0.13.7"
},
"uptime_seconds": {
"type": "integer",
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.13.6",
"version": "0.13.7",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
+39 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Developer smoke/integration tests for GREE Controller API 0.13.6.
"""Developer smoke/integration tests for GREE Controller API 0.13.7.
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.6"
DEFAULT_EXPECTED_VERSION = "0.13.7"
SETTINGS_PATHS: dict[str, set[str]] = {
"/api/settings/application": {"simulator_enabled"},
@@ -131,6 +131,10 @@ EXPECTED_OPENAPI_PATHS = set(SETTINGS_PATHS) | {
"/api/integrations/home-assistant/snapshot",
}
BOOTSTRAP_SETTINGS_SECTIONS = {
"application", "gree", "history", "influxdb", "notifications", "night", "home_assistant", "debug"
}
class TestFailure(AssertionError):
pass
@@ -343,6 +347,36 @@ def test_openapi(client: ApiClient, expected_version: str) -> str:
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"},
"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")
@@ -519,7 +553,8 @@ 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.6 contract", lambda: test_openapi(client, args.expected_version))
runner.run("OpenAPI 0.13.7 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:
@@ -587,7 +622,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.6.",
description="Developer smoke/integration tests for GREE Controller API 0.13.7.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""Examples:
python3 scripts/api_dev_test.py
+18 -7
View File
@@ -27,7 +27,7 @@ class FakeWebSocket {
}
const app = {
devices: [], zones: [], groups: [], schedules: [], automations: [], flows: [],
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',
@@ -54,7 +54,6 @@ const context = {
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; },
@@ -78,6 +77,7 @@ 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/bootstrap.js'), 'utf8'), context, { filename: 'bootstrap.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);
@@ -85,7 +85,6 @@ 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; };
@@ -112,10 +111,8 @@ async function testBootstrapReloadQueue() {
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 };
return { devices: [], zones: [], groups: [], schedules: [], automations: [], flows: [], access_tokens: [], settings: {}, 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; },
@@ -126,8 +123,9 @@ async function testBootstrapReloadQueue() {
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 });
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);
@@ -138,6 +136,7 @@ async function testBootstrapReloadQueue() {
async function main() {
await testBootstrapReloadQueue();
const apiCallsBeforeBootstrap = apiCalls.length;
await send('bootstrap', {
devices: [{ id: 'd1', power: false }],
zones: [{ id: 'z1', demand: false }],
@@ -145,9 +144,21 @@ async function main() {
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);
+29
View File
@@ -48,6 +48,35 @@ grep -qi 'swagger-ui' "$TMP/swagger.html"
curl -fsS "http://127.0.0.1:$PORT/api/bootstrap" >"$TMP/bootstrap.json"
grep -q 'sim-salon' "$TMP/bootstrap.json"
python3 - "$TMP/bootstrap.json" "$PORT" <<'PYBOOTSTRAP'
import json, sys, urllib.request
bootstrap = json.load(open(sys.argv[1], encoding="utf-8"))
port = sys.argv[2]
settings = bootstrap.get("settings")
assert isinstance(settings, dict), "bootstrap.settings missing"
paths = {
"application": "/api/settings/application",
"gree": "/api/settings/gree",
"history": "/api/settings/history",
"influxdb": "/api/settings/influxdb",
"notifications": "/api/settings/notifications",
"night": "/api/settings/night",
"home_assistant": "/api/settings/home-assistant",
"debug": "/api/settings/debug",
}
assert set(paths) <= set(settings), f"bootstrap settings sections missing: {set(paths) - set(settings)}"
for section, path in paths.items():
with urllib.request.urlopen(f"http://127.0.0.1:{port}{path}") as response:
standalone = json.load(response)
assert settings[section] == standalone, f"bootstrap settings mismatch for {section}"
assert "password" not in settings["influxdb"]
assert "token" not in settings["influxdb"]
assert "token" not in settings["home_assistant"]
for secret in ("pushover_app_token", "pushover_user_key", "slack_webhook_url", "discord_webhook_url"):
assert secret not in settings["notifications"], f"secret field leaked: {secret}"
PYBOOTSTRAP
curl -fsS -X POST -H 'Content-Type: application/json' \
-d '{"power":true,"mode":"cool","target_temperature":22}' \
+3 -2
View File
@@ -4,12 +4,13 @@ use crate::{
home_assistant, influxdb,
models::{
ApiTokenInfo, ApplicationSettings, Automation, ClimateGroup, ConfigurationExport,
DebugSettings, Device, DeviceCommand, DevicePatch, DiscoveryRequest, GreeSettings,
DebugSettings, Device, DeviceCommand, DevicePatch, DiscoveryRequest, Flow, GreeSettings,
GroupControlPatch, HaReading, HistorySettings, HomeAssistantSettings,
HomeAssistantSettingsUpdate, HomeAssistantSettingsView, InfluxDbSettings,
InfluxDbSettingsUpdate, InfluxDbSettingsView, ManualDeviceRequest, NightModeSettings,
NotificationSettings, NotificationSettingsUpdate, NotificationSettingsView, Reading,
RuntimeSettings, Schedule, TemporaryQuickThermostat, TemporaryQuickThermostatRequest, Zone,
RuntimeSettings, Schedule, SettingsSnapshot, TemporaryQuickThermostat,
TemporaryQuickThermostatRequest, Zone,
ZoneControlPatch, ZoneReading,
},
notifications,
+13
View File
@@ -70,6 +70,19 @@ fn home_assistant_settings(settings: &RuntimeSettings) -> HomeAssistantSettingsV
}
}
fn settings_snapshot(settings: &RuntimeSettings) -> SettingsSnapshot {
SettingsSnapshot {
application: application_settings(settings),
gree: gree_settings(settings),
history: history_settings(settings),
influxdb: influxdb_settings(settings),
notifications: notification_settings(settings),
night: settings.night_mode.clone(),
home_assistant: home_assistant_settings(settings),
debug: settings.debug.clone(),
}
}
async fn get_application_settings(State(state): State<AppState>) -> Json<ApplicationSettings> {
Json(application_settings(&*state.settings.read().await))
}
+93 -52
View File
@@ -1,3 +1,42 @@
#[derive(Debug, serde::Serialize)]
struct HouseSnapshot {
mode: String,
}
#[derive(Debug, serde::Serialize)]
struct SystemInfoResponse {
version: &'static str,
uptime_seconds: u64,
auth_required: bool,
control_ready: bool,
database: String,
device_count: usize,
online_count: usize,
simulator_count: usize,
bind: String,
base_path: String,
gree_interface: String,
gree_received_frames: u64,
gree_received_frames_by_device: std::collections::HashMap<String, u64>,
}
#[derive(Debug, serde::Serialize)]
struct BootstrapResponse {
devices: Vec<Device>,
zones: Vec<Zone>,
groups: Vec<ClimateGroup>,
schedules: Vec<Schedule>,
automations: Vec<Automation>,
flows: Vec<Flow>,
access_tokens: Vec<ApiTokenInfo>,
settings: SettingsSnapshot,
house: HouseSnapshot,
outdoor_temperature: Option<f64>,
control_plan: crate::models::ControlPlan,
control_plan_revision: u64,
system: SystemInfoResponse,
}
async fn health(State(state): State<AppState>) -> Json<Value> {
Json(json!({
"status": "ok",
@@ -9,64 +48,66 @@ async fn health(State(state): State<AppState>) -> Json<Value> {
}))
}
async fn bootstrap(State(state): State<AppState>) -> Result<Json<Value>, AppError> {
async fn bootstrap(State(state): State<AppState>) -> Result<Json<BootstrapResponse>, AppError> {
Ok(Json(build_bootstrap(&state).await?))
}
async fn build_bootstrap(state: &AppState) -> Result<Value, AppError> {
let settings = state.settings.read().await.clone();
async fn build_bootstrap(state: &AppState) -> Result<BootstrapResponse, AppError> {
let (settings, house_mode) = {
let settings = state.settings.read().await;
(settings_snapshot(&settings), settings.house_mode.clone())
};
let devices = state.db.list_devices()?;
let device_count = devices.len();
let online_count = devices.iter().filter(|value| value.online).count();
let simulator_count = devices.iter().filter(|value| value.simulated).count();
let (received_frames_total, received_frames_by_device) = state.gree.received_frame_stats();
let system = build_system_info(state, &devices);
let control_plan = engine::get_control_plan_snapshot(state).await?;
Ok(json!({
"devices": devices,
"zones": state.db.list_zones()?,
"groups": state.db.list_groups()?,
"schedules": state.db.list_schedules()?,
"automations": state.db.list_automations()?,
"flows": state.db.list_flows()?,
"access_tokens": state.db.list_api_tokens()?,
"house": {"mode": settings.house_mode},
"outdoor_temperature": *state.outdoor_temperature.read().await,
"control_plan": control_plan.plan.as_ref(),
"control_plan_revision": control_plan.revision,
"system": {
"version": env!("CARGO_PKG_VERSION"),
"uptime_seconds": state.started.elapsed().as_secs(),
"auth_required": !state.config.app_token.trim().is_empty(),
"control_ready": state.initial_device_sync_complete.load(Ordering::Acquire),
"database": state.config.database.display().to_string(),
"device_count": device_count,
"online_count": online_count,
"simulator_count": simulator_count,
"bind": state.config.bind.to_string(),
"base_path": if state.config.base_path.is_empty() { "/" } else { state.config.base_path.as_str() },
"gree_interface": if state.config.gree_interface.trim().is_empty() { "auto" } else { state.config.gree_interface.trim() },
"gree_received_frames": received_frames_total,
"gree_received_frames_by_device": received_frames_by_device,
}
}))
Ok(BootstrapResponse {
devices,
zones: state.db.list_zones()?,
groups: state.db.list_groups()?,
schedules: state.db.list_schedules()?,
automations: state.db.list_automations()?,
flows: state.db.list_flows()?,
access_tokens: state.db.list_api_tokens()?,
settings,
house: HouseSnapshot { mode: house_mode },
outdoor_temperature: *state.outdoor_temperature.read().await,
control_plan: control_plan.plan.as_ref().clone(),
control_plan_revision: control_plan.revision,
system,
})
}
async fn system_info(State(state): State<AppState>) -> Result<Json<Value>, AppError> {
let devices = state.db.list_devices()?;
fn build_system_info(state: &AppState, devices: &[Device]) -> SystemInfoResponse {
let (received_frames_total, received_frames_by_device) = state.gree.received_frame_stats();
Ok(Json(json!({
"version": env!("CARGO_PKG_VERSION"),
"uptime_seconds": state.started.elapsed().as_secs(),
"database": state.config.database.display().to_string(),
"device_count": devices.len(),
"online_count": devices.iter().filter(|v| v.online).count(),
"simulator_count": devices.iter().filter(|v| v.simulated).count(),
"control_ready": state.initial_device_sync_complete.load(Ordering::Acquire),
"auth_required": !state.config.app_token.trim().is_empty(),
"bind": state.config.bind.to_string(),
"base_path": if state.config.base_path.is_empty() { "/" } else { state.config.base_path.as_str() },
"gree_interface": if state.config.gree_interface.trim().is_empty() { "auto" } else { state.config.gree_interface.trim() },
"gree_received_frames": received_frames_total,
"gree_received_frames_by_device": received_frames_by_device,
})))
SystemInfoResponse {
version: env!("CARGO_PKG_VERSION"),
uptime_seconds: state.started.elapsed().as_secs(),
auth_required: !state.config.app_token.trim().is_empty(),
control_ready: state.initial_device_sync_complete.load(Ordering::Acquire),
database: state.config.database.display().to_string(),
device_count: devices.len(),
online_count: devices.iter().filter(|device| device.online).count(),
simulator_count: devices.iter().filter(|device| device.simulated).count(),
bind: state.config.bind.to_string(),
base_path: if state.config.base_path.is_empty() {
"/".to_string()
} else {
state.config.base_path.clone()
},
gree_interface: if state.config.gree_interface.trim().is_empty() {
"auto".to_string()
} else {
state.config.gree_interface.trim().to_string()
},
gree_received_frames: received_frames_total,
gree_received_frames_by_device: received_frames_by_device,
}
}
async fn system_info(
State(state): State<AppState>,
) -> Result<Json<SystemInfoResponse>, AppError> {
let devices = state.db.list_devices()?;
Ok(Json(build_system_info(&state, &devices)))
}
+1 -1
View File
@@ -28,7 +28,7 @@ fn control_plan_ws_message(snapshot: &crate::state::ControlPlanSnapshot) -> Valu
async fn send_ws_bootstrap(state: &AppState, socket: &mut WebSocket) -> Result<Option<u64>, ()> {
let (message, revision) = match build_bootstrap(state).await {
Ok(data) => {
let revision = data.get("control_plan_revision").and_then(Value::as_u64);
let revision = Some(data.control_plan_revision);
(
json!({"event":"bootstrap","timestamp":Utc::now(),"data":data}),
revision,
+13
View File
@@ -111,3 +111,16 @@ pub struct HomeAssistantSettingsView {
pub flow_inputs: Vec<FlowSharedInput>,
pub outdoor_assist_enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsSnapshot {
pub application: ApplicationSettings,
pub gree: GreeSettings,
pub history: HistorySettings,
pub influxdb: InfluxDbSettingsView,
pub notifications: NotificationSettingsView,
pub night: NightModeSettings,
pub home_assistant: HomeAssistantSettingsView,
pub debug: DebugSettings,
}
+52 -11
View File
@@ -1,8 +1,36 @@
async function loadBootstrap(showMessage = false) {
if (app.loading) { app.bootstrapReloadPending = true; return; }
app.loading = true;
try {
const [data, sections] = await Promise.all([api('/api/bootstrap'), fetchSettingsSections()]);
function settingsFromSnapshot(sections, houseMode = app.settings?.house_mode || 'cool') {
const application = sections?.application || {};
const gree = sections?.gree || {};
const history = sections?.history || {};
const influxdb = sections?.influxdb || {};
const notifications = sections?.notifications || {};
const night = sections?.night || {};
const homeAssistant = sections?.home_assistant || {};
const debug = sections?.debug || {};
return {
simulator_enabled: !!application.simulator_enabled,
controller_id: gree.controller_id,
poll_interval_seconds: Number(gree.poll_interval_seconds),
zone_interval_seconds: Number(gree.zone_interval_seconds),
discovery_timeout_ms: Number(gree.discovery_timeout_ms),
discovery_broadcast: gree.discovery_broadcast,
suppress_device_beep: !!gree.suppress_device_beep,
compressor_protection_enabled: gree.compressor_protection_enabled !== false,
compressor_protection_seconds: Number(gree.compressor_protection_seconds),
history_retention_days: Number(history.retention_days),
history_compaction_enabled: history.compaction_enabled !== false,
event_log_retention_days: Number(history.event_retention_days),
influxdb,
notifications,
night_mode: night,
home_assistant: homeAssistant,
outdoor_assist_enabled: !!homeAssistant.outdoor_assist_enabled,
debug,
house_mode: houseMode || 'cool',
};
}
function applyBootstrapSnapshot(data) {
app.devices = data.devices || [];
app.zones = data.zones || [];
app.groups = data.groups || [];
@@ -10,16 +38,30 @@ async function loadBootstrap(showMessage = false) {
app.automations = data.automations || [];
app.flows = data.flows || [];
app.accessTokens = data.access_tokens || [];
app.settings = aggregateSettingsSections(sections, data.house?.mode || 'cool');
app.settings = settingsFromSnapshot(data.settings, data.house?.mode || 'cool');
app.sensorAliases = { ...(app.settings.home_assistant?.sensor_aliases || {}) };
app.flowSharedInputs = JSON.parse(JSON.stringify(app.settings.home_assistant?.flow_inputs || []));
app.system = data.system || {};
app.systemSnapshotAt = Date.now();
app.outdoorTemperature = Number.isFinite(Number(data.outdoor_temperature)) ? Number(data.outdoor_temperature) : null;
app.controlPlan = data.control_plan || app.controlPlan;
app.controlPlanRevision = Number.isFinite(Number(data.control_plan_revision)) ? Number(data.control_plan_revision) : app.controlPlanRevision;
const outdoorTemperature = data.outdoor_temperature;
app.outdoorTemperature = outdoorTemperature === null || outdoorTemperature === undefined || outdoorTemperature === ''
? null
: (Number.isFinite(Number(outdoorTemperature)) ? Number(outdoorTemperature) : null);
const hasControlPlan = !!data.control_plan;
if (hasControlPlan) app.controlPlan = data.control_plan;
if (Number.isFinite(Number(data.control_plan_revision))) app.controlPlanRevision = Number(data.control_plan_revision);
return hasControlPlan;
}
async function loadBootstrap(showMessage = false) {
if (app.loading) { app.bootstrapReloadPending = true; return; }
app.loading = true;
try {
const data = await api('/api/bootstrap');
const hasControlPlan = applyBootstrapSnapshot(data);
renderAll();
if (!data.control_plan) scheduleControlPlanLoad(0);
if (!hasControlPlan) scheduleControlPlanLoad(0);
if (app.settings?.debug?.overlay_enabled) loadDebugBacklog();
if (showMessage) toast(tr('common.updated'));
if ($('#tokenDialog').open) $('#tokenDialog').close();
@@ -34,4 +76,3 @@ async function loadBootstrap(showMessage = false) {
}
}
}
+8 -11
View File
@@ -26,17 +26,14 @@ async function handleWebSocketMessage(event) {
try {
const message = JSON.parse(event.data);
if (message.event === 'bootstrap') {
const data = message.data; app.devices = data.devices || []; app.zones = data.zones || []; app.groups = data.groups || []; app.schedules = data.schedules || []; app.automations = data.automations || [];
app.flows = data.flows || []; await loadSettingsSections(data.house?.mode || app.settings?.house_mode || 'cool'); app.system = data.system || app.system; app.systemSnapshotAt = Date.now(); app.outdoorTemperature = Number.isFinite(Number(data.outdoor_temperature)) ? Number(data.outdoor_temperature) : app.outdoorTemperature;
if (data.control_plan) {
app.controlPlan = data.control_plan;
app.controlPlanRevision = Number.isFinite(Number(data.control_plan_revision)) ? Number(data.control_plan_revision) : app.controlPlanRevision;
app.controlPlanPushReady = true;
stopControlPlanFallback();
} else {
app.controlPlanPushReady = false;
}
renderAll(); if (!data.control_plan) scheduleControlPlanLoad(0); if (app.settings?.debug?.overlay_enabled) loadDebugBacklog(); return;
const data = message.data || {};
const hasControlPlan = applyBootstrapSnapshot(data);
app.controlPlanPushReady = hasControlPlan;
if (hasControlPlan) stopControlPlanFallback();
renderAll();
if (!hasControlPlan) scheduleControlPlanLoad(0);
if (app.settings?.debug?.overlay_enabled) loadDebugBacklog();
return;
}
const data = message.data || {};
if (message.event === 'control_plan.updated') { app.controlPlanPushReady = true; stopControlPlanFallback(); applyControlPlan(data.plan, data.revision); }
-46
View File
@@ -9,52 +9,6 @@ const SETTINGS_ENDPOINTS = {
debug: '/api/settings/debug',
};
async function fetchSettingsSections() {
const [application, gree, history, influxdb, notifications, night, homeAssistant, debug] = await Promise.all([
api(SETTINGS_ENDPOINTS.application),
api(SETTINGS_ENDPOINTS.gree),
api(SETTINGS_ENDPOINTS.history),
api(SETTINGS_ENDPOINTS.influxdb),
api(SETTINGS_ENDPOINTS.notifications),
api(SETTINGS_ENDPOINTS.night),
api(SETTINGS_ENDPOINTS.homeAssistant),
api(SETTINGS_ENDPOINTS.debug),
]);
return { application, gree, history, influxdb, notifications, night, homeAssistant, debug };
}
function aggregateSettingsSections(sections, houseMode = app.settings?.house_mode || 'cool') {
return {
simulator_enabled: !!sections.application.simulator_enabled,
controller_id: sections.gree.controller_id,
poll_interval_seconds: Number(sections.gree.poll_interval_seconds),
zone_interval_seconds: Number(sections.gree.zone_interval_seconds),
discovery_timeout_ms: Number(sections.gree.discovery_timeout_ms),
discovery_broadcast: sections.gree.discovery_broadcast,
suppress_device_beep: !!sections.gree.suppress_device_beep,
compressor_protection_enabled: sections.gree.compressor_protection_enabled !== false,
compressor_protection_seconds: Number(sections.gree.compressor_protection_seconds),
history_retention_days: Number(sections.history.retention_days),
history_compaction_enabled: sections.history.compaction_enabled !== false,
event_log_retention_days: Number(sections.history.event_retention_days),
influxdb: sections.influxdb,
notifications: sections.notifications,
night_mode: sections.night,
home_assistant: sections.homeAssistant,
outdoor_assist_enabled: !!sections.homeAssistant.outdoor_assist_enabled,
debug: sections.debug,
house_mode: houseMode || 'cool',
};
}
async function loadSettingsSections(houseMode = app.settings?.house_mode || 'cool') {
const sections = await fetchSettingsSections();
app.settings = aggregateSettingsSections(sections, houseMode);
app.sensorAliases = { ...(app.settings.home_assistant?.sensor_aliases || {}) };
app.flowSharedInputs = JSON.parse(JSON.stringify(app.settings.home_assistant?.flow_inputs || []));
return app.settings;
}
function applySettingsSection(section, data) {
app.settings = app.settings || {};
if (section === 'application') app.settings.simulator_enabled = !!data.simulator_enabled;