v0.13.7
This commit is contained in:
Generated
+1
-1
@@ -700,7 +700,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.13.6"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
Vendored
+52
-11
@@ -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
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user