diff --git a/Cargo.lock b/Cargo.lock index 803563c..4c820c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.14.23" +version = "0.14.24" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index d6fa9b8..e079e24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.14.23" +version = "0.14.24" edition = "2021" authors = ["GREE Controller contributors"] description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support" diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index 0831f70..8799a25 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,14 +1,14 @@ de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -9016ecffc9cc145fe5c2cee1e097b170de5c71a6f9c971331f5ee388c2d0509c ./Cargo.lock -f22febd60f53442e200fd1cb2d077d55319918a034812645f96f07d70abace89 ./Cargo.toml +f71644d81f7f91aee248bf6f61d711a8429694dae6a785706679468cde082e28 ./Cargo.lock +ed4fcc9fcf0d11995f086047c5876787e035a539d0aa588397f90862686adab1 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -120d48f01abeee74e84d4257bffc1fe7da9e8b3a07a048284b2c18ea273b07a6 ./README.md -83dbdd1c6acd9ffbab24d367b70ce20c59bf03d6a545b6bac8348145c777cdc2 ./SOURCE_MANIFEST.sha256 +9b0edc59898f7fdfa169cacc17b9c3307fd555cd9c4ca4079b534d0ae2437d23 ./README.md +67fc31faccb818e097c4ee5fd54c5929c6a1d63cf12cc36c652d6562a3bc82cd ./SOURCE_MANIFEST.sha256 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs -27203e279ab83dd2ff638db02d23d4b00f8864c92f09e8ed3194574b989c43ae ./docs/API.md +dfb2736a61aa52634a0ac23fe379baf5f6c03e04837356a1c0ff93e25dc41832 ./docs/API.md 8b548d02dbdf0563f2be617a1d677459bd1959bed93a1799a01bf61bb5aed405 ./docs/FLOW.md -bd95027c838d47f4829082c29d2d8b108d56cbd37230f8b894e954208ef18a37 ./docs/openapi.json +d9bf3a1644ed3628bd574109602857d7e425a272b9d168416911a8d93bf6f8c5 ./docs/openapi.json c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example 0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md @@ -22,7 +22,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom 41a8958a6fe6d10e5679c95ac34832583a2a15ecfd67d09b4e094a83d06f5c47 ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png -3235e64a80782c561645e4bad4444e2409a19b4d1f8f4c93a0296a443d6cca3b ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +37ce45f7e9a9fdf258ee1e97ca47599a86b33017e0c52227cdfc900f637b7f80 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py 39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py @@ -30,10 +30,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom 6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json 13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json -24f341cd167c903db0522a6a21f0714a7ec19c560be183bed745c50d0d6b599a ./ha-addon/repository/gree-controller/CHANGELOG.md +a1cfb2ec0dfc786e8d2c9adad5ad7b62abdf917c0090f06c817e78cd26115683 ./ha-addon/repository/gree-controller/CHANGELOG.md 1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.md c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md -c849408ccc076b254f01dc464935812c44e4bbec1e9727cc7664c900d4539563 ./ha-addon/repository/gree-controller/config.yaml +e87466f09667d3b7ba35a09e1879cd616ce62c28d99c60363ec835a3bb07ffa6 ./ha-addon/repository/gree-controller/config.yaml 3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png 0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png 2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml @@ -200,7 +200,7 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/ 8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html -8b8b5e9511a833d6dc0eb902048e8f48f26395faf55f19962c3e6f06ae0fb4e4 ./web/css/styles.css +6531bd30b81e8fe22ce973c2be3308e8ef7e3aa42285d86175b8329f2a670b61 ./web/css/styles.css 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg 784e2c28effb9872b582298292d3e2a6b8dd6140207342452193894d210e2f45 ./web/index.html @@ -221,7 +221,7 @@ e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon. c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js f7245f0e6cb5011196704f201675ebc6c19d902b8253ffcdbf9ead46cb1c6f86 ./web/js-dynamic/settings-ui.js 672e404470bc6331aa6d658523b221659ec64222af9cc9c17ced978f725790da ./web/js-dynamic/settings.js -930777048669688985b494a10dad8e591c82666071bde1018eef8f725a660895 ./web/js/custom-chart.js +489ab0208c86f7a2bb0d721106f01fb5d4f39f41e94749b440a6c58e13c4de07 ./web/js/custom-chart.js 6ae536b18db756bf5eda9adb1b8a550d5a02bfc80f0b7da27fa146039f22e884 ./web/js/lang-init.js e3fc113d4c349f33f3faa32e7a46c96a85c71643f43f2e488065dc9715e27333 ./web/js/sw.js d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/js/theme-init.js diff --git a/README.md b/README.md index e0bc767..0c9e927 100644 --- a/README.md +++ b/README.md @@ -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. -**Version: 0.14.23** +**Version: 0.14.24** -Version 0.14.23 includes code cleanup and small UX changes. +Version 0.14.24 improves public Custom Charts with single-pass language loading, English 404 fallback, pinch-to-zoom/pan and clickable metric legends, and includes the related public-chart, favicon and access-token fixes. > [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples. diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256 index ff4b6d6..c4e5a80 100644 --- a/SOURCE_MANIFEST.sha256 +++ b/SOURCE_MANIFEST.sha256 @@ -1,13 +1,13 @@ de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -9016ecffc9cc145fe5c2cee1e097b170de5c71a6f9c971331f5ee388c2d0509c ./Cargo.lock -f22febd60f53442e200fd1cb2d077d55319918a034812645f96f07d70abace89 ./Cargo.toml +f71644d81f7f91aee248bf6f61d711a8429694dae6a785706679468cde082e28 ./Cargo.lock +ed4fcc9fcf0d11995f086047c5876787e035a539d0aa588397f90862686adab1 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -120d48f01abeee74e84d4257bffc1fe7da9e8b3a07a048284b2c18ea273b07a6 ./README.md +9b0edc59898f7fdfa169cacc17b9c3307fd555cd9c4ca4079b534d0ae2437d23 ./README.md 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs -27203e279ab83dd2ff638db02d23d4b00f8864c92f09e8ed3194574b989c43ae ./docs/API.md +dfb2736a61aa52634a0ac23fe379baf5f6c03e04837356a1c0ff93e25dc41832 ./docs/API.md 8b548d02dbdf0563f2be617a1d677459bd1959bed93a1799a01bf61bb5aed405 ./docs/FLOW.md -bd95027c838d47f4829082c29d2d8b108d56cbd37230f8b894e954208ef18a37 ./docs/openapi.json +d9bf3a1644ed3628bd574109602857d7e425a272b9d168416911a8d93bf6f8c5 ./docs/openapi.json c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example 0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md @@ -21,7 +21,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom 41a8958a6fe6d10e5679c95ac34832583a2a15ecfd67d09b4e094a83d06f5c47 ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png -3235e64a80782c561645e4bad4444e2409a19b4d1f8f4c93a0296a443d6cca3b ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +37ce45f7e9a9fdf258ee1e97ca47599a86b33017e0c52227cdfc900f637b7f80 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py 39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py @@ -29,10 +29,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom 6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json 13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json -24f341cd167c903db0522a6a21f0714a7ec19c560be183bed745c50d0d6b599a ./ha-addon/repository/gree-controller/CHANGELOG.md +a1cfb2ec0dfc786e8d2c9adad5ad7b62abdf917c0090f06c817e78cd26115683 ./ha-addon/repository/gree-controller/CHANGELOG.md 1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.md c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md -c849408ccc076b254f01dc464935812c44e4bbec1e9727cc7664c900d4539563 ./ha-addon/repository/gree-controller/config.yaml +e87466f09667d3b7ba35a09e1879cd616ce62c28d99c60363ec835a3bb07ffa6 ./ha-addon/repository/gree-controller/config.yaml 3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png 0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png 2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml @@ -199,7 +199,7 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/ 8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html -8b8b5e9511a833d6dc0eb902048e8f48f26395faf55f19962c3e6f06ae0fb4e4 ./web/css/styles.css +6531bd30b81e8fe22ce973c2be3308e8ef7e3aa42285d86175b8329f2a670b61 ./web/css/styles.css 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg 784e2c28effb9872b582298292d3e2a6b8dd6140207342452193894d210e2f45 ./web/index.html @@ -220,7 +220,7 @@ e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon. c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js f7245f0e6cb5011196704f201675ebc6c19d902b8253ffcdbf9ead46cb1c6f86 ./web/js-dynamic/settings-ui.js 672e404470bc6331aa6d658523b221659ec64222af9cc9c17ced978f725790da ./web/js-dynamic/settings.js -930777048669688985b494a10dad8e591c82666071bde1018eef8f725a660895 ./web/js/custom-chart.js +489ab0208c86f7a2bb0d721106f01fb5d4f39f41e94749b440a6c58e13c4de07 ./web/js/custom-chart.js 6ae536b18db756bf5eda9adb1b8a550d5a02bfc80f0b7da27fa146039f22e884 ./web/js/lang-init.js e3fc113d4c349f33f3faa32e7a46c96a85c71643f43f2e488065dc9715e27333 ./web/js/sw.js d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/js/theme-init.js diff --git a/docs/API.md b/docs/API.md index 9ffa635..4620e6e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # GREE Controller API reference -HTTP and WebSocket API for GREE Controller **0.14.23**. +HTTP and WebSocket API for GREE Controller **0.14.24**. [← Main documentation](../README.md) @@ -240,7 +240,7 @@ Response: { "status": "ok", "name": "gree-controller", - "version": "0.14.23", + "version": "0.14.24", "uptime_seconds": 1234, "control_ready": true, "time": "2026-08-30T06:54:00Z" @@ -303,7 +303,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.14.23", + "version": "0.14.24", "uptime_seconds": 1234, "auth_required": false, "control_ready": true, diff --git a/docs/openapi.json b/docs/openapi.json index ced7d1d..52fc6bc 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2,9 +2,9 @@ "openapi": "3.1.0", "info": { "title": "GREE Controller API", - "version": "0.14.23", + "version": "0.14.24", "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\nAdministrator endpoints accept `Authorization: Bearer ` or `x-api-token: ` when configured. Restricted Home Assistant control endpoints also accept generated integration tokens.\n\n### Local and GREE Cloud\nDevices use one API model and one command surface. `connection_type=local` is dispatched only to the existing LAN/UDP provider; `connection_type=gree_cloud` is dispatched only to the GREE Cloud provider. No automatic transport fallback is performed.\n\n### GREE Cloud security\nCloud passwords, REST/MQTT tokens, Authorization values and device cipher keys are never returned in normal API responses or Cloud diagnostics. MQTT uses TLS with certificate and hostname validation.\n\n### Energy\nGREE Cloud `ElcAll` and selected Home Assistant cumulative energy sensors are normalized to kWh and converted to consumption deltas before aggregation. Raw cumulative values are retained only as meter samples/baselines.\n\n### 0.14.23\nCode cleanup and small UX changes.\n\n### 0.14.22\nCode cleanup and small UX changes.\n\n### 0.14.21\nCode cleanup and small UX changes.\n\n### 0.14.20\nFixes the ARM cross-build bootstrap by removing the invalid xx-cargo version probe, prepares the target Rust standard library in a cacheable pre-source layer, shares dependency fetching across target architectures, and isolates compiled target caches per architecture while preserving DISTRO-selected Alpine/musl and Debian Trixie/glibc runtimes.\n\n### 0.14.19\nHome Assistant OCI builds now compile Rust for amd64/arm64 on the native build platform through xx cross-compilation, keep Cargo registry/target caches between builds, and send a smaller Docker context while preserving the selected Alpine (musl) or Debian Trixie (glibc) runtime. The Web UI also deduplicates the initial GREE Cloud runtime-status GET across the HTTP and WebSocket bootstrap renders.\n\n### 0.14.18\nShared Web UI CSS now uses generic reusable class names for segmented tabs, configuration layouts, chart cards, enable toggles, diagram primitives and Flow block categories; behavior and visual styling are unchanged.\n\n### 0.14.17\nHistory navigation now reuses the same segmented tab component as Flow / Schedules / Automations, with redundant History-specific tab CSS removed.\n\n### 0.14.16\nCustom Charts now expose saved-chart editing in the Web UI, public chart pages can switch history range through an optional hours override, and the standalone chart layout fits the viewport without vertical page scrolling.\n\n### 0.14.15\nHome Assistant add-on Custom Chart links now resolve the primary HA host IPv4 through Supervisor, support an explicit public_chart_base_url override, and enforce the fixed 8787 ingress/application port contract at startup and build time. Standalone bind/port behavior remains configurable.\n\n### 0.14.14\nShared Flow input Home Assistant sources now expose a dedicated entity search box separate from the selected entity_id, making large live entity catalogs easy to filter by name, ID and state.\n\n### 0.14.13\nHome Assistant add-on authentication is now hidden behind an automatic Supervisor status, manual URL/token fallback is unlocked only after a failed HA test, connection tests return a live sample entity reading, and Shared Flow input HA sources include searchable live entity suggestions.\n\n### 0.14.12\nThe Home Assistant add-on now uses the Supervisor Core API proxy and runtime SUPERVISOR_TOKEN automatically; standalone Home Assistant URL/token configuration is unchanged.\n\n### 0.14.11\nWeb UI icons now use a shared inline SVG set instead of font-dependent Unicode glyphs across navigation, dialogs, Flow, charts, controls and event categories for consistent cross-platform rendering.\n\n\n### 0.14.10\nNight mode and Home Assistant / Sensors standalone settings now use the full available page width, matching the other Web UI sections.\n\n### 0.14.9\nPower controls use an embedded SVG icon instead of a font-dependent Unicode glyph. Home Assistant room-sensor stale/error events identify the affected entity and zone, and historical event rows can recover that context from metadata.\n\n### 0.14.8\nRemaining Home Assistant and notification diagnostic errors no longer use device-transport 502 responses. Missing/invalid integration configuration returns 400, configured external dependency failures return 424, and 502 is reserved for Local/LAN GREE device communication.\n\n### 0.14.7\nHome Assistant energy-sensor discovery now returns an empty optional result when HA is not configured, uses 424 for configured external-dependency failures, and custom select popovers use their rendered height when positioning above a field.\n\n\n### 0.14.6\nFixes dynamic custom-select duplication in History → Energy and hides Energy history controls when no energy source is configured.\n\n### 0.14.5\nWeb UI single-choice controls now use one consistent custom dropdown, language packs are loaded on demand, and the dashboard outdoor temperature opens a 24-hour history modal.\n\n### 0.14.4\nHome Assistant connection tests validate only the configured server/API token and require no entity. `outdoor_entity_id` is the global Home Assistant outdoor-temperature sensor; zones can override it with `ha_outdoor_entity_id`. Room-temperature Home Assistant sensors remain configured per zone with `ha_entity_id`. Global/per-zone outdoor sensors are integrated with aliases, metrics/history and Visual Flow entity suggestions.\n\n### 0.14.2\nAdds independent vertical/horizontal swing controls across manual device control, thermostat zones, legacy automations, Visual Flow and Home Assistant zone climates. Swing is treated as an auxiliary one-shot unit setting and does not replace thermostat ownership.\n\n### 0.14.1\nAdds split/multisplit installation groups with shared energy and outdoor-temperature sources, multi-target energy charts with period comparison, cleaner Cloud status/alerts and safe MQTT cleanup after the last Cloud device is removed.\n\n### 0.14.0\nAdds first-class GREE Cloud MQTT status/control, reconnect/resubscribe lifecycle, Cloud diagnostics, per-device capabilities, Cloud/HA energy sources and energy-history aggregation while preserving the LAN transport contract.", + "description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nAdministrator endpoints accept `Authorization: Bearer ` or `x-api-token: ` when configured. Restricted Home Assistant control endpoints also accept generated integration tokens.\n\n### Local and GREE Cloud\nDevices use one API model and one command surface. `connection_type=local` is dispatched only to the existing LAN/UDP provider; `connection_type=gree_cloud` is dispatched only to the GREE Cloud provider. No automatic transport fallback is performed.\n\n### GREE Cloud security\nCloud passwords, REST/MQTT tokens, Authorization values and device cipher keys are never returned in normal API responses or Cloud diagnostics. MQTT uses TLS with certificate and hostname validation.\n\n### Energy\nGREE Cloud `ElcAll` and selected Home Assistant cumulative energy sensors are normalized to kWh and converted to consumption deltas before aggregation. Raw cumulative values are retained only as meter samples/baselines.\n\n### 0.14.24\nPublic Custom Charts now fetch chart metadata before language assets, use English for missing-chart errors, support pinch-to-zoom/panning and clickable metric legends, and include the related missing-ID, favicon and Access Token UX fixes.\n\n### 0.14.23\nCode cleanup and small UX changes.\n\n### 0.14.22\nCode cleanup and small UX changes.\n\n### 0.14.21\nCode cleanup and small UX changes.\n\n### 0.14.20\nFixes the ARM cross-build bootstrap by removing the invalid xx-cargo version probe, prepares the target Rust standard library in a cacheable pre-source layer, shares dependency fetching across target architectures, and isolates compiled target caches per architecture while preserving DISTRO-selected Alpine/musl and Debian Trixie/glibc runtimes.\n\n### 0.14.19\nHome Assistant OCI builds now compile Rust for amd64/arm64 on the native build platform through xx cross-compilation, keep Cargo registry/target caches between builds, and send a smaller Docker context while preserving the selected Alpine (musl) or Debian Trixie (glibc) runtime. The Web UI also deduplicates the initial GREE Cloud runtime-status GET across the HTTP and WebSocket bootstrap renders.\n\n### 0.14.18\nShared Web UI CSS now uses generic reusable class names for segmented tabs, configuration layouts, chart cards, enable toggles, diagram primitives and Flow block categories; behavior and visual styling are unchanged.\n\n### 0.14.17\nHistory navigation now reuses the same segmented tab component as Flow / Schedules / Automations, with redundant History-specific tab CSS removed.\n\n### 0.14.16\nCustom Charts now expose saved-chart editing in the Web UI, public chart pages can switch history range through an optional hours override, and the standalone chart layout fits the viewport without vertical page scrolling.\n\n### 0.14.15\nHome Assistant add-on Custom Chart links now resolve the primary HA host IPv4 through Supervisor, support an explicit public_chart_base_url override, and enforce the fixed 8787 ingress/application port contract at startup and build time. Standalone bind/port behavior remains configurable.\n\n### 0.14.14\nShared Flow input Home Assistant sources now expose a dedicated entity search box separate from the selected entity_id, making large live entity catalogs easy to filter by name, ID and state.\n\n### 0.14.13\nHome Assistant add-on authentication is now hidden behind an automatic Supervisor status, manual URL/token fallback is unlocked only after a failed HA test, connection tests return a live sample entity reading, and Shared Flow input HA sources include searchable live entity suggestions.\n\n### 0.14.12\nThe Home Assistant add-on now uses the Supervisor Core API proxy and runtime SUPERVISOR_TOKEN automatically; standalone Home Assistant URL/token configuration is unchanged.\n\n### 0.14.11\nWeb UI icons now use a shared inline SVG set instead of font-dependent Unicode glyphs across navigation, dialogs, Flow, charts, controls and event categories for consistent cross-platform rendering.\n\n\n### 0.14.10\nNight mode and Home Assistant / Sensors standalone settings now use the full available page width, matching the other Web UI sections.\n\n### 0.14.9\nPower controls use an embedded SVG icon instead of a font-dependent Unicode glyph. Home Assistant room-sensor stale/error events identify the affected entity and zone, and historical event rows can recover that context from metadata.\n\n### 0.14.8\nRemaining Home Assistant and notification diagnostic errors no longer use device-transport 502 responses. Missing/invalid integration configuration returns 400, configured external dependency failures return 424, and 502 is reserved for Local/LAN GREE device communication.\n\n### 0.14.7\nHome Assistant energy-sensor discovery now returns an empty optional result when HA is not configured, uses 424 for configured external-dependency failures, and custom select popovers use their rendered height when positioning above a field.\n\n\n### 0.14.6\nFixes dynamic custom-select duplication in History → Energy and hides Energy history controls when no energy source is configured.\n\n### 0.14.5\nWeb UI single-choice controls now use one consistent custom dropdown, language packs are loaded on demand, and the dashboard outdoor temperature opens a 24-hour history modal.\n\n### 0.14.4\nHome Assistant connection tests validate only the configured server/API token and require no entity. `outdoor_entity_id` is the global Home Assistant outdoor-temperature sensor; zones can override it with `ha_outdoor_entity_id`. Room-temperature Home Assistant sensors remain configured per zone with `ha_entity_id`. Global/per-zone outdoor sensors are integrated with aliases, metrics/history and Visual Flow entity suggestions.\n\n### 0.14.2\nAdds independent vertical/horizontal swing controls across manual device control, thermostat zones, legacy automations, Visual Flow and Home Assistant zone climates. Swing is treated as an auxiliary one-shot unit setting and does not replace thermostat ownership.\n\n### 0.14.1\nAdds split/multisplit installation groups with shared energy and outdoor-temperature sources, multi-target energy charts with period comparison, cleaner Cloud status/alerts and safe MQTT cleanup after the last Cloud device is removed.\n\n### 0.14.0\nAdds first-class GREE Cloud MQTT status/control, reconnect/resubscribe lifecycle, Cloud diagnostics, per-device capabilities, Cloud/HA energy sources and energy-history aggregation while preserving the LAN transport contract.", "license": { "name": "MIT" } @@ -5900,7 +5900,7 @@ }, "version": { "type": "string", - "example": "0.14.23" + "example": "0.14.24" }, "uptime_seconds": { "type": "integer", diff --git a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json index a633858..90e7ec9 100644 --- a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json +++ b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "gree_controller", "name": "GREE Controller", - "version": "0.14.23", + "version": "0.14.24", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/ha-addon/repository/gree-controller/CHANGELOG.md b/ha-addon/repository/gree-controller/CHANGELOG.md index a1db643..0a60f3e 100644 --- a/ha-addon/repository/gree-controller/CHANGELOG.md +++ b/ha-addon/repository/gree-controller/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.14.24 + +- Public Custom Charts fetch chart metadata before language assets, so a chart loads only its selected language pack; missing chart IDs fall back to English. +- Public Custom Charts support pinch-to-zoom, one-finger horizontal panning while zoomed and per-metric visibility toggles through the legend. +- Missing public chart IDs now return `chart id not found`, the favicon is no longer redundantly pre-cached, and invalid Access Tokens show an inline authentication error. + ## 0.14.23 - Code cleanup and small UX changes. diff --git a/ha-addon/repository/gree-controller/config.yaml b/ha-addon/repository/gree-controller/config.yaml index f0d4e1c..4c99ade 100644 --- a/ha-addon/repository/gree-controller/config.yaml +++ b/ha-addon/repository/gree-controller/config.yaml @@ -1,5 +1,5 @@ name: "GREE Controller" -version: "0.14.23" +version: "0.14.24" slug: "gree_controller" description: "Local GREE HVAC controller with Web UI and Home Assistant integration" url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/" diff --git a/web/css/styles.css b/web/css/styles.css index a041ccb..3fbf10d 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -10226,6 +10226,7 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr min-width: 720px; height: 100%; min-height: 0; + touch-action: none; } .public-custom-chart-card .legend { @@ -10241,13 +10242,7 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr } .public-chart-legend-item { - cursor: default; -} - -.public-chart-legend-item:hover { - border-color: transparent; - background: transparent; - color: var(--muted); + flex: 0 0 auto; } .public-custom-chart-error { diff --git a/web/js/custom-chart.js b/web/js/custom-chart.js index 266a543..536b360 100644 --- a/web/js/custom-chart.js +++ b/web/js/custom-chart.js @@ -4,6 +4,7 @@ const PUBLIC_CHART_COLORS = ['--accent', '--info', '--warning', '--purple', '--d const $ = selector => document.querySelector(selector); const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); const PUBLIC_CHART_RANGES = [6, 24, 168, 720, 2160, 8760]; +const PUBLIC_MAX_CHART_ZOOM = 16; function basePathFromScript() { const src = document.currentScript?.src || ''; @@ -23,11 +24,8 @@ const publicLanguagePacks = new Map(); let publicChartLanguage = PUBLIC_DEFAULT_LANGUAGE; let publicChartTranslations = {}; let publicChartLocale = PUBLIC_DEFAULT_LANGUAGE; - -function publicCookie(name) { - const row = document.cookie.split('; ').find(item => item.startsWith(`${name}=`)); - return row ? decodeURIComponent(row.split('=').slice(1).join('=')) : ''; -} +let publicChartZoom = 1; +const publicChartHiddenSeries = new Set(); function publicTr(key, params = {}) { const template = publicChartTranslations[key] ?? key; @@ -85,7 +83,8 @@ function preciseTime(timestamp, locale) { function prepareCanvas(canvas, height = 520) { const wrap = canvas.parentElement; - const width = Math.max(720, Math.floor(wrap?.clientWidth || canvas.getBoundingClientRect().width || 720)); + const baseWidth = Math.max(720, Math.floor(wrap?.clientWidth || canvas.getBoundingClientRect().width || 720)); + const width = Math.max(1, Math.floor(baseWidth * publicChartZoom)); const actualHeight = Math.max(180, Math.floor(wrap?.clientHeight || height)); const dpr = window.devicePixelRatio || 1; canvas.width = Math.floor(width * dpr); @@ -98,12 +97,21 @@ function prepareCanvas(canvas, height = 520) { return { ctx, width, height: actualHeight }; } +function publicChartSeriesKey(item, index) { + return item.key || `${index}:${item.label}`; +} + +function isPublicChartSeriesHidden(item, index) { + return publicChartHiddenSeries.has(publicChartSeriesKey(item, index)); +} + function renderLegend(series) { const host = $('#publicCustomChartLegend'); host.innerHTML = series.map((item, index) => { - const color = cssColor(PUBLIC_CHART_COLORS[index % PUBLIC_CHART_COLORS.length], '#3ecf8e'); + const hidden = isPublicChartSeriesHidden(item, index); const dashed = item.dashed ? ' is-dashed' : ''; - return `${escapeHtml(item.label)}`; + const title = publicTr(hidden ? 'history.legendShow' : 'history.legendHide', { name: item.label }); + return ``; }).join(''); } @@ -117,6 +125,13 @@ function drawEmpty(canvas, message) { ctx.font = '13px system-ui'; ctx.textAlign = 'center'; ctx.fillText(message, width / 2, height / 2); + const wrap = canvas.parentElement; + const tooltip = wrap?.querySelector('.chart-tooltip'); + const line = wrap?.querySelector('.chart-hover-line'); + if (tooltip) tooltip.hidden = true; + if (line) line.hidden = true; + canvas.onpointermove = null; + canvas.onpointerleave = null; } function nearestPoint(points, targetTs) { @@ -142,7 +157,7 @@ function bindTooltip(canvas, series, geometry, locale) { const timestampPoints = timestamps.map(ts => ({ ts })); const seriesPoints = series.map((item, index) => ({ item, - color: cssColor(PUBLIC_CHART_COLORS[index % PUBLIC_CHART_COLORS.length], '#3ecf8e'), + color: item.color || cssColor(PUBLIC_CHART_COLORS[index % PUBLIC_CHART_COLORS.length], '#3ecf8e'), points: item.points.map(point => ({ ts: new Date(point.timestamp).getTime(), value: Number(point.value) })) .filter(point => Number.isFinite(point.ts) && Number.isFinite(point.value)) .sort((a, b) => a.ts - b.ts) @@ -178,12 +193,13 @@ function bindTooltip(canvas, series, geometry, locale) { } function drawChart(canvas, payload, locale) { - const series = (payload.series || []).map((item, index) => ({ + const allSeries = (payload.series || []).map((item, index) => ({ ...item, color: cssColor(PUBLIC_CHART_COLORS[index % PUBLIC_CHART_COLORS.length], '#3ecf8e'), points: Array.isArray(item.points) ? item.points : [] })); - renderLegend(series); + renderLegend(allSeries); + const series = allSeries.filter((item, index) => !isPublicChartSeriesHidden(item, index)); const allPoints = series.flatMap(item => item.points.map(point => ({ ...point, value: Number(point.value) }))) .filter(point => Number.isFinite(point.value) && Number.isFinite(new Date(point.timestamp).getTime())); if (!allPoints.length) return drawEmpty(canvas, payload.no_data_label || publicTr('publicChart.noData')); @@ -288,6 +304,80 @@ function redrawPublicChart() { drawChart($('#publicCustomChart'), publicChartPayload, publicChartLocale); } +function setPublicChartZoom(value, anchorRatio = null, anchorViewportX = null) { + const canvas = $('#publicCustomChart'); + const wrap = canvas?.parentElement; + if (!canvas || !wrap || !publicChartPayload) return; + const oldWidth = canvas.getBoundingClientRect().width || 1; + const ratio = Number.isFinite(anchorRatio) + ? clamp(anchorRatio, 0, 1) + : clamp((wrap.scrollLeft + wrap.clientWidth / 2) / oldWidth, 0, 1); + const viewportX = Number.isFinite(anchorViewportX) ? anchorViewportX : wrap.clientWidth / 2; + const next = clamp(Number(value) || 1, 1, PUBLIC_MAX_CHART_ZOOM); + if (Math.abs(next - publicChartZoom) < 0.001) return; + publicChartZoom = next; + redrawPublicChart(); + const nextWidth = canvas.getBoundingClientRect().width || 1; + wrap.scrollLeft = Math.max(0, ratio * nextWidth - viewportX); +} + +function bindPublicChartTouchGestures() { + const canvas = $('#publicCustomChart'); + const wrap = canvas?.parentElement; + if (!canvas || !wrap) return; + let pinch = null; + let pan = null; + const distance = touches => Math.hypot( + touches[0].clientX - touches[1].clientX, + touches[0].clientY - touches[1].clientY + ); + const beginPinch = touches => { + const rect = canvas.getBoundingClientRect(); + const wrapRect = wrap.getBoundingClientRect(); + const centerX = (touches[0].clientX + touches[1].clientX) / 2; + pinch = { + distance: Math.max(1, distance(touches)), + zoom: publicChartZoom, + anchorRatio: clamp((centerX - rect.left) / Math.max(1, rect.width), 0, 1), + anchorViewportX: clamp(centerX - wrapRect.left, 0, wrap.clientWidth), + }; + pan = null; + }; + const beginPan = touch => { + pan = { clientX: touch.clientX, scrollLeft: wrap.scrollLeft }; + }; + + canvas.addEventListener('touchstart', event => { + if (event.touches.length >= 2) { + event.preventDefault(); + beginPinch(event.touches); + } else if (event.touches.length === 1) { + beginPan(event.touches[0]); + } + }, { passive: false }); + + canvas.addEventListener('touchmove', event => { + if (event.touches.length >= 2) { + event.preventDefault(); + if (!pinch) beginPinch(event.touches); + const scale = distance(event.touches) / Math.max(1, pinch.distance); + setPublicChartZoom(pinch.zoom * scale, pinch.anchorRatio, pinch.anchorViewportX); + return; + } + if (event.touches.length === 1 && pan && wrap.scrollWidth > wrap.clientWidth + 1) { + event.preventDefault(); + wrap.scrollLeft = pan.scrollLeft + (pan.clientX - event.touches[0].clientX); + } + }, { passive: false }); + + canvas.addEventListener('touchend', event => { + pinch = null; + if (event.touches.length === 1) beginPan(event.touches[0]); + else pan = null; + }, { passive: true }); + canvas.addEventListener('touchcancel', () => { pinch = null; pan = null; }, { passive: true }); +} + async function loadPublicChart(hours = requestedPublicChartHours()) { const token = publicChartToken(); const title = $('#publicChartTitle'); @@ -297,6 +387,7 @@ async function loadPublicChart(hours = requestedPublicChartHours()) { const range = $('#publicChartHours'); if (!token.startsWith('chart_')) { + await setPublicChartLanguage('en'); title.textContent = publicTr('publicChart.title'); hint.textContent = ''; error.hidden = false; @@ -306,16 +397,19 @@ async function loadPublicChart(hours = requestedPublicChartHours()) { } if (range) range.disabled = true; - if (hint) hint.textContent = publicTr('publicChart.loading'); + if (hint) hint.textContent = Object.keys(publicChartTranslations).length ? publicTr('publicChart.loading') : 'Loading…'; error.hidden = true; const response = await fetch(withBase(publicChartApiPath(token, hours)), { headers: { Accept: 'application/json' }, cache: 'no-store' }); if (!response.ok) { let message = `${response.status}`; try { message = (await response.json()).error || message; } catch (_) { } - throw new Error(message); + if (response.status === 404) await setPublicChartLanguage('en'); + const requestError = new Error(message); + requestError.status = response.status; + throw requestError; } const payload = await response.json(); - await setPublicChartLanguage(payload.lang || publicChartLanguage); + await setPublicChartLanguage(payload.lang || 'en'); title.textContent = payload.title || publicTr('publicChart.title'); hint.textContent = payload.hint || publicTr('publicChart.periodHint', { hours: payload.hours }); canvas.setAttribute('aria-label', title.textContent); @@ -329,6 +423,18 @@ async function loadPublicChart(hours = requestedPublicChartHours()) { if (range) range.disabled = false; } +$('#publicCustomChartLegend')?.addEventListener('click', event => { + const button = event.target.closest('button[data-public-chart-legend]'); + if (!button || !publicChartPayload) return; + const index = Number(button.dataset.publicChartLegend); + const item = publicChartPayload.series?.[index]; + if (!item) return; + const key = publicChartSeriesKey(item, index); + if (publicChartHiddenSeries.has(key)) publicChartHiddenSeries.delete(key); + else publicChartHiddenSeries.add(key); + redrawPublicChart(); +}); + $('#publicChartHours')?.addEventListener('change', async event => { const hours = Number(event.currentTarget.value); if (!Number.isInteger(hours) || hours < 1 || hours > 87600) return; @@ -351,16 +457,15 @@ window.addEventListener('resize', () => { }); async function initPublicChart() { - try { - await setPublicChartLanguage(publicCookie('gree_controller_language') || PUBLIC_DEFAULT_LANGUAGE); - } catch (error) { - console.error('Public chart language initialization failed:', error); - } finally { - document.documentElement.classList.remove('i18n-loading'); - } + bindPublicChartTouchGestures(); try { await loadPublicChart(); } catch (error) { + if (!Object.keys(publicChartTranslations).length) { + try { await setPublicChartLanguage('en'); } catch (languageError) { + console.error('Public chart fallback language initialization failed:', languageError); + } + } const host = $('#publicChartError'); host.hidden = false; host.textContent = publicChartTranslations['publicChart.loadFailed'] @@ -369,6 +474,8 @@ async function initPublicChart() { $('#publicChartHint').textContent = ''; $('#publicChartHours').disabled = false; drawEmpty($('#publicCustomChart'), host.textContent); + } finally { + document.documentElement.classList.remove('i18n-loading'); } }