This commit is contained in:
Mateusz Gruszczyński
2026-09-17 10:06:11 +02:00
parent 2f0bf20033
commit 32f8a32bd2
12 changed files with 169 additions and 61 deletions
Generated
+1 -1
View File
@@ -700,7 +700,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.14.23"
version = "0.14.24"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -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"
+11 -11
View File
@@ -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
+2 -2
View File
@@ -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.
+10 -10
View File
@@ -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
+3 -3
View File
@@ -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,
+3 -3
View File
File diff suppressed because one or more lines are too long
@@ -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",
@@ -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.
@@ -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/"
+2 -7
View File
@@ -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 {
+128 -21
View File
@@ -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 `<span class="legend-item public-chart-legend-item"><i class="legend-line${dashed}" style="--legend-color:${color}"></i><span>${escapeHtml(item.label)}</span></span>`;
const title = publicTr(hidden ? 'history.legendShow' : 'history.legendHide', { name: item.label });
return `<button type="button" class="legend-item public-chart-legend-item${hidden ? ' is-hidden' : ''}" data-public-chart-legend="${index}" aria-pressed="${hidden ? 'false' : 'true'}" title="${escapeHtml(title)}"><i class="legend-line${dashed}" style="--legend-color:${escapeHtml(item.color)}"></i><span>${escapeHtml(item.label)}</span></button>`;
}).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');
}
}