v0.8.10
This commit is contained in:
+11
-8
@@ -1,15 +1,18 @@
|
|||||||
# GREE Controller v0.8.9 - remote takeover diagnostics report
|
# GREE Controller v0.8.10 - dashboard and notification-filter report
|
||||||
|
|
||||||
Source baseline: GREE Controller v0.8.8.
|
Source baseline: GREE Controller v0.8.9.
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
|
|
||||||
- removed the v0.8.8 debounce/confirmation heuristic that required an unexpected post-command GREE state to survive two polls;
|
- added fine-grained notification categories with a dedicated stale Home Assistant sensor switch;
|
||||||
- restored the previous controller-settling rule: only values matching a recent controller command or its pre-command baseline are suppressed; any other changed control field is immediately eligible for manual/pilot takeover;
|
- split stale HA reads into the `ha.sensor_stale` event kind while retaining other HA failures as `ha.sensor_error`;
|
||||||
- added an explicit `device.remote_control_detected` event before a poll-detected takeover is persisted;
|
- rebuilt the Dashboard as horizontal **Main / Thermostats / Manual control** tabs with one vertically scrolling panel at a time;
|
||||||
- diagnostic metadata contains raw and effective changed fields, compact before/after physical device state, recent controller commands and baselines, remaining settling-window time, and current zone/Temporary Quick Thermostat state;
|
- moved LAN discovery from the global toolbar to Devices;
|
||||||
- retained the v0.8.8 mobile modal layout fix.
|
- added centered `active/total running` unit status to the top toolbar;
|
||||||
|
- changed control-owner metadata to show date and time instead of time only;
|
||||||
|
- removed visible "Quick" wording from the Dashboard thermostat/manual-control section titles;
|
||||||
|
- rotated the PWA cache key for the changed frontend.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
The Rust toolchain is not installed in this build environment, so `cargo test`/`cargo check` cannot be run here. Static source checks, JavaScript syntax checks, JSON parsing, shell syntax checks, manifest regeneration and ZIP integrity verification are performed before packaging. The target-host updater should run the full Rust test suite before replacing the service.
|
The Rust toolchain is not installed in this build environment, so `cargo test`/`cargo check` cannot be run here. JavaScript syntax checks, JSON parsing, HTML ID checks, shell syntax checks, manifest regeneration and ZIP integrity verification are performed before packaging. The target-host updater should run the full Rust test suite before replacing the service.
|
||||||
|
|||||||
Generated
+1
-1
@@ -633,7 +633,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gree-controller"
|
name = "gree-controller"
|
||||||
version = "0.8.9"
|
version = "0.8.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gree-controller"
|
name = "gree-controller"
|
||||||
version = "0.8.9"
|
version = "0.8.10"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["GREE Controller contributors"]
|
authors = ["GREE Controller contributors"]
|
||||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||||
|
|||||||
+15
-15
@@ -1,10 +1,10 @@
|
|||||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||||
330ef304a321f8ada55e4c2953793d8cd6709d4c476522608bd91ccd26810059 ./BUILD_REPORT.md
|
875d85d3388362890ef0fa28b7192634856b8c095f5fbdacc75d60e014f692cc ./BUILD_REPORT.md
|
||||||
c0de04155e7751108a924de64923a64cbd8e0cddaf39f1b8af5b92cecda90f15 ./Cargo.lock
|
c91ff48b9278d8394e79e734b11cc49d0c092881500edc11121b6baca23d2e67 ./Cargo.lock
|
||||||
ec8ba278feb21789672828847085c3671fbe8af179efc9284ae12f3084760897 ./Cargo.toml
|
43bf5b7dc87db920fdd5a7157c50c92aef7053a293c51c86c5b5fa76c761f9d7 ./Cargo.toml
|
||||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||||
840e83950ea5fa9532f855d10a10acc05b42e11207a476ef3f07a8c940d12218 ./README.md
|
21dd3dc409d51f29890ee303f97eaa4e68362c100e499c48bfc1ef8e57f525fd ./README.md
|
||||||
91c826e2f8c974bdb7b15e01e9b20487f06ff887247ecd9600f1263c11a61b96 ./build.rs
|
91c826e2f8c974bdb7b15e01e9b20487f06ff887247ecd9600f1263c11a61b96 ./build.rs
|
||||||
6e2953807ea0d1f346f99adb527391806cb088afdbc62e721193be7253a3bc9c ./docs/API.md
|
6e2953807ea0d1f346f99adb527391806cb088afdbc62e721193be7253a3bc9c ./docs/API.md
|
||||||
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
|
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
|
||||||
@@ -24,7 +24,7 @@ ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assista
|
|||||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
|
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/icon.png
|
||||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
|
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
|
||||||
b59c800f2abc81b9eef671eb5daa070bd1ce99edabfd91e78e09a8995ece2bba ./home-assistant/custom_components/gree_controller/manifest.json
|
98f494432dbc861b5b4b8928715845590189008482733006af0b0d9277d193a8 ./home-assistant/custom_components/gree_controller/manifest.json
|
||||||
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
|
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
|
||||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
|
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
|
||||||
1cae12876eb81085910907cf1fcf340a18dcba4f8c967ef222c9496325057849 ./home-assistant/custom_components/gree_controller/sensor.py
|
1cae12876eb81085910907cf1fcf340a18dcba4f8c967ef222c9496325057849 ./home-assistant/custom_components/gree_controller/sensor.py
|
||||||
@@ -33,8 +33,8 @@ b59c800f2abc81b9eef671eb5daa070bd1ce99edabfd91e78e09a8995ece2bba ./home-assista
|
|||||||
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
|
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
|
||||||
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
|
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
|
||||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||||
cf7819fb7572ed0b082c52f9cb5a9300abbc54ae499742e543d311de052386b1 ./lang/en.json
|
29cc08123a581474a4445d8bf98dcf22f17cdd2e0e7f1358158e588e1252eec5 ./lang/en.json
|
||||||
8a54ee55314fab8aaea867de4352dfac9c9b2022402d911c7804cf4b62a1a181 ./lang/pl.json
|
166d7daf829d8b3f9127a90f2da8c3db0ee2d8b490a083d1c278e7ccbde48a27 ./lang/pl.json
|
||||||
028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py
|
028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py
|
||||||
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
|
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
|
||||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||||
@@ -47,26 +47,26 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
|
|||||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||||
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
|
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
|
||||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||||
efd6541d9b044686fd6c456031a2cdec825f50e558255b5c0ce3e22ba09a1a66 ./src/api.rs
|
8295771c6907bcbc4e57d3c4572c120b72501167d02706f1fade7c1c09d6a6bf ./src/api.rs
|
||||||
9040e8cb6647c76a875148b7591abcccfe4e2d4708ef462a1e5d25dd4ccac911 ./src/config.rs
|
9040e8cb6647c76a875148b7591abcccfe4e2d4708ef462a1e5d25dd4ccac911 ./src/config.rs
|
||||||
5dfda2f4dc540c502885b0cd7017dc77768684588acf528f01d1fd88f1af4aec ./src/db.rs
|
5dfda2f4dc540c502885b0cd7017dc77768684588acf528f01d1fd88f1af4aec ./src/db.rs
|
||||||
cb50fa07357ca110bf37038262d3ede008ef36682cdb99cc18e0e66755e6289f ./src/engine.rs
|
15d07a2c0ad8ec6530897980787e6e9a94103bda849f07c233f6a37796d1f0b1 ./src/engine.rs
|
||||||
4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs
|
4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs
|
||||||
c6ff66da9ad08506f839ec56a869ead3abc824b5311e0ea9244517d11f7f4207 ./src/home_assistant.rs
|
c6ff66da9ad08506f839ec56a869ead3abc824b5311e0ea9244517d11f7f4207 ./src/home_assistant.rs
|
||||||
190b0a33431539676e5dd7796698077f16c179d42eae4501ca96a91bf797cbf8 ./src/influxdb.rs
|
190b0a33431539676e5dd7796698077f16c179d42eae4501ca96a91bf797cbf8 ./src/influxdb.rs
|
||||||
c67212da6a2bd5c2933ece31586ca4048b484fda3105f23a6e06e881b079d129 ./src/main.rs
|
c67212da6a2bd5c2933ece31586ca4048b484fda3105f23a6e06e881b079d129 ./src/main.rs
|
||||||
3c085cb91f514ffef78a68505cfda21ad9947aa91f7a3946ec66893b1e3ba042 ./src/models.rs
|
ef841f3ab27da395465ef94256dba74b6f04a24f65b248d80eb560f542ee968e ./src/models.rs
|
||||||
ea4bd7f20759101c2525600a1f342d2fc0946e2f0b3208b03cc5a415b6947c10 ./src/notifications.rs
|
9c735d7c475f21f2f388c21be784376f7184f0bf18af1886544ef51585a3b290 ./src/notifications.rs
|
||||||
7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs
|
7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs
|
||||||
bc03d88e5476386747ff5e32f1788cdc1fcd4bef238ab66df2dada282c348c8f ./src/protocol/gree.rs
|
bc03d88e5476386747ff5e32f1788cdc1fcd4bef238ab66df2dada282c348c8f ./src/protocol/gree.rs
|
||||||
a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol/mod.rs
|
a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol/mod.rs
|
||||||
6a1c0cab3eab80ecd254c5e486b1fba068523edb8d699c64054c17f67f4a31d0 ./src/queries.rs
|
6a1c0cab3eab80ecd254c5e486b1fba068523edb8d699c64054c17f67f4a31d0 ./src/queries.rs
|
||||||
2d69811db832c90ce06035ee29665205a04e4c514a21ecae9623991a9b14d825 ./src/state.rs
|
2d69811db832c90ce06035ee29665205a04e4c514a21ecae9623991a9b14d825 ./src/state.rs
|
||||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||||
4ea78fda84eb3202c8e16b9ab5f43fb77036163ff8f170757eb5bf32b711eb37 ./web/app.js
|
22c7882d8830101f21435069489dc976acb31fe027be8182c5bc444d3367c451 ./web/app.js
|
||||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||||
ee64c273b334c2d0f9747408b6142a8d506021cbf2107178c2213ca4e4a17c8c ./web/index.html
|
5900f4f892c1515178c26a8a243476010513970185009a0b3d44889e0c4c9681 ./web/index.html
|
||||||
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
|
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
|
||||||
c4e68eae3cfb5c44f5297eec2e97545d6e493d2e9fc7a0d4c2d7edc5bd609225 ./web/styles.css
|
db9319c0a0c6927a4f3e2b0a2a83927dd61ad6a456e9ca8fad9f3b48d1987778 ./web/styles.css
|
||||||
9d585effe4389fc307f8be8d656cf3eae4a96ad492a847c7acc8002ee52acbcb ./web/sw.js
|
2da305f344ba9cf2d56975fdc4c446ef47b65a117daf13e0689091bd31303f32 ./web/sw.js
|
||||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
Standalone local GREE air-conditioner controller written in Rust. It runs on a regular Linux host or an LXC container and provides a mobile-first web interface without depending on the vendor cloud.
|
Standalone local GREE air-conditioner controller written in Rust. It runs on a regular Linux host or an LXC container and provides a mobile-first web interface without depending on the vendor cloud.
|
||||||
|
|
||||||
Current version: **0.8.9**.
|
Current version: **0.8.10**.
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ See [`BUILD_REPORT.md`](BUILD_REPORT.md) for package validation details and [`do
|
|||||||
On Debian, Ubuntu or an LXC container:
|
On Debian, Ubuntu or an LXC container:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
unzip gree-controller-v0.8.9.zip
|
unzip gree-controller-v0.8.10.zip
|
||||||
cd gree-controller
|
cd gree-controller
|
||||||
chmod +x scripts/*.sh
|
chmod +x scripts/*.sh
|
||||||
./scripts/dev.sh
|
./scripts/dev.sh
|
||||||
@@ -481,6 +481,15 @@ Zones now use setpoint modulation, global seasonal house mode, cooling/heating C
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 0.8.10 Dashboard sections and notification filters
|
||||||
|
|
||||||
|
- Dashboard is split into three horizontal tabs: **Main**, **Thermostats** and **Manual control**. Each tab opens one normal vertically scrolling section instead of nested disclosure panels.
|
||||||
|
- Main keeps the summary, global power/house climate and control plan; Thermostats contains group controls and zone thermostats; Manual control contains direct unit controls.
|
||||||
|
- The top toolbar shows the live powered-unit count (`active/total`), and **Discover** moved from the global toolbar to the Devices page.
|
||||||
|
- Runtime control ownership timestamps now include both date and time.
|
||||||
|
- Notification settings now expose per-category delivery switches. Stale Home Assistant readings have their own `ha.sensor_stale` category and can be suppressed without disabling other sensor, communication, automation or thermostat alerts.
|
||||||
|
- Rotated the PWA cache key so upgraded clients fetch the new dashboard and settings UI.
|
||||||
|
|
||||||
## 0.8.9 Remote-takeover diagnostics
|
## 0.8.9 Remote-takeover diagnostics
|
||||||
|
|
||||||
- Reverted the v0.8.8 two-poll confirmation heuristic for unexpected GREE states; unexpected states outside the known controller-command/baseline settling envelope are again acted on immediately.
|
- Reverted the v0.8.8 two-poll confirmation heuristic for unexpected GREE states; unexpected states outside the known controller-command/baseline settling envelope are again acted on immediately.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "gree_controller",
|
"domain": "gree_controller",
|
||||||
"name": "GREE Controller",
|
"name": "GREE Controller",
|
||||||
"version": "0.8.9",
|
"version": "0.8.10",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
|
|||||||
+19
-3
@@ -46,7 +46,7 @@
|
|||||||
"dashboard.home": "Home",
|
"dashboard.home": "Home",
|
||||||
"dashboard.title": "Comfort under control",
|
"dashboard.title": "Comfort under control",
|
||||||
"dashboard.loading": "Loading devices…",
|
"dashboard.loading": "Loading devices…",
|
||||||
"dashboard.quickControl": "Quick control",
|
"dashboard.quickControl": "Manual control",
|
||||||
"dashboard.summary": "{online} of {total} devices online{active}.",
|
"dashboard.summary": "{online} of {total} devices online{active}.",
|
||||||
"dashboard.summaryActive": ", {count} running",
|
"dashboard.summaryActive": ", {count} running",
|
||||||
"dashboard.empty": "No devices. Start discovery or add a device manually.",
|
"dashboard.empty": "No devices. Start discovery or add a device manually.",
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
"actions.later": "Later",
|
"actions.later": "Later",
|
||||||
"discovery.nameDevices": "Name discovered devices",
|
"discovery.nameDevices": "Name discovered devices",
|
||||||
"discovery.nameDevicesHelp": "Give each new unit a friendly room name. The technical model and MAC remain available in diagnostics.",
|
"discovery.nameDevicesHelp": "Give each new unit a friendly room name. The technical model and MAC remain available in diagnostics.",
|
||||||
"dashboard.quickThermostats": "Quick thermostats",
|
"dashboard.quickThermostats": "Thermostats",
|
||||||
"common.unavailable": "Unavailable",
|
"common.unavailable": "Unavailable",
|
||||||
"mode.off": "Off",
|
"mode.off": "Off",
|
||||||
"house.seasonMode": "Home climate mode",
|
"house.seasonMode": "Home climate mode",
|
||||||
@@ -676,6 +676,22 @@
|
|||||||
"zones.temporaryInvalidStartAt": "Enter a valid start date and time",
|
"zones.temporaryInvalidStartAt": "Enter a valid start date and time",
|
||||||
"zones.temporaryInvalidUntil": "Enter a valid finish date and time",
|
"zones.temporaryInvalidUntil": "Enter a valid finish date and time",
|
||||||
"zones.temporaryEndAfterStart": "The finish time must be after Quick Thermostat starts",
|
"zones.temporaryEndAfterStart": "The finish time must be after Quick Thermostat starts",
|
||||||
"zones.automationToggleHint": "Enables or disables thermostat automation for this zone. This is not temporary control."
|
"zones.automationToggleHint": "Enables or disables thermostat automation for this zone. This is not temporary control.",
|
||||||
|
"dashboard.sections": "Dashboard sections",
|
||||||
|
"dashboard.tabMain": "Main",
|
||||||
|
"dashboard.tabThermostats": "Thermostats",
|
||||||
|
"dashboard.tabManual": "Manual control",
|
||||||
|
"dashboard.toolbarWorking": "{active}/{total} running",
|
||||||
|
"notifications.alertTypes": "Alert types",
|
||||||
|
"notifications.alertTypesHint": "Choose which alerts may be delivered. Thresholds above still define when an applicable alert is raised.",
|
||||||
|
"notifications.alertStaleSensor": "Stale Home Assistant sensor readings",
|
||||||
|
"notifications.alertSensorErrors": "Other Home Assistant sensor errors",
|
||||||
|
"notifications.alertCommunication": "Device communication problems",
|
||||||
|
"notifications.alertTargetTimeout": "Target temperature timeout",
|
||||||
|
"notifications.alertAutomation": "Automation errors and conflicts",
|
||||||
|
"notifications.alertControlErrors": "Thermostat and group control errors",
|
||||||
|
"notifications.alertImportantEvents": "Important state changes",
|
||||||
|
"notifications.alertOther": "Other warnings and errors",
|
||||||
|
"notifications.testSent": "Test notification sent"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-3
@@ -46,7 +46,7 @@
|
|||||||
"dashboard.home": "Dom",
|
"dashboard.home": "Dom",
|
||||||
"dashboard.title": "Komfort pod kontrolą",
|
"dashboard.title": "Komfort pod kontrolą",
|
||||||
"dashboard.loading": "Wczytywanie urządzeń…",
|
"dashboard.loading": "Wczytywanie urządzeń…",
|
||||||
"dashboard.quickControl": "Szybkie sterowanie",
|
"dashboard.quickControl": "Sterowanie ręczne",
|
||||||
"dashboard.summary": "{online} z {total} urządzeń online{active}.",
|
"dashboard.summary": "{online} z {total} urządzeń online{active}.",
|
||||||
"dashboard.summaryActive": ", {count} pracuje",
|
"dashboard.summaryActive": ", {count} pracuje",
|
||||||
"dashboard.empty": "Brak urządzeń. Uruchom wykrywanie lub dodaj urządzenie ręcznie.",
|
"dashboard.empty": "Brak urządzeń. Uruchom wykrywanie lub dodaj urządzenie ręcznie.",
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
"actions.later": "Później",
|
"actions.later": "Później",
|
||||||
"discovery.nameDevices": "Nazwij znalezione urządzenia",
|
"discovery.nameDevices": "Nazwij znalezione urządzenia",
|
||||||
"discovery.nameDevicesHelp": "Nadaj każdej nowej jednostce przyjazną nazwę pokoju. Model techniczny i MAC pozostaną dostępne w diagnostyce.",
|
"discovery.nameDevicesHelp": "Nadaj każdej nowej jednostce przyjazną nazwę pokoju. Model techniczny i MAC pozostaną dostępne w diagnostyce.",
|
||||||
"dashboard.quickThermostats": "Szybkie termostaty",
|
"dashboard.quickThermostats": "Termostaty",
|
||||||
"common.unavailable": "Niedostępne",
|
"common.unavailable": "Niedostępne",
|
||||||
"mode.off": "Wyłączone",
|
"mode.off": "Wyłączone",
|
||||||
"house.seasonMode": "Tryb klimatu domu",
|
"house.seasonMode": "Tryb klimatu domu",
|
||||||
@@ -676,6 +676,22 @@
|
|||||||
"zones.temporaryInvalidStartAt": "Podaj prawidłową datę i godzinę uruchomienia",
|
"zones.temporaryInvalidStartAt": "Podaj prawidłową datę i godzinę uruchomienia",
|
||||||
"zones.temporaryInvalidUntil": "Podaj prawidłową datę i godzinę zakończenia",
|
"zones.temporaryInvalidUntil": "Podaj prawidłową datę i godzinę zakończenia",
|
||||||
"zones.temporaryEndAfterStart": "Czas zakończenia musi przypadać po uruchomieniu szybkiego termostatu",
|
"zones.temporaryEndAfterStart": "Czas zakończenia musi przypadać po uruchomieniu szybkiego termostatu",
|
||||||
"zones.automationToggleHint": "Włącza lub wyłącza automatykę termostatu tej strefy. Nie jest to chwilowe sterowanie."
|
"zones.automationToggleHint": "Włącza lub wyłącza automatykę termostatu tej strefy. Nie jest to chwilowe sterowanie.",
|
||||||
|
"dashboard.sections": "Sekcje pulpitu",
|
||||||
|
"dashboard.tabMain": "Główna",
|
||||||
|
"dashboard.tabThermostats": "Termostaty",
|
||||||
|
"dashboard.tabManual": "Sterowanie ręczne",
|
||||||
|
"dashboard.toolbarWorking": "{active}/{total} pracuje",
|
||||||
|
"notifications.alertTypes": "Typy alertów",
|
||||||
|
"notifications.alertTypesHint": "Wybierz, które alerty mogą być wysyłane. Progi powyżej nadal określają, kiedy dany alarm powstaje.",
|
||||||
|
"notifications.alertStaleSensor": "Stare odczyty czujników Home Assistant",
|
||||||
|
"notifications.alertSensorErrors": "Pozostałe błędy czujników Home Assistant",
|
||||||
|
"notifications.alertCommunication": "Problemy komunikacji z urządzeniami",
|
||||||
|
"notifications.alertTargetTimeout": "Nieosiągnięcie temperatury w limicie czasu",
|
||||||
|
"notifications.alertAutomation": "Błędy i konflikty automatyzacji",
|
||||||
|
"notifications.alertControlErrors": "Błędy sterowania termostatami i grupami",
|
||||||
|
"notifications.alertImportantEvents": "Ważne zmiany stanu",
|
||||||
|
"notifications.alertOther": "Pozostałe ostrzeżenia i błędy",
|
||||||
|
"notifications.testSent": "Wysłano powiadomienie testowe"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2705,6 +2705,7 @@ fn public_settings(settings: &RuntimeSettings) -> Value {
|
|||||||
"cooldown_seconds": settings.notifications.cooldown_seconds,
|
"cooldown_seconds": settings.notifications.cooldown_seconds,
|
||||||
"communication_failure_threshold": settings.notifications.communication_failure_threshold,
|
"communication_failure_threshold": settings.notifications.communication_failure_threshold,
|
||||||
"target_timeout_minutes": settings.notifications.target_timeout_minutes,
|
"target_timeout_minutes": settings.notifications.target_timeout_minutes,
|
||||||
|
"alert_types": &settings.notifications.alert_types,
|
||||||
},
|
},
|
||||||
"influxdb": {
|
"influxdb": {
|
||||||
"enabled": settings.influxdb.enabled,
|
"enabled": settings.influxdb.enabled,
|
||||||
|
|||||||
+6
-1
@@ -1915,7 +1915,12 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
|||||||
}
|
}
|
||||||
Some((resolved_entity, Err(err))) => {
|
Some((resolved_entity, Err(err))) => {
|
||||||
if !matches!(previous_source.as_str(), "device_fallback" | "device_discrepancy_fallback") {
|
if !matches!(previous_source.as_str(), "device_fallback" | "device_discrepancy_fallback") {
|
||||||
state.log("warn", "ha.sensor_error", &err, json!({
|
let notification_kind = if err.contains("Home Assistant sensor is stale:") {
|
||||||
|
"ha.sensor_stale"
|
||||||
|
} else {
|
||||||
|
"ha.sensor_error"
|
||||||
|
};
|
||||||
|
state.log("warn", notification_kind, &err, json!({
|
||||||
"zone_id": zone.id,
|
"zone_id": zone.id,
|
||||||
"configured_entity_id": zone.ha_entity_id.as_deref(),
|
"configured_entity_id": zone.ha_entity_id.as_deref(),
|
||||||
"resolved_entity_id": resolved_entity,
|
"resolved_entity_id": resolved_entity,
|
||||||
|
|||||||
@@ -764,6 +764,43 @@ impl Default for InfluxDbSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NotificationAlertTypes {
|
||||||
|
/// Home Assistant sensor exceeded the configured freshness window.
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub stale_sensor: bool,
|
||||||
|
/// Other Home Assistant sensor errors (missing/unavailable/invalid value).
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub sensor_errors: bool,
|
||||||
|
/// Device connectivity, polling and communication problems.
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub communication: bool,
|
||||||
|
/// Zone failed to reach its target within the configured timeout.
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub target_timeout: bool,
|
||||||
|
/// Automation execution errors and conflicts.
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub automation: bool,
|
||||||
|
/// Thermostat/group/device control failures and sensor discrepancies.
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub control_errors: bool,
|
||||||
|
/// Informational state changes sent when notification mode is "important".
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub important_events: bool,
|
||||||
|
/// Any warning/error not matched by one of the categories above.
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub other: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NotificationAlertTypes {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
stale_sensor: true, sensor_errors: true, communication: true, target_timeout: true,
|
||||||
|
automation: true, control_errors: true, important_events: true, other: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct NotificationSettings {
|
pub struct NotificationSettings {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -788,6 +825,9 @@ pub struct NotificationSettings {
|
|||||||
pub communication_failure_threshold: u32,
|
pub communication_failure_threshold: u32,
|
||||||
#[serde(default = "default_notification_target_timeout")]
|
#[serde(default = "default_notification_target_timeout")]
|
||||||
pub target_timeout_minutes: u32,
|
pub target_timeout_minutes: u32,
|
||||||
|
/// Fine-grained selection of which alert categories may be delivered.
|
||||||
|
#[serde(default)]
|
||||||
|
pub alert_types: NotificationAlertTypes,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_notification_mode() -> String { "problems".into() }
|
fn default_notification_mode() -> String { "problems".into() }
|
||||||
@@ -805,6 +845,7 @@ impl Default for NotificationSettings {
|
|||||||
cooldown_seconds: default_notification_cooldown(),
|
cooldown_seconds: default_notification_cooldown(),
|
||||||
communication_failure_threshold: default_notification_failure_threshold(),
|
communication_failure_threshold: default_notification_failure_threshold(),
|
||||||
target_timeout_minutes: default_notification_target_timeout(),
|
target_timeout_minutes: default_notification_target_timeout(),
|
||||||
|
alert_types: NotificationAlertTypes::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-1
@@ -12,8 +12,25 @@ fn important_kind(kind: &str) -> bool {
|
|||||||
"device.communication_error")
|
"device.communication_error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn alert_type_enabled(cfg: &NotificationSettings, kind: &str) -> bool {
|
||||||
|
let types = &cfg.alert_types;
|
||||||
|
if kind == "ha.sensor_stale" { return types.stale_sensor; }
|
||||||
|
if kind.starts_with("ha.sensor_") { return types.sensor_errors; }
|
||||||
|
if matches!(kind, "device.offline" | "device.communication_error" | "device.command_unconfirmed") { return types.communication; }
|
||||||
|
if kind == "zone.target_timeout" { return types.target_timeout; }
|
||||||
|
if kind.starts_with("automation.") { return types.automation; }
|
||||||
|
if matches!(kind,
|
||||||
|
"zone.action_error" | "zone.mode_change_off_error" | "zone.local_power_error" |
|
||||||
|
"zone.device_missing" | "zone.sensor_discrepancy" |
|
||||||
|
"zone.temporary_quick_thermostat_cancelled" | "zone.temporary_quick_thermostat_poweroff_error" |
|
||||||
|
"group.power_error"
|
||||||
|
) { return types.control_errors; }
|
||||||
|
if important_kind(kind) { return types.important_events; }
|
||||||
|
types.other
|
||||||
|
}
|
||||||
|
|
||||||
fn should_send(cfg: &NotificationSettings, level: &str, kind: &str) -> bool {
|
fn should_send(cfg: &NotificationSettings, level: &str, kind: &str) -> bool {
|
||||||
if !cfg.enabled { return false; }
|
if !cfg.enabled || !alert_type_enabled(cfg, kind) { return false; }
|
||||||
if level == "error" || level == "warn" { return true; }
|
if level == "error" || level == "warn" { return true; }
|
||||||
cfg.mode == "important" && important_kind(kind)
|
cfg.mode == "important" && important_kind(kind)
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-7
@@ -28,7 +28,7 @@ const app = {
|
|||||||
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyLoading: false,
|
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyLoading: false,
|
||||||
customChartSeries: [], savedCharts: [], zoneControlSeq: {}, zoneTemperatureTimers: {},
|
customChartSeries: [], savedCharts: [], zoneControlSeq: {}, zoneTemperatureTimers: {},
|
||||||
controlPlan: null, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, sensorAliases: {},
|
controlPlan: null, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, sensorAliases: {},
|
||||||
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false,
|
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main',
|
||||||
};
|
};
|
||||||
try { app.savedCharts = JSON.parse(localStorage.getItem('gree_controller_saved_charts') || '[]'); } catch (_) { app.savedCharts = []; }
|
try { app.savedCharts = JSON.parse(localStorage.getItem('gree_controller_saved_charts') || '[]'); } catch (_) { app.savedCharts = []; }
|
||||||
|
|
||||||
@@ -335,6 +335,8 @@ function renderSummary() {
|
|||||||
const online = app.devices.filter(d => d.online).length;
|
const online = app.devices.filter(d => d.online).length;
|
||||||
const active = app.devices.filter(d => d.power).length;
|
const active = app.devices.filter(d => d.power).length;
|
||||||
const demand = app.zones.filter(z => z.enabled && z.demand).length;
|
const demand = app.zones.filter(z => z.enabled && z.demand).length;
|
||||||
|
const toolbarStatus = $('#toolbarRuntimeStatus');
|
||||||
|
if (toolbarStatus) toolbarStatus.textContent = tr('dashboard.toolbarWorking', {active, total:app.devices.length});
|
||||||
$('#heroTemperature').innerHTML = `${average === null ? '--' : average.toFixed(1)}<small>°C</small>`;
|
$('#heroTemperature').innerHTML = `${average === null ? '--' : average.toFixed(1)}<small>°C</small>`;
|
||||||
const activeSuffix = active ? tr('dashboard.summaryActive', {count: active}) : '';
|
const activeSuffix = active ? tr('dashboard.summaryActive', {count: active}) : '';
|
||||||
$('#summaryText').textContent = app.devices.length
|
$('#summaryText').textContent = app.devices.length
|
||||||
@@ -804,9 +806,9 @@ function zoneControlOwnerLabel(zone) {
|
|||||||
|
|
||||||
function zoneControlOwnerMeta(zone) {
|
function zoneControlOwnerMeta(zone) {
|
||||||
const parts=[];
|
const parts=[];
|
||||||
if(zone.control_since) parts.push(`${tr('zones.ownerSince')} ${new Date(zone.control_since).toLocaleTimeString(locale(),{hour:'2-digit',minute:'2-digit'})}`);
|
if(zone.control_since) parts.push(`${tr('zones.ownerSince')} ${dateTime(zone.control_since)}`);
|
||||||
if(zone.control_resume_at) parts.push(`${tr('zones.ownerResume')} ${new Date(zone.control_resume_at).toLocaleTimeString(locale(),{hour:'2-digit',minute:'2-digit'})}`);
|
if(zone.control_resume_at) parts.push(`${tr('zones.ownerResume')} ${dateTime(zone.control_resume_at)}`);
|
||||||
if(zone.lockout_until && new Date(zone.lockout_until)>new Date()) parts.push(`${tr('zones.lockoutUntil')} ${new Date(zone.lockout_until).toLocaleTimeString(locale(),{hour:'2-digit',minute:'2-digit'})}`);
|
if(zone.lockout_until && new Date(zone.lockout_until)>new Date()) parts.push(`${tr('zones.lockoutUntil')} ${dateTime(zone.lockout_until)}`);
|
||||||
return parts.join(' · ');
|
return parts.join(' · ');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1112,6 +1114,15 @@ function renderSettings() {
|
|||||||
form.slack_webhook_url.placeholder=n.slack_configured?'Saved - leave empty to keep':'https://hooks.slack.com/services/…';
|
form.slack_webhook_url.placeholder=n.slack_configured?'Saved - leave empty to keep':'https://hooks.slack.com/services/…';
|
||||||
form.discord_webhook_url.placeholder=n.discord_configured?'Saved - leave empty to keep':'https://discord.com/api/webhooks/…';
|
form.discord_webhook_url.placeholder=n.discord_configured?'Saved - leave empty to keep':'https://discord.com/api/webhooks/…';
|
||||||
form.notification_cooldown_seconds.value=n.cooldown_seconds||300; form.notification_failure_threshold.value=n.communication_failure_threshold||3; form.notification_target_timeout.value=n.target_timeout_minutes||60;
|
form.notification_cooldown_seconds.value=n.cooldown_seconds||300; form.notification_failure_threshold.value=n.communication_failure_threshold||3; form.notification_target_timeout.value=n.target_timeout_minutes||60;
|
||||||
|
const alerts=n.alert_types||{};
|
||||||
|
form.notification_alert_stale_sensor.checked=alerts.stale_sensor!==false;
|
||||||
|
form.notification_alert_sensor_errors.checked=alerts.sensor_errors!==false;
|
||||||
|
form.notification_alert_communication.checked=alerts.communication!==false;
|
||||||
|
form.notification_alert_target_timeout.checked=alerts.target_timeout!==false;
|
||||||
|
form.notification_alert_automation.checked=alerts.automation!==false;
|
||||||
|
form.notification_alert_control_errors.checked=alerts.control_errors!==false;
|
||||||
|
form.notification_alert_important_events.checked=alerts.important_events!==false;
|
||||||
|
form.notification_alert_other.checked=alerts.other!==false;
|
||||||
updateNotificationFields();
|
updateNotificationFields();
|
||||||
updateInfluxFields();
|
updateInfluxFields();
|
||||||
renderLogRetention();
|
renderLogRetention();
|
||||||
@@ -1226,12 +1237,29 @@ function updateBrowserUrl(path, replace=false) {
|
|||||||
history[replace ? 'replaceState' : 'pushState']({}, '', target);
|
history[replace ? 'replaceState' : 'pushState']({}, '', target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setDashboardTab(tab, {scroll=true}={}) {
|
||||||
|
const next = ['main','thermostats','manual'].includes(tab) ? tab : 'main';
|
||||||
|
app.dashboardTab = next;
|
||||||
|
$$('[data-dashboard-tab]').forEach(button => {
|
||||||
|
const active = button.dataset.dashboardTab === next;
|
||||||
|
button.classList.toggle('active', active);
|
||||||
|
button.setAttribute('aria-selected', String(active));
|
||||||
|
});
|
||||||
|
$$('[data-dashboard-panel]').forEach(panel => {
|
||||||
|
const active = panel.dataset.dashboardPanel === next;
|
||||||
|
panel.classList.toggle('active', active);
|
||||||
|
panel.hidden = !active;
|
||||||
|
});
|
||||||
|
if (scroll && app.currentView === 'dashboard') window.scrollTo({top:0, behavior:'smooth'});
|
||||||
|
}
|
||||||
|
|
||||||
function showView(name, {push=true, scroll=true}={}) {
|
function showView(name, {push=true, scroll=true}={}) {
|
||||||
app.currentView = name;
|
app.currentView = name;
|
||||||
$$('.view').forEach(view => view.classList.toggle('active', view.dataset.view === name));
|
$$('.view').forEach(view => view.classList.toggle('active', view.dataset.view === name));
|
||||||
$$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['groups','schedules','automations','simulation','night','homeassistant','settings','logs'].includes(name))));
|
$$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['groups','schedules','automations','simulation','night','homeassistant','settings','logs'].includes(name))));
|
||||||
if (push) updateBrowserUrl(name === 'history' ? currentHistoryPath() : (VIEW_ROUTES[name] || '/dashboard'));
|
if (push) updateBrowserUrl(name === 'history' ? currentHistoryPath() : (VIEW_ROUTES[name] || '/dashboard'));
|
||||||
if (scroll) window.scrollTo({top: 0, behavior: 'smooth'});
|
if (scroll) window.scrollTo({top: 0, behavior: 'smooth'});
|
||||||
|
if (name === 'dashboard') setDashboardTab(app.dashboardTab, {scroll:false});
|
||||||
if (name === 'history') { renderHistoryNavigation(); loadHistory(); }
|
if (name === 'history') { renderHistoryNavigation(); loadHistory(); }
|
||||||
if (name === 'logs') loadLogs();
|
if (name === 'logs') loadLogs();
|
||||||
}
|
}
|
||||||
@@ -1920,6 +1948,7 @@ function connectWebSocket() {
|
|||||||
|
|
||||||
document.addEventListener('click', async event => {
|
document.addEventListener('click', async event => {
|
||||||
const button = event.target.closest('button'); if (!button) return;
|
const button = event.target.closest('button'); if (!button) return;
|
||||||
|
if (button.dataset.dashboardTab) { setDashboardTab(button.dataset.dashboardTab); return; }
|
||||||
if (button.dataset.nav) {
|
if (button.dataset.nav) {
|
||||||
if (button.dataset.nav === 'more') openDialog('moreDialog'); else showView(button.dataset.nav);
|
if (button.dataset.nav === 'more') openDialog('moreDialog'); else showView(button.dataset.nav);
|
||||||
return;
|
return;
|
||||||
@@ -2015,9 +2044,8 @@ document.addEventListener('click', async event => {
|
|||||||
}
|
}
|
||||||
if (action === 'zone-go-control') {
|
if (action === 'zone-go-control') {
|
||||||
showView('dashboard', {scroll:false});
|
showView('dashboard', {scroll:false});
|
||||||
|
setDashboardTab('thermostats', {scroll:false});
|
||||||
const target = [...document.querySelectorAll('[data-zone-card]')].find(card => card.dataset.zoneCard === button.dataset.id);
|
const target = [...document.querySelectorAll('[data-zone-card]')].find(card => card.dataset.zoneCard === button.dataset.id);
|
||||||
const disclosure = target?.closest('details.dashboard-disclosure');
|
|
||||||
if (disclosure) disclosure.open = true;
|
|
||||||
requestAnimationFrame(() => target?.scrollIntoView({behavior:'smooth', block:'center'}));
|
requestAnimationFrame(() => target?.scrollIntoView({behavior:'smooth', block:'center'}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2064,7 +2092,7 @@ $('#simulationOpenTab')?.addEventListener('click', () => { const params=new URLS
|
|||||||
$('#simulationFullscreen')?.addEventListener('click', async () => { try { if(document.fullscreenElement) await document.exitFullscreen(); else await document.documentElement.requestFullscreen(); } catch(error) { toast(error.message, true); } });
|
$('#simulationFullscreen')?.addEventListener('click', async () => { try { if(document.fullscreenElement) await document.exitFullscreen(); else await document.documentElement.requestFullscreen(); } catch(error) { toast(error.message, true); } });
|
||||||
document.addEventListener('fullscreenchange', () => { const button=$('#simulationFullscreen'); if(button) button.textContent=tr(document.fullscreenElement?'simulation.exitFullscreen':'simulation.fullscreen'); });
|
document.addEventListener('fullscreenchange', () => { const button=$('#simulationFullscreen'); if(button) button.textContent=tr(document.fullscreenElement?'simulation.exitFullscreen':'simulation.fullscreen'); });
|
||||||
|
|
||||||
$('#testNotifications')?.addEventListener('click', async () => { const f=$('#settingsForm'); const saved=app.settings?.notifications||{}; const body={enabled:true,mode:f.notifications_mode.value,provider:f.notifications_provider.value,pushover_app_token:f.pushover_app_token.value.trim(),pushover_user_key:f.pushover_user_key.value.trim(),slack_webhook_url:f.slack_webhook_url.value.trim(),discord_webhook_url:f.discord_webhook_url.value.trim(),cooldown_seconds:Number(f.notification_cooldown_seconds.value||300),communication_failure_threshold:Number(f.notification_failure_threshold.value||3),target_timeout_minutes:Number(f.notification_target_timeout.value||60)}; try{await api('/api/integrations/notifications/test',{method:'POST',body});toast('Test notification sent');}catch(e){toast(e.message,true);} });
|
$('#testNotifications')?.addEventListener('click', async () => { const f=$('#settingsForm'); const body={enabled:true,mode:f.notifications_mode.value,provider:f.notifications_provider.value,pushover_app_token:f.pushover_app_token.value.trim(),pushover_user_key:f.pushover_user_key.value.trim(),slack_webhook_url:f.slack_webhook_url.value.trim(),discord_webhook_url:f.discord_webhook_url.value.trim(),cooldown_seconds:Number(f.notification_cooldown_seconds.value||300),communication_failure_threshold:Number(f.notification_failure_threshold.value||3),target_timeout_minutes:Number(f.notification_target_timeout.value||60),alert_types:{stale_sensor:f.notification_alert_stale_sensor.checked,sensor_errors:f.notification_alert_sensor_errors.checked,communication:f.notification_alert_communication.checked,target_timeout:f.notification_alert_target_timeout.checked,automation:f.notification_alert_automation.checked,control_errors:f.notification_alert_control_errors.checked,important_events:f.notification_alert_important_events.checked,other:f.notification_alert_other.checked}}; try{await api('/api/integrations/notifications/test',{method:'POST',body});toast(tr('notifications.testSent'));}catch(e){toast(e.message,true);} });
|
||||||
|
|
||||||
$('#copyZoneSettings')?.addEventListener('click',()=>{
|
$('#copyZoneSettings')?.addEventListener('click',()=>{
|
||||||
const f=$('#zoneForm');
|
const f=$('#zoneForm');
|
||||||
@@ -2279,6 +2307,16 @@ function currentSettingsBody() {
|
|||||||
enabled: !!settings.notifications?.enabled, mode: settings.notifications?.mode || 'problems', provider: settings.notifications?.provider || 'pushover',
|
enabled: !!settings.notifications?.enabled, mode: settings.notifications?.mode || 'problems', provider: settings.notifications?.provider || 'pushover',
|
||||||
pushover_app_token: '', pushover_user_key: '', slack_webhook_url: '', discord_webhook_url: '',
|
pushover_app_token: '', pushover_user_key: '', slack_webhook_url: '', discord_webhook_url: '',
|
||||||
cooldown_seconds: Number(settings.notifications?.cooldown_seconds || 300), communication_failure_threshold: Number(settings.notifications?.communication_failure_threshold || 3), target_timeout_minutes: Number(settings.notifications?.target_timeout_minutes || 60),
|
cooldown_seconds: Number(settings.notifications?.cooldown_seconds || 300), communication_failure_threshold: Number(settings.notifications?.communication_failure_threshold || 3), target_timeout_minutes: Number(settings.notifications?.target_timeout_minutes || 60),
|
||||||
|
alert_types: {
|
||||||
|
stale_sensor: settings.notifications?.alert_types?.stale_sensor !== false,
|
||||||
|
sensor_errors: settings.notifications?.alert_types?.sensor_errors !== false,
|
||||||
|
communication: settings.notifications?.alert_types?.communication !== false,
|
||||||
|
target_timeout: settings.notifications?.alert_types?.target_timeout !== false,
|
||||||
|
automation: settings.notifications?.alert_types?.automation !== false,
|
||||||
|
control_errors: settings.notifications?.alert_types?.control_errors !== false,
|
||||||
|
important_events: settings.notifications?.alert_types?.important_events !== false,
|
||||||
|
other: settings.notifications?.alert_types?.other !== false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
night_mode: {
|
night_mode: {
|
||||||
enabled: !!settings.night_mode?.enabled,
|
enabled: !!settings.night_mode?.enabled,
|
||||||
@@ -2343,6 +2381,16 @@ function settingsBodyFromForm(form) {
|
|||||||
cooldown_seconds: Number(raw.notification_cooldown_seconds || 300),
|
cooldown_seconds: Number(raw.notification_cooldown_seconds || 300),
|
||||||
communication_failure_threshold: Number(raw.notification_failure_threshold || 3),
|
communication_failure_threshold: Number(raw.notification_failure_threshold || 3),
|
||||||
target_timeout_minutes: Number(raw.notification_target_timeout || 60),
|
target_timeout_minutes: Number(raw.notification_target_timeout || 60),
|
||||||
|
alert_types: {
|
||||||
|
stale_sensor: form.notification_alert_stale_sensor.checked,
|
||||||
|
sensor_errors: form.notification_alert_sensor_errors.checked,
|
||||||
|
communication: form.notification_alert_communication.checked,
|
||||||
|
target_timeout: form.notification_alert_target_timeout.checked,
|
||||||
|
automation: form.notification_alert_automation.checked,
|
||||||
|
control_errors: form.notification_alert_control_errors.checked,
|
||||||
|
important_events: form.notification_alert_important_events.checked,
|
||||||
|
other: form.notification_alert_other.checked,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-32
@@ -16,6 +16,7 @@
|
|||||||
<div class="brand">
|
<div class="brand">
|
||||||
<div class="brand-line"><strong>GREE Controller</strong><small id="connectionLabel" class="connection-dot connecting" role="status" aria-label="Connecting" title="Connecting"></small></div>
|
<div class="brand-line"><strong>GREE Controller</strong><small id="connectionLabel" class="connection-dot connecting" role="status" aria-label="Connecting" title="Connecting"></small></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="toolbar-runtime-status" id="toolbarRuntimeStatus" aria-live="polite">0/0</div>
|
||||||
<div class="top-actions">
|
<div class="top-actions">
|
||||||
<select class="toolbar-select" id="languageSelect" data-i18n-aria="controls.language" aria-label="Language">
|
<select class="toolbar-select" id="languageSelect" data-i18n-aria="controls.language" aria-label="Language">
|
||||||
<option value="en">English</option>
|
<option value="en">English</option>
|
||||||
@@ -26,53 +27,70 @@
|
|||||||
<option value="dark" data-i18n="theme.dark">Dark</option>
|
<option value="dark" data-i18n="theme.dark">Dark</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="icon-button" id="refreshButton" data-i18n-title="actions.refresh" data-i18n-aria="actions.refresh" title="Refresh" aria-label="Refresh">↻</button>
|
<button class="icon-button" id="refreshButton" data-i18n-title="actions.refresh" data-i18n-aria="actions.refresh" title="Refresh" aria-label="Refresh">↻</button>
|
||||||
<button class="primary compact" id="discoverButton" data-i18n="actions.discover">Discover</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<section class="view active" data-view="dashboard">
|
<section class="view active" data-view="dashboard">
|
||||||
<div class="hero dashboard-summary-card">
|
<nav class="dashboard-tabs" data-i18n-aria="dashboard.sections" aria-label="Dashboard sections">
|
||||||
<div class="dashboard-summary-copy"><span class="eyebrow" data-i18n="dashboard.home">Home</span><h1 data-i18n="dashboard.title">Comfort under control</h1><p id="summaryText" data-i18n="dashboard.loading">Loading devices…</p></div>
|
<button type="button" class="active" data-dashboard-tab="main" data-i18n="dashboard.tabMain">Main</button>
|
||||||
<div class="dashboard-summary-status"><div class="metrics" id="metrics"></div><div class="hero-temp" id="heroTemperature">--<small>°C</small></div></div>
|
<button type="button" data-dashboard-tab="thermostats" data-i18n="dashboard.tabThermostats">Thermostats</button>
|
||||||
</div>
|
<button type="button" data-dashboard-tab="manual" data-i18n="dashboard.tabManual">Manual control</button>
|
||||||
<div class="panel dashboard-global-panel">
|
</nav>
|
||||||
<div class="dashboard-global-power">
|
|
||||||
<div><span class="eyebrow" data-i18n="house.powerAllEyebrow">Global power</span><h3 data-i18n="house.powerAllTitle">All air conditioners</h3><p data-i18n="house.powerAllHint">Master power for thermostat zones. Turn all off also disables all groups; turn all on starts enabled zones only.</p></div>
|
<div class="dashboard-tab-panel active" data-dashboard-panel="main">
|
||||||
<div class="house-power-actions"><button id="housePowerOn" type="button" class="secondary" data-action="house-power" data-value="true" data-i18n="house.powerOnAll">Turn all on</button><button id="housePowerOff" type="button" class="secondary" data-action="house-power" data-value="false" data-i18n="house.powerOffAll">Turn all off</button></div>
|
<div class="hero dashboard-summary-card">
|
||||||
|
<div class="dashboard-summary-copy"><span class="eyebrow" data-i18n="dashboard.home">Home</span><h1 data-i18n="dashboard.title">Comfort under control</h1><p id="summaryText" data-i18n="dashboard.loading">Loading devices…</p></div>
|
||||||
|
<div class="dashboard-summary-status"><div class="metrics" id="metrics"></div><div class="hero-temp" id="heroTemperature">--<small>°C</small></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-global-divider"></div>
|
<div class="panel dashboard-global-panel">
|
||||||
<div class="house-climate dashboard-house-climate" id="houseClimate"></div>
|
<div class="dashboard-global-power">
|
||||||
|
<div><span class="eyebrow" data-i18n="house.powerAllEyebrow">Global power</span><h3 data-i18n="house.powerAllTitle">All air conditioners</h3><p data-i18n="house.powerAllHint">Master power for thermostat zones. Turn all off also disables all groups; turn all on starts enabled zones only.</p></div>
|
||||||
|
<div class="house-power-actions"><button id="housePowerOn" type="button" class="secondary" data-action="house-power" data-value="true" data-i18n="house.powerOnAll">Turn all on</button><button id="housePowerOff" type="button" class="secondary" data-action="house-power" data-value="false" data-i18n="house.powerOffAll">Turn all off</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-global-divider"></div>
|
||||||
|
<div class="house-climate dashboard-house-climate" id="houseClimate"></div>
|
||||||
|
</div>
|
||||||
|
<section class="panel dashboard-section-panel">
|
||||||
|
<div class="dashboard-section-head compact-head">
|
||||||
|
<div><span class="eyebrow" data-i18n="plan.eyebrow">Control plan</span><h2 data-i18n="plan.title">What happens next</h2></div>
|
||||||
|
</div>
|
||||||
|
<div class="automation-plan-grid" id="controlPlan"></div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="dashboard-primary-section">
|
<div class="dashboard-tab-panel" data-dashboard-panel="thermostats" hidden>
|
||||||
<div class="dashboard-section-head">
|
<section class="dashboard-primary-section">
|
||||||
<div><span class="eyebrow" data-i18n="groups.eyebrow">Group control</span><h2 data-i18n="groups.dashboardTitle">Control selected air conditioners</h2><p data-i18n="groups.dashboardHint">Each group can use its own cooling/heating mode and Auto/Comfort/Sleep/Away profile.</p></div>
|
<div class="dashboard-section-head">
|
||||||
<div class="dashboard-section-actions"><span class="badge" id="dashboardGroupCount">0</span><button type="button" class="secondary" data-go="groups" data-i18n="groups.manage">Manage groups</button></div>
|
<div><span class="eyebrow" data-i18n="groups.eyebrow">Group control</span><h2 data-i18n="groups.dashboardTitle">Control selected air conditioners</h2><p data-i18n="groups.dashboardHint">Each group can use its own cooling/heating mode and Auto/Comfort/Sleep/Away profile.</p></div>
|
||||||
</div>
|
<div class="dashboard-section-actions"><span class="badge" id="dashboardGroupCount">0</span><button type="button" class="secondary" data-go="groups" data-i18n="groups.manage">Manage groups</button></div>
|
||||||
<div class="group-quick-grid dashboard-group-strip" id="dashboardGroups"></div>
|
</div>
|
||||||
</section>
|
<div class="group-quick-grid dashboard-group-strip" id="dashboardGroups"></div>
|
||||||
|
</section>
|
||||||
|
<section class="panel dashboard-section-panel">
|
||||||
|
<div class="dashboard-section-head compact-head">
|
||||||
|
<div><span class="eyebrow" data-i18n="nav.zones">Zones</span><h2 data-i18n="dashboard.quickThermostats">Thermostats</h2></div>
|
||||||
|
<span class="badge" id="dashboardZoneCount">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-grid dashboard-zones" id="dashboardZones"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-secondary-stack">
|
<div class="dashboard-tab-panel" data-dashboard-panel="manual" hidden>
|
||||||
<details class="panel dashboard-disclosure">
|
<section class="panel dashboard-section-panel">
|
||||||
<summary><div><span class="eyebrow" data-i18n="plan.eyebrow">Control plan</span><h3 data-i18n="plan.title">What happens next</h3></div><span class="dashboard-disclosure-chevron" aria-hidden="true">⌄</span></summary>
|
<div class="dashboard-section-head compact-head">
|
||||||
<div class="dashboard-disclosure-body"><div class="automation-plan-grid" id="controlPlan"></div></div>
|
<div><span class="eyebrow" data-i18n="nav.devices">Devices</span><h2 data-i18n="dashboard.quickControl">Manual control</h2></div>
|
||||||
</details>
|
<span class="badge" id="dashboardDeviceCount">0</span>
|
||||||
<details class="panel dashboard-disclosure">
|
</div>
|
||||||
<summary><div><span class="eyebrow" data-i18n="nav.zones">Zones</span><h3 data-i18n="dashboard.quickThermostats">Quick thermostats</h3></div><div class="dashboard-summary-side"><span class="badge" id="dashboardZoneCount">0</span><span class="dashboard-disclosure-chevron" aria-hidden="true">⌄</span></div></summary>
|
<div class="device-grid" id="dashboardDevices"></div>
|
||||||
<div class="dashboard-disclosure-body"><div class="list-grid dashboard-zones" id="dashboardZones"></div></div>
|
</section>
|
||||||
</details>
|
|
||||||
<details class="panel dashboard-disclosure">
|
|
||||||
<summary><div><span class="eyebrow" data-i18n="nav.devices">Devices</span><h3 data-i18n="dashboard.quickControl">Direct device control</h3></div><div class="dashboard-summary-side"><span class="badge" id="dashboardDeviceCount">0</span><span class="dashboard-disclosure-chevron" aria-hidden="true">⌄</span></div></summary>
|
|
||||||
<div class="dashboard-disclosure-body"><div class="device-grid" id="dashboardDevices"></div></div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="view" data-view="devices">
|
<section class="view" data-view="devices">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<div><span class="eyebrow" data-i18n="devices.lan">LAN network</span><h1 data-i18n="nav.devices">Devices</h1></div>
|
<div><span class="eyebrow" data-i18n="devices.lan">LAN network</span><h1 data-i18n="nav.devices">Devices</h1></div>
|
||||||
<button class="secondary" data-open="deviceDialog" data-i18n="devices.addManual">Add manually</button>
|
<div class="section-heading-actions"><button class="primary compact" id="discoverButton" data-i18n="actions.discover">Discover</button><button class="secondary" data-open="deviceDialog" data-i18n="devices.addManual">Add manually</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="device-grid" id="deviceList"></div>
|
<div class="device-grid" id="deviceList"></div>
|
||||||
</section>
|
</section>
|
||||||
@@ -237,6 +255,20 @@
|
|||||||
<label><span data-i18n="notifications.cooldown">Cooldown (s)</span><input type="number" name="notification_cooldown_seconds" min="30" max="86400" value="300"></label>
|
<label><span data-i18n="notifications.cooldown">Cooldown (s)</span><input type="number" name="notification_cooldown_seconds" min="30" max="86400" value="300"></label>
|
||||||
<label><span data-i18n="notifications.failureThreshold">Communication failures before alert</span><input type="number" name="notification_failure_threshold" min="2" max="100" value="3"></label>
|
<label><span data-i18n="notifications.failureThreshold">Communication failures before alert</span><input type="number" name="notification_failure_threshold" min="2" max="100" value="3"></label>
|
||||||
<label><span data-i18n="notifications.targetTimeout">Target not reached timeout (min)</span><input type="number" name="notification_target_timeout" min="5" max="1440" value="60"></label>
|
<label><span data-i18n="notifications.targetTimeout">Target not reached timeout (min)</span><input type="number" name="notification_target_timeout" min="5" max="1440" value="60"></label>
|
||||||
|
<fieldset class="notification-alert-types wide">
|
||||||
|
<legend data-i18n="notifications.alertTypes">Alert types</legend>
|
||||||
|
<p class="field-note" data-i18n="notifications.alertTypesHint">Choose exactly which alerts may be delivered. Thresholds above still define when applicable alerts are raised.</p>
|
||||||
|
<div class="notification-alert-grid">
|
||||||
|
<label class="check"><input type="checkbox" name="notification_alert_stale_sensor"> <span data-i18n="notifications.alertStaleSensor">Stale Home Assistant sensor readings</span></label>
|
||||||
|
<label class="check"><input type="checkbox" name="notification_alert_sensor_errors"> <span data-i18n="notifications.alertSensorErrors">Other Home Assistant sensor errors</span></label>
|
||||||
|
<label class="check"><input type="checkbox" name="notification_alert_communication"> <span data-i18n="notifications.alertCommunication">Device communication problems</span></label>
|
||||||
|
<label class="check"><input type="checkbox" name="notification_alert_target_timeout"> <span data-i18n="notifications.alertTargetTimeout">Target temperature timeout</span></label>
|
||||||
|
<label class="check"><input type="checkbox" name="notification_alert_automation"> <span data-i18n="notifications.alertAutomation">Automation errors and conflicts</span></label>
|
||||||
|
<label class="check"><input type="checkbox" name="notification_alert_control_errors"> <span data-i18n="notifications.alertControlErrors">Thermostat and group control errors</span></label>
|
||||||
|
<label class="check"><input type="checkbox" name="notification_alert_important_events"> <span data-i18n="notifications.alertImportantEvents">Important state changes</span></label>
|
||||||
|
<label class="check"><input type="checkbox" name="notification_alert_other"> <span data-i18n="notifications.alertOther">Other warnings and errors</span></label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
<div class="form-actions wide"><button type="button" class="secondary" id="testNotifications" data-i18n="notifications.test">Send test notification</button></div>
|
<div class="form-actions wide"><button type="button" class="secondary" id="testNotifications" data-i18n="notifications.test">Send test notification</button></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+32
-3
@@ -82,14 +82,15 @@ body { margin: 0; min-height: 100vh; color: var(--text); }
|
|||||||
button, input, select { font: inherit; }
|
button, input, select { font: inherit; }
|
||||||
button { cursor: pointer; }
|
button { cursor: pointer; }
|
||||||
button:disabled { opacity: .5; cursor: wait; }
|
button:disabled { opacity: .5; cursor: wait; }
|
||||||
.topbar { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; justify-content: space-between; gap: 1rem; min-height: 72px; padding: 12px max(18px, env(safe-area-inset-left)); border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--nav-bg) 94%, transparent); backdrop-filter: blur(14px); }
|
.topbar { position: sticky; top: 0; z-index: 20; display: grid; grid-template-columns: minmax(0,1fr) auto minmax(0,1fr); align-items: center; gap: 1rem; min-height: 72px; padding: 12px max(18px, env(safe-area-inset-left)); border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--nav-bg) 94%, transparent); backdrop-filter: blur(14px); }
|
||||||
.brand { display: flex; align-items: center; min-width: 0; }
|
.brand { display: flex; align-items: center; justify-self:start; min-width: 0; }
|
||||||
.brand strong, .brand small { display: block; }
|
.brand strong, .brand small { display: block; }
|
||||||
.brand strong { font-size: 15px; letter-spacing: -.01em; white-space: nowrap; }
|
.brand strong { font-size: 15px; letter-spacing: -.01em; white-space: nowrap; }
|
||||||
.brand small { margin-top: 2px; color: var(--muted); font-size: 11px; }
|
.brand small { margin-top: 2px; color: var(--muted); font-size: 11px; }
|
||||||
.brand.large { margin-bottom: 1rem; }
|
.brand.large { margin-bottom: 1rem; }
|
||||||
.brand.large strong { font-size: 21px; }
|
.brand.large strong { font-size: 21px; }
|
||||||
.top-actions { display: flex; align-items: center; justify-content: flex-end; gap: 7px; }
|
.top-actions { display: flex; align-items: center; justify-content: flex-end; justify-self:end; gap: 7px; }
|
||||||
|
.toolbar-runtime-status { justify-self:center; padding:7px 10px; border:1px solid var(--line); border-radius:999px; color:var(--text-soft); background:var(--surface-muted); font-size:11px; font-weight:750; font-variant-numeric:tabular-nums; white-space:nowrap; }
|
||||||
.toolbar-select { width: auto; min-width: 58px; min-height: 40px; padding: 7px 28px 7px 10px; border-radius: 11px; }
|
.toolbar-select { width: auto; min-width: 58px; min-height: 40px; padding: 7px 28px 7px 10px; border-radius: 11px; }
|
||||||
.theme-select { min-width: 94px; }
|
.theme-select { min-width: 94px; }
|
||||||
button { border: 0; border-radius: 13px; padding: 11px 15px; color: var(--text); background: var(--surface-2); transition: transform .15s ease, background .15s ease, opacity .15s ease; }
|
button { border: 0; border-radius: 13px; padding: 11px 15px; color: var(--text); background: var(--surface-2); transition: transform .15s ease, background .15s ease, opacity .15s ease; }
|
||||||
@@ -122,6 +123,7 @@ h3 { margin-bottom: 10px; }
|
|||||||
.metric span { display: block; margin-bottom: 8px; color: var(--muted); font-size: 12px; }
|
.metric span { display: block; margin-bottom: 8px; color: var(--muted); font-size: 12px; }
|
||||||
.metric strong { font-size: 24px; letter-spacing: -.03em; }
|
.metric strong { font-size: 24px; letter-spacing: -.03em; }
|
||||||
.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; margin: 8px 0 18px; }
|
.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; margin: 8px 0 18px; }
|
||||||
|
.section-heading-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; }
|
||||||
.device-grid, .list-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: 14px; }
|
.device-grid, .list-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: 14px; }
|
||||||
.device-card, .panel, .list-card { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
|
.device-card, .panel, .list-card { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
|
||||||
.device-card { overflow: hidden; padding: 20px; }
|
.device-card { overflow: hidden; padding: 20px; }
|
||||||
@@ -829,6 +831,33 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
|
|||||||
.temporary-active-summary .danger { width:100%; white-space:normal; }
|
.temporary-active-summary .danger { width:100%; white-space:normal; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dashboard sections: horizontal tabs, one vertically scrollable panel at a time. */
|
||||||
|
.dashboard-tabs { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; margin:0 0 12px; padding:6px; border:1px solid var(--line); border-radius:17px; background:var(--surface); }
|
||||||
|
.dashboard-tabs button { min-width:0; min-height:40px; padding:8px 10px; border:1px solid transparent; border-radius:12px; color:var(--muted); background:transparent; font-size:12px; font-weight:750; }
|
||||||
|
.dashboard-tabs button.active { border-color:var(--accent-border); color:var(--accent); background:var(--accent-soft); }
|
||||||
|
.dashboard-tab-panel[hidden] { display:none !important; }
|
||||||
|
.dashboard-tab-panel.active { display:block; animation:reveal .18s ease; }
|
||||||
|
.dashboard-section-panel { margin-bottom:18px; }
|
||||||
|
.dashboard-section-head.compact-head { align-items:center; margin-bottom:12px; }
|
||||||
|
.dashboard-section-head.compact-head h2 { font-size:19px; }
|
||||||
|
.dashboard-section-panel > .device-grid,
|
||||||
|
.dashboard-section-panel > .list-grid,
|
||||||
|
.dashboard-section-panel > .automation-plan-grid { margin-bottom:0; }
|
||||||
|
.notification-alert-types { display:grid; gap:10px; }
|
||||||
|
.notification-alert-types > .field-note { margin:0; }
|
||||||
|
.notification-alert-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:4px 14px; }
|
||||||
|
.notification-alert-grid .check { min-height:36px; }
|
||||||
|
|
||||||
|
@media (max-width:700px) {
|
||||||
|
.topbar { gap:7px; padding-inline:10px; }
|
||||||
|
.toolbar-runtime-status { padding:6px 8px; font-size:10px; }
|
||||||
|
.section-heading { align-items:stretch; flex-direction:column; }
|
||||||
|
.section-heading-actions { display:grid; grid-template-columns:1fr 1fr; width:100%; }
|
||||||
|
.dashboard-tabs { gap:5px; padding:5px; }
|
||||||
|
.dashboard-tabs button { min-height:42px; padding:6px 5px; font-size:10px; line-height:1.15; }
|
||||||
|
.notification-alert-grid { grid-template-columns:1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
/* 0.6.9 dashboard density and fit fixes */
|
/* 0.6.9 dashboard density and fit fixes */
|
||||||
[data-view="dashboard"] .dashboard-summary-card {
|
[data-view="dashboard"] .dashboard-summary-card {
|
||||||
min-height:0;
|
min-height:0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'gree-controller-v088-manual-takeover-confirmation';
|
const CACHE = 'gree-controller-v0810-dashboard-alert-filters';
|
||||||
const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
|
const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
|
||||||
const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/';
|
const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/';
|
||||||
const ASSETS = [path('/'), path('/styles.css'), path('/app.js'), path('/theme-init.js'), path('/favicon.svg'), path('/manifest.webmanifest'), path('/lang/index.json'), path('/lang/en.json')];
|
const ASSETS = [path('/'), path('/styles.css'), path('/app.js'), path('/theme-init.js'), path('/favicon.svg'), path('/manifest.webmanifest'), path('/lang/index.json'), path('/lang/en.json')];
|
||||||
|
|||||||
Reference in New Issue
Block a user