v0.13.8
This commit is contained in:
Generated
+1
-1
@@ -700,7 +700,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.13.7"
|
||||
version = "0.13.8"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.13.7"
|
||||
version = "0.13.8"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+16
-15
@@ -1,13 +1,13 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
d4330d1b80dd2bf948a5157c97581c2ca10dc69b7d6740fd55c48a699c2e3bc6 ./Cargo.lock
|
||||
71d0ce719983e6d41bd89b592a6eaf66658479c0c30424b88d1133d80936d4aa ./Cargo.toml
|
||||
ff8ef8ed21e7d3617f9939cd9e809bff06744ffc99202e037b2f31ade4157867 ./Cargo.lock
|
||||
fb8bbd5479705582e3aad71568ada6892bc266a56be6ec3d69647a4b8dd30b69 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
a7acf66c0bbae0c6ca3faa2993b4b8ba80fec935902d0102f34693ffc96b7a19 ./README.md
|
||||
d56cfff4179a6511aa764a91c6ddebe21fcd35f36b7298e62d012e11f62bfeb9 ./build.rs
|
||||
6d42d846df9e6115acedd9ac0ba84d05689046dc3f71fb2311c074638488d8f6 ./docs/API.md
|
||||
cd7b1af125c0d65b52042a844bfe9e9575d1c5bde31ebddfc2534231a7599fbb ./README.md
|
||||
bb549919b96b0413152dd905aec8640bb53301f07ec7d0b1be48ccbd37936fef ./build.rs
|
||||
eff2912449a4b78e0b9eb4ff02fa6752e31abcb71fa097a46f1d7596a4500303 ./docs/API.md
|
||||
2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md
|
||||
6da82ee5007330b210a1645aed7df7951f8c858e04009631e89de51f32815ecf ./docs/openapi.json
|
||||
8f3ca55ab14f855644b3c85074400bf4a4a6b37659f4aa5dd686757a0d2bcd56 ./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
|
||||
542d58cc7f47b55c786619c831f7bcdecaa3699940f29d9a7f26f01aaa0f0547 ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
9b31873cdc44b589a4230346e6cd9ee25325b431ccf06b39f07393793410bf65 ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./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,7 +69,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
|
||||
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
|
||||
b1e6a729a28eec03b38f013377d26b95669b58c035802c96ea9201cb633d87b2 ./scripts/api_dev_test.py
|
||||
615b8c7c1c085f3dba2ddc10184b2223b2106c94f7a69ccc0592bf05b2323f27 ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
@@ -82,8 +82,8 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
472db31959be29ba99247f7e9cc07a44bc090c538cd654ad8d16e094726037a3 ./scripts/smoke.sh
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
6b6cab48685c56893fbe31174460254347d73a354598d0bb1f22b195df91da3e ./src/api.rs
|
||||
5027797b9cefb9f0aa04c792e2ac9a38745c6bf1ee822f50eb70574fa6f997e2 ./src/api/assets.rs
|
||||
0da4e4fcb8e77109fd56b301472d08acbab5ad9ecfe861296153a6a5eef723b3 ./src/api.rs
|
||||
549941b91d9f9bb829f9f64345866a1a76eebcb323d15806d9d11545566dfe6b ./src/api/assets.rs
|
||||
05c255919f7b3d81188fc5f1410be6a0037471dfc44e93bffb009f29a1e6a3fc ./src/api/auth.rs
|
||||
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
|
||||
368cd843dcac970f1aa6800ba132a4bbc3429e8379ed7abf9878dcb0c633af40 ./src/api/configuration.rs
|
||||
@@ -176,24 +176,25 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
9e98df7a759bd76bc7682b23c3c4b7e29f7bde972f03ae316ba7f8159301e2c6 ./web/index.html
|
||||
2808c2152a3dc56d80c158980f2a00d73e078b16731bdff240a62431ca95dea9 ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
||||
f272912f6e5d8b34786827d78ba592513e3ca1e85e07e4c073089c5bbb75f282 ./web/js/bootstrap.js
|
||||
badde8a5214a0322b2c45803c02a1cb723d53b25e57c687369cd804f509d9744 ./web/js/charts.js
|
||||
dd596b3362643b96be8537b9415e8c1be90d7cf458124b61121016a6742951c7 ./web/js/core.js
|
||||
ff427103d12cb99977cf1b32e6978506b6ee5179a10568b13673292c13ae3b65 ./web/js/core.js
|
||||
d880ffdac6aa07cd10d79fbacf12f87eb4c69b48a80564bbfdd0248abe8884d5 ./web/js/dashboard.js
|
||||
6c0d310e39a1e1f618a31fbdf5560a850023bf2219d897c69d636d0179dff004 ./web/js/entities.js
|
||||
0a23ac65a4a0bd113a9f78fe1d25621d6a178e10aaca53e5df8df87587998cb4 ./web/js/events.js
|
||||
2e822a00b5a6941b6a251eaf2c54ca95a53399b5d1056f3d8240e11f82d6065f ./web/js/flows.js
|
||||
cf1ebf3b9d666f309a1add070b4da86be86d990b02c8d6474f6d121c6d332746 ./web/js/forms.js
|
||||
dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js
|
||||
7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js
|
||||
e934dbe7810da4565a4474338955b044448f3a268160e55bf98afefa4b2d97de ./web/js/main.js
|
||||
733fcc595ed9c03c9639acc6caba46f0588f29e0b95bf72891e34443e7aa9db8 ./web/js/navigation.js
|
||||
e0a6b5d2900f5524dba395b97288f8d75976e31b27e26810d42febbfd723085c ./web/js/realtime.js
|
||||
c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/router.js
|
||||
25f5c9a0aa8622f419b05f64712c9eca4896f0e79818616904b83051fd77b07b ./web/js/settings-ui.js
|
||||
4aadfdf107593d32ab73514688f225bf559fd5ce6bbf9d9459a8098daf1fbe3d ./web/js/settings.js
|
||||
3815fea7ae1694568c2f249bc8460e0563ced6bde89214f74f21b41fe8cad9a6 ./web/lang-init.js
|
||||
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
|
||||
0b9774bb388158e23f8c90297d70659a015e0db58d25eadcae316a49e3c9fcb7 ./web/styles.css
|
||||
ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js
|
||||
8ea4f5cf1ebdf3894e17457020bde6d5fae324eadcc0ff35a6ac4efe5d5b2b6a ./web/styles.css
|
||||
fefdf12cbbb0a5d8a2cb89275cae28455aa42aaf179542305cc8f5e0f4825898 ./web/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
|
||||
|
||||
**Current release: 0.13.7**
|
||||
**Current release: 0.13.8**
|
||||
|
||||
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.8 removes the visible English-to-selected-language flash during Web UI startup. A dedicated `lang-init.js` applies the preferred language before first paint, the application is revealed only after localization initialization, and content-versioned language packs can use the browser cache safely.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ fn main() {
|
||||
println!("cargo:rerun-if-changed={}", preset_dir.display());
|
||||
|
||||
let theme_init_path = web_dir.join("theme-init.js");
|
||||
let lang_init_path = web_dir.join("lang-init.js");
|
||||
let styles_path = web_dir.join("styles.css");
|
||||
let index_path = web_dir.join("index.html");
|
||||
let not_found_path = web_dir.join("404.html");
|
||||
@@ -86,6 +87,7 @@ fn main() {
|
||||
let favicon_path = web_dir.join("favicon.svg");
|
||||
for path in [
|
||||
&theme_init_path,
|
||||
&lang_init_path,
|
||||
&styles_path,
|
||||
&index_path,
|
||||
¬_found_path,
|
||||
@@ -100,17 +102,22 @@ fn main() {
|
||||
let app_js_hash = content_hash(app_js.as_bytes());
|
||||
let theme_init_bytes = fs::read(&theme_init_path)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", theme_init_path.display()));
|
||||
let lang_init_bytes = fs::read(&lang_init_path)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", lang_init_path.display()));
|
||||
let styles_bytes = fs::read(&styles_path)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", styles_path.display()));
|
||||
let theme_init_hash = content_hash(&theme_init_bytes);
|
||||
let lang_init_hash = content_hash(&lang_init_bytes);
|
||||
let styles_hash = content_hash(&styles_bytes);
|
||||
let app_js_asset_path = format!("/app-{}.js", &app_js_hash[..12]);
|
||||
let theme_init_asset_path = format!("/theme-init-{}.js", &theme_init_hash[..12]);
|
||||
let lang_init_asset_path = format!("/lang-init-{}.js", &lang_init_hash[..12]);
|
||||
let styles_asset_path = format!("/styles-{}.css", &styles_hash[..12]);
|
||||
|
||||
let mut build_hash = fnv1a_update(0xcbf29ce484222325u64, app_js.as_bytes());
|
||||
for path in [
|
||||
&theme_init_path,
|
||||
&lang_init_path,
|
||||
&styles_path,
|
||||
&index_path,
|
||||
¬_found_path,
|
||||
@@ -189,12 +196,13 @@ fn main() {
|
||||
has_english = true;
|
||||
}
|
||||
|
||||
let language_hash = content_hash(source.as_bytes());
|
||||
manifest_languages.push(json!({
|
||||
"code": code,
|
||||
"name": name,
|
||||
"native_name": native_name,
|
||||
"locale": locale,
|
||||
"path": format!("lang/{code}.json")
|
||||
"path": format!("lang/{code}.json?v={}", &language_hash[..12])
|
||||
}));
|
||||
assets.push((code.to_owned(), path));
|
||||
}
|
||||
@@ -298,6 +306,10 @@ fn main() {
|
||||
"pub const THEME_INIT_ASSET_PATH: &str = {:?};\n",
|
||||
theme_init_asset_path
|
||||
));
|
||||
generated.push_str(&format!(
|
||||
"pub const LANG_INIT_ASSET_PATH: &str = {:?};\n",
|
||||
lang_init_asset_path
|
||||
));
|
||||
generated.push_str(&format!(
|
||||
"pub const STYLES_CSS_ASSET_PATH: &str = {:?};\n",
|
||||
styles_asset_path
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
# GREE Controller API reference
|
||||
|
||||
HTTP and WebSocket API for GREE Controller **0.13.7**.
|
||||
HTTP and WebSocket API for GREE Controller **0.13.8**.
|
||||
|
||||
[← Main documentation](../README.md)
|
||||
|
||||
@@ -48,6 +48,7 @@ GET /
|
||||
GET /index.html
|
||||
GET /app.js
|
||||
GET /theme-init.js
|
||||
GET /lang-init.js
|
||||
GET /styles.css
|
||||
GET /manifest.webmanifest
|
||||
GET /sw.js
|
||||
@@ -231,7 +232,7 @@ Response:
|
||||
{
|
||||
"status": "ok",
|
||||
"name": "gree-controller",
|
||||
"version": "0.13.7",
|
||||
"version": "0.13.8",
|
||||
"uptime_seconds": 1234,
|
||||
"control_ready": true,
|
||||
"time": "2026-08-30T06:54:00Z"
|
||||
@@ -294,7 +295,7 @@ Returns the initial Web UI snapshot:
|
||||
"control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []},
|
||||
"control_plan_revision": 42,
|
||||
"system": {
|
||||
"version": "0.13.7",
|
||||
"version": "0.13.8",
|
||||
"uptime_seconds": 1234,
|
||||
"auth_required": false,
|
||||
"control_ready": true,
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "GREE Controller API",
|
||||
"version": "0.13.7",
|
||||
"version": "0.13.8",
|
||||
"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.\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.",
|
||||
"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.\n\n### 0.13.8\nThe bundled Web UI suppresses the initial English-to-selected-language flash with a dedicated pre-paint language initializer and reveals the application only after localization is initialized. Language-pack URLs are content-versioned so immutable browser caching stays safe across updates.",
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
}
|
||||
@@ -4891,7 +4891,7 @@
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "0.13.7"
|
||||
"example": "0.13.8"
|
||||
},
|
||||
"uptime_seconds": {
|
||||
"type": "integer",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.13.7",
|
||||
"version": "0.13.8",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.13.7.
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.13.8.
|
||||
|
||||
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.7"
|
||||
DEFAULT_EXPECTED_VERSION = "0.13.8"
|
||||
|
||||
SETTINGS_PATHS: dict[str, set[str]] = {
|
||||
"/api/settings/application": {"simulator_enabled"},
|
||||
@@ -553,7 +553,7 @@ def run_suite(args: argparse.Namespace) -> int:
|
||||
print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n")
|
||||
|
||||
runner.run("public health", lambda: test_health(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.13.7 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.13.8 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))
|
||||
|
||||
@@ -622,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.7.",
|
||||
description="Developer smoke/integration tests for GREE Controller API 0.13.8.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""Examples:
|
||||
python3 scripts/api_dev_test.py
|
||||
|
||||
@@ -50,6 +50,7 @@ const INDEX_HTML: &str = include_str!("../web/index.html");
|
||||
const NOT_FOUND_HTML: &str = include_str!("../web/404.html");
|
||||
const APP_JS: &str = include_str!(concat!(env!("OUT_DIR"), "/app.bundle.js"));
|
||||
const THEME_INIT_JS: &str = include_str!("../web/theme-init.js");
|
||||
const LANG_INIT_JS: &str = include_str!("../web/lang-init.js");
|
||||
const STYLES_CSS: &str = include_str!("../web/styles.css");
|
||||
const MANIFEST: &str = include_str!("../web/manifest.webmanifest");
|
||||
const SERVICE_WORKER: &str = include_str!("../web/sw.js");
|
||||
@@ -257,6 +258,7 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/index.html", get(index))
|
||||
.route(APP_JS_ASSET_PATH, get(app_js))
|
||||
.route(THEME_INIT_ASSET_PATH, get(theme_init_js))
|
||||
.route(LANG_INIT_ASSET_PATH, get(lang_init_js))
|
||||
.route(STYLES_CSS_ASSET_PATH, get(styles_css))
|
||||
.route("/manifest.webmanifest", get(manifest))
|
||||
.route("/sw.js", get(service_worker))
|
||||
|
||||
+13
-1
@@ -38,6 +38,10 @@ async fn index(State(state): State<AppState>, headers: HeaderMap) -> Response {
|
||||
"__GREE_THEME_INIT_ASSET__",
|
||||
&format!("{base}{THEME_INIT_ASSET_PATH}"),
|
||||
)
|
||||
.replace(
|
||||
"__GREE_LANG_INIT_ASSET__",
|
||||
&format!("{base}{LANG_INIT_ASSET_PATH}"),
|
||||
)
|
||||
.replace(
|
||||
"__GREE_STYLES_ASSET__",
|
||||
&format!("{base}{STYLES_CSS_ASSET_PATH}"),
|
||||
@@ -95,6 +99,13 @@ async fn theme_init_js() -> Response {
|
||||
"public, max-age=31536000, immutable",
|
||||
)
|
||||
}
|
||||
async fn lang_init_js() -> Response {
|
||||
static_response(
|
||||
LANG_INIT_JS,
|
||||
"application/javascript; charset=utf-8",
|
||||
"public, max-age=31536000, immutable",
|
||||
)
|
||||
}
|
||||
async fn styles_css() -> Response {
|
||||
static_response(
|
||||
STYLES_CSS,
|
||||
@@ -114,6 +125,7 @@ async fn service_worker() -> Response {
|
||||
.replace("__GREE_ASSET_CACHE__", ASSET_BUILD_ID)
|
||||
.replace("__GREE_APP_ASSET__", APP_JS_ASSET_PATH)
|
||||
.replace("__GREE_THEME_INIT_ASSET__", THEME_INIT_ASSET_PATH)
|
||||
.replace("__GREE_LANG_INIT_ASSET__", LANG_INIT_ASSET_PATH)
|
||||
.replace("__GREE_STYLES_ASSET__", STYLES_CSS_ASSET_PATH);
|
||||
owned_response(body, "application/javascript; charset=utf-8", "no-cache")
|
||||
}
|
||||
@@ -133,7 +145,7 @@ async fn language_file(Path(file): Path<String>) -> Response {
|
||||
.iter()
|
||||
.find(|(language, _)| *language == code)
|
||||
{
|
||||
return static_response(*body, "application/json; charset=utf-8", "no-cache");
|
||||
return static_response(*body, "application/json; charset=utf-8", "public, max-age=31536000, immutable");
|
||||
}
|
||||
let mut response = Response::new(Body::from("Language not found"));
|
||||
*response.status_mut() = StatusCode::NOT_FOUND;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<link rel="icon" href="__GREE_BASE_PATH__/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="__GREE_STYLES_ASSET__">
|
||||
<script src="__GREE_THEME_INIT_ASSET__"></script>
|
||||
<script src="__GREE_LANG_INIT_ASSET__"></script>
|
||||
<title>GREE Controller</title>
|
||||
</head>
|
||||
|
||||
|
||||
+2
-7
@@ -265,19 +265,14 @@ function renderLanguageOptions() {
|
||||
|
||||
async function loadLanguages() {
|
||||
try {
|
||||
const languageNonce = Date.now().toString(36);
|
||||
const languageUrl = path => {
|
||||
const separator = String(path).includes('?') ? '&' : '?';
|
||||
return `${withBase(path)}${separator}v=${languageNonce}`;
|
||||
};
|
||||
const response = await fetch(languageUrl('/lang/index.json'), { cache: 'no-store' });
|
||||
const response = await fetch(withBase('/lang/index.json'));
|
||||
if (!response.ok) throw new Error(`Language index HTTP ${response.status}`);
|
||||
const manifest = await response.json();
|
||||
const languages = Array.isArray(manifest.languages) ? manifest.languages : [];
|
||||
if (!languages.some(item => item.code === DEFAULT_LANGUAGE)) throw new Error('Default English language pack is missing');
|
||||
|
||||
const loaded = await Promise.all(languages.map(async item => {
|
||||
const packResponse = await fetch(languageUrl(item.path || `/lang/${encodeURIComponent(item.code)}.json`), { cache: 'no-store' });
|
||||
const packResponse = await fetch(withBase(item.path || `/lang/${encodeURIComponent(item.code)}.json`));
|
||||
if (!packResponse.ok) throw new Error(`Language ${item.code} HTTP ${packResponse.status}`);
|
||||
const pack = await packResponse.json();
|
||||
return { item, pack };
|
||||
|
||||
+8
-4
@@ -4,10 +4,14 @@ window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', ()
|
||||
if ('serviceWorker' in navigator) window.addEventListener('load', () => navigator.serviceWorker.register(withBase('/sw.js')).catch(() => { }));
|
||||
|
||||
async function startApplication() {
|
||||
observeTopbarHeight();
|
||||
applyTheme();
|
||||
await loadLanguages();
|
||||
applyTranslations();
|
||||
try {
|
||||
observeTopbarHeight();
|
||||
applyTheme();
|
||||
await loadLanguages();
|
||||
applyTranslations();
|
||||
} finally {
|
||||
document.documentElement.classList.remove('i18n-loading');
|
||||
}
|
||||
setupFormUx();
|
||||
updateZoneSensorFields();
|
||||
updateSchedulePresetField();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
(() => {
|
||||
const row = document.cookie.split('; ').find(item => item.startsWith('gree_controller_language='));
|
||||
const language = row ? decodeURIComponent(row.split('=').slice(1).join('=')) : 'en';
|
||||
document.documentElement.lang = language || 'en';
|
||||
document.documentElement.classList.add('i18n-loading');
|
||||
})();
|
||||
@@ -95,6 +95,10 @@ body {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
html.i18n-loading body {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const CACHE = 'gree-controller-__GREE_ASSET_CACHE__';
|
||||
const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
|
||||
const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/';
|
||||
const ASSETS = [path('__GREE_STYLES_ASSET__'), path('__GREE_APP_ASSET__'), path('__GREE_THEME_INIT_ASSET__'), path('/favicon.svg'), path('/manifest.webmanifest')];
|
||||
const ASSETS = [path('__GREE_STYLES_ASSET__'), path('__GREE_APP_ASSET__'), path('__GREE_THEME_INIT_ASSET__'), path('__GREE_LANG_INIT_ASSET__'), path('/favicon.svg'), path('/manifest.webmanifest')];
|
||||
self.addEventListener('install', event => event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(ASSETS)).then(() => self.skipWaiting())));
|
||||
self.addEventListener('activate', event => event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))).then(() => self.clients.claim())));
|
||||
self.addEventListener('fetch', event => {
|
||||
|
||||
Reference in New Issue
Block a user