v0.14.11
This commit is contained in:
Generated
+1
-1
@@ -700,7 +700,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.14.10"
|
||||
version = "0.14.11"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.14.10"
|
||||
version = "0.14.11"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+24
-24
@@ -1,14 +1,14 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
9b08cf91cf8e9749d499af4675ae22f5f9dc8c987807f00fd712f8323afe4be8 ./Cargo.lock
|
||||
f99b0690cecca25d6ddae3d268d50383a13c9a6e6cf6f4880da40772f06488f9 ./Cargo.toml
|
||||
c9a8dafdb9328539fd9372533794010ed8870d0ccf0207e722a0dc02cc01cdfe ./Cargo.lock
|
||||
b4f4da4922f2c08642dfd41f6f0d99889ebe7b2c6437e48985cb2d7bb573feca ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
47c5c4b13cab9829e4593e86e1bed4d2ed232f96dc4df5fab0e639b65f21e795 ./README.md
|
||||
843141bfb0c2f5c13e1208d4a138c99c199e7388561b1ce087e381e61ce4d44c ./SOURCE_MANIFEST.sha256
|
||||
d4779b55a4e485344cbea50f413de7fd4624d85b2487d6a88ac448ea51ea2439 ./README.md
|
||||
721e840e5ef62b61e4be0a1c4a97ac1af97e0220305c8763b84b18b46443d8ab ./SOURCE_MANIFEST.sha256
|
||||
5302570299af2c2650bada12e123f598c5db8009949c75761385f73ed57343c5 ./build.rs
|
||||
27b27ce9a219b27898a5eae14087d392bd6c79a1f62e93da632283b3254412c7 ./docs/API.md
|
||||
0aeab38e1f3fc7fc126c625ac8ece370048da132411c3aed1bf4427a747befc6 ./docs/API.md
|
||||
f21d732eb1147f8f3167d2172cdf0b57e61edabdfecbaf97811d46c5bff7e47a ./docs/FLOW.md
|
||||
19b33362601a3cf2edaff08f7c8b8e7710710c010da6b8198c68b30af419682e ./docs/openapi.json
|
||||
91491953a94cdb4a573eab300b1c49b710df11467cb6e725427abbed22e61b64 ./docs/openapi.json
|
||||
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
|
||||
a18d2460340264d34c267fa704ee51a42e59bcc1783ee4c0dbb2a8ef42fa0f16 ./ha-addon/Dockerfile
|
||||
0417c88a990c72aecf071fba64d5d9da65dcda4d396cd7565477f7a4532c8c5e ./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
|
||||
67efa623556a2dbcf5afacc011ae8d4db07e401241f3c0804113f82be62ed402 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
|
||||
67147cb87c2a641385e02d7fa99237a3d590c4f305511eb04e422bfd539ef3dc ./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
|
||||
920319646a83ab11a74c2845b5c95b8126e4fbc81f67ba23051961a95cabdb84 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
9ca650cd8994d2de0ac0a8827c1f5db50f5b2c144e1f030d9078a7fa392cf470 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
3c06bd11671452ea6472afd5543fb91a39aba73a1d2d95c7762c04f94ab64030 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
|
||||
557ed503d299de0fc09ddb52242c5f525d4154aecb8b9073e6e67043e387cf88 ./ha-addon/repository/gree-controller/config.yaml
|
||||
a07b8c3d2afac7d80ccbd8c0c1673eb575c3658fe6bf0509200a3c2a1619293e ./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
|
||||
d13e4c2b993d209078342c0c86d852b300692089b377aa044f811308dd188a1e ./ha-addon/repository/gree-controller/translations/en.yaml
|
||||
@@ -85,7 +85,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
|
||||
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
|
||||
c9b30d870f44b558e75e49be0ee1377916c5c59e464adcc6561a315924e6a451 ./scripts/api_dev_test.py
|
||||
d4bbad174adb471745c5b09d234d02db65405444c20ed15be94524b44a2c5b76 ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
@@ -102,7 +102,7 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda
|
||||
3dfd1e053cbfa0d684dd88cf99b244a96a36e2ff23d2a0715b8327ab62258254 ./src/api/assets.rs
|
||||
05c255919f7b3d81188fc5f1410be6a0037471dfc44e93bffb009f29a1e6a3fc ./src/api/auth.rs
|
||||
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
|
||||
3da54a12f73faad90ff6a921ab544d01c15957d9c026363eb0ca47001594c60e ./src/api/configuration.rs
|
||||
11f4cbe485047dd3b723149d6cdae3f03232ae3d4c8a0c62fccaedb1eadfcf2c ./src/api/configuration.rs
|
||||
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
|
||||
32ade46ec45d6faf7852242b9f5ba1be6eacbacbeb33d2deabfab59bf1610ce0 ./src/api/device_groups.rs
|
||||
fcbb3da0538aab451f71810f829cc248d6bb67e586dc418d71a572980583823d ./src/api/devices.rs
|
||||
@@ -201,26 +201,26 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
1c206e7168770128f5afbb8a95f96e72ae2e9249a646f0651399ff71d6b1895f ./web/index.html
|
||||
22b3a1497fc17e72b60ecf0e79eb416108948678cc556018abd62bd02e24af64 ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
||||
27eff2821c40395369be15e197fa830535d2327fcb27c93a0e8e65a1c62de3ad ./web/js/bootstrap.js
|
||||
d3266522e02efa9f8e6eca6c15b53bc1114686aedcaac9e89a59be42b7145156 ./web/js/charts.js
|
||||
c4531f79ba2e15501407767a417085eab0a61bd280f8356b6960f6e320736b05 ./web/js/core.js
|
||||
f682b80c3b348a6e4832dc7d51227f696ab43b26d93f5ac52cabf35aed3c9ff8 ./web/js/dashboard.js
|
||||
574e2dda06bfa692832ad393a51964e8671a89d45ff4612f3c49e2218577e034 ./web/js/entities.js
|
||||
5970f3d05006ab975c7c88baaed4cc73eb014fc40dbfb3f07a191db3534211ea ./web/js/charts.js
|
||||
e1b6addca9512cf397111e6b554be0dcabe84196a539341eda7d6bdda7ff37a8 ./web/js/core.js
|
||||
c4ef56259052ec75d362fcffbb42cdd5062485a177108c175d89614527d421e6 ./web/js/dashboard.js
|
||||
c5476aaa91123bc9d2b0ab1cb2870cba04fd67ae0067f86b3edb415b76cd4864 ./web/js/entities.js
|
||||
a59fd5b822029d0c1e74ba8078f55f13122347e036a96dec383293ae386f6667 ./web/js/events.js
|
||||
0f63a4172fd76eed70f44ff1edbe9cdd0c20e31cfff5de15f17ed2e591fda9b4 ./web/js/flows.js
|
||||
63d508dc7bee43935086e8400366c271d7ebd9e8782e058296b449237399bf61 ./web/js/forms.js
|
||||
5ded2b5533700e266d30fc3cd3a0e8a46ec647140f16df5304eff513d6c72f82 ./web/js/history.js
|
||||
0dc879c0056251618633c9efd188fc1593eeae6965ad1bc993132dea6e171b44 ./web/js/main.js
|
||||
b08b0fe301642d1daa084b322a02e351d284d2465231505d45079a52ccb4028f ./web/js/flows.js
|
||||
8e6798405332eec8439c9ea28e967ee2b1d62b57012452c03089aff01dcd6452 ./web/js/forms.js
|
||||
593bba6062e39c22c8ed226ba238d02e026c451521c12aa188fcbd55c10cc64a ./web/js/history.js
|
||||
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js/main.js
|
||||
6bb9386707dcf601111fea15a1a366d8798313b973ca1624878618f4c3095add ./web/js/navigation.js
|
||||
219be50853c83c581cc6cf747af393a0de52eda1659092152c9ee78f36306c7b ./web/js/realtime.js
|
||||
799613c6d465cb2f92ecb57cb082d0c33729a03d88ab4ca7fa12a3cbd7e38e75 ./web/js/realtime.js
|
||||
9546ca5c5566994aff3c33d92295be46b65d51d874681764abe77470dc9c9c73 ./web/js/router.js
|
||||
b6c894e49e105c8afad8c5b21d322692cee6b3120ee10ef676bcd5e521c8956e ./web/js/select-ui.js
|
||||
cdc55061a41b7325aad552357d8359b42d881589c39b438cdc2a48bf0f690db9 ./web/js/settings-ui.js
|
||||
195e8a2ec5faa3da28e0c38e1eff429744139cb6fb859748cd931fb34378d4b4 ./web/js/select-ui.js
|
||||
c48b6101b203b40832976532d2f9c9633c0eaaf3b06cca43a0cfce1531a2b006 ./web/js/settings-ui.js
|
||||
f00c3f603eb314f755a0c46424681b579e0b890453e2550007c719273815147c ./web/js/settings.js
|
||||
3815fea7ae1694568c2f249bc8460e0563ced6bde89214f74f21b41fe8cad9a6 ./web/lang-init.js
|
||||
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
|
||||
98b2b3c78f7c8e6e284a80313131b9535497318fd75c62020f5615c4873879dd ./web/styles.css
|
||||
efea259fc5e6b078c4a0670cd6272ae86d7b7105f9be6ce449c4609ab64c1d7a ./web/styles.css
|
||||
fefdf12cbbb0a5d8a2cb89275cae28455aa42aaf179542305cc8f5e0f4825898 ./web/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
|
||||
|
||||
**Version: 0.14.10**
|
||||
**Version: 0.14.11**
|
||||
|
||||
Version 0.14.10 aligns the standalone Night mode and Home Assistant / Sensors pages with the full content width used by the rest of the Web UI. It also includes the 0.14.9 power-icon and Home Assistant sensor-log fixes.
|
||||
Version 0.14.11 replaces font-dependent Unicode UI icons with a shared inline SVG set across navigation, dialogs, Flow, charts, controls and event categories for consistent rendering across Android, iOS and desktop browsers. It also includes the 0.14.10 full-width settings layout fix.
|
||||
|
||||
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
|
||||
|
||||
|
||||
+23
-23
@@ -1,13 +1,13 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
9b08cf91cf8e9749d499af4675ae22f5f9dc8c987807f00fd712f8323afe4be8 ./Cargo.lock
|
||||
f99b0690cecca25d6ddae3d268d50383a13c9a6e6cf6f4880da40772f06488f9 ./Cargo.toml
|
||||
c9a8dafdb9328539fd9372533794010ed8870d0ccf0207e722a0dc02cc01cdfe ./Cargo.lock
|
||||
b4f4da4922f2c08642dfd41f6f0d99889ebe7b2c6437e48985cb2d7bb573feca ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
47c5c4b13cab9829e4593e86e1bed4d2ed232f96dc4df5fab0e639b65f21e795 ./README.md
|
||||
d4779b55a4e485344cbea50f413de7fd4624d85b2487d6a88ac448ea51ea2439 ./README.md
|
||||
5302570299af2c2650bada12e123f598c5db8009949c75761385f73ed57343c5 ./build.rs
|
||||
27b27ce9a219b27898a5eae14087d392bd6c79a1f62e93da632283b3254412c7 ./docs/API.md
|
||||
0aeab38e1f3fc7fc126c625ac8ece370048da132411c3aed1bf4427a747befc6 ./docs/API.md
|
||||
f21d732eb1147f8f3167d2172cdf0b57e61edabdfecbaf97811d46c5bff7e47a ./docs/FLOW.md
|
||||
19b33362601a3cf2edaff08f7c8b8e7710710c010da6b8198c68b30af419682e ./docs/openapi.json
|
||||
91491953a94cdb4a573eab300b1c49b710df11467cb6e725427abbed22e61b64 ./docs/openapi.json
|
||||
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
|
||||
a18d2460340264d34c267fa704ee51a42e59bcc1783ee4c0dbb2a8ef42fa0f16 ./ha-addon/Dockerfile
|
||||
0417c88a990c72aecf071fba64d5d9da65dcda4d396cd7565477f7a4532c8c5e ./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
|
||||
67efa623556a2dbcf5afacc011ae8d4db07e401241f3c0804113f82be62ed402 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
|
||||
67147cb87c2a641385e02d7fa99237a3d590c4f305511eb04e422bfd539ef3dc ./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
|
||||
920319646a83ab11a74c2845b5c95b8126e4fbc81f67ba23051961a95cabdb84 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
9ca650cd8994d2de0ac0a8827c1f5db50f5b2c144e1f030d9078a7fa392cf470 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
3c06bd11671452ea6472afd5543fb91a39aba73a1d2d95c7762c04f94ab64030 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
|
||||
557ed503d299de0fc09ddb52242c5f525d4154aecb8b9073e6e67043e387cf88 ./ha-addon/repository/gree-controller/config.yaml
|
||||
a07b8c3d2afac7d80ccbd8c0c1673eb575c3658fe6bf0509200a3c2a1619293e ./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
|
||||
d13e4c2b993d209078342c0c86d852b300692089b377aa044f811308dd188a1e ./ha-addon/repository/gree-controller/translations/en.yaml
|
||||
@@ -84,7 +84,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
|
||||
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
|
||||
c9b30d870f44b558e75e49be0ee1377916c5c59e464adcc6561a315924e6a451 ./scripts/api_dev_test.py
|
||||
d4bbad174adb471745c5b09d234d02db65405444c20ed15be94524b44a2c5b76 ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
@@ -101,7 +101,7 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda
|
||||
3dfd1e053cbfa0d684dd88cf99b244a96a36e2ff23d2a0715b8327ab62258254 ./src/api/assets.rs
|
||||
05c255919f7b3d81188fc5f1410be6a0037471dfc44e93bffb009f29a1e6a3fc ./src/api/auth.rs
|
||||
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
|
||||
3da54a12f73faad90ff6a921ab544d01c15957d9c026363eb0ca47001594c60e ./src/api/configuration.rs
|
||||
11f4cbe485047dd3b723149d6cdae3f03232ae3d4c8a0c62fccaedb1eadfcf2c ./src/api/configuration.rs
|
||||
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
|
||||
32ade46ec45d6faf7852242b9f5ba1be6eacbacbeb33d2deabfab59bf1610ce0 ./src/api/device_groups.rs
|
||||
fcbb3da0538aab451f71810f829cc248d6bb67e586dc418d71a572980583823d ./src/api/devices.rs
|
||||
@@ -200,26 +200,26 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
1c206e7168770128f5afbb8a95f96e72ae2e9249a646f0651399ff71d6b1895f ./web/index.html
|
||||
22b3a1497fc17e72b60ecf0e79eb416108948678cc556018abd62bd02e24af64 ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
||||
27eff2821c40395369be15e197fa830535d2327fcb27c93a0e8e65a1c62de3ad ./web/js/bootstrap.js
|
||||
d3266522e02efa9f8e6eca6c15b53bc1114686aedcaac9e89a59be42b7145156 ./web/js/charts.js
|
||||
c4531f79ba2e15501407767a417085eab0a61bd280f8356b6960f6e320736b05 ./web/js/core.js
|
||||
f682b80c3b348a6e4832dc7d51227f696ab43b26d93f5ac52cabf35aed3c9ff8 ./web/js/dashboard.js
|
||||
574e2dda06bfa692832ad393a51964e8671a89d45ff4612f3c49e2218577e034 ./web/js/entities.js
|
||||
5970f3d05006ab975c7c88baaed4cc73eb014fc40dbfb3f07a191db3534211ea ./web/js/charts.js
|
||||
e1b6addca9512cf397111e6b554be0dcabe84196a539341eda7d6bdda7ff37a8 ./web/js/core.js
|
||||
c4ef56259052ec75d362fcffbb42cdd5062485a177108c175d89614527d421e6 ./web/js/dashboard.js
|
||||
c5476aaa91123bc9d2b0ab1cb2870cba04fd67ae0067f86b3edb415b76cd4864 ./web/js/entities.js
|
||||
a59fd5b822029d0c1e74ba8078f55f13122347e036a96dec383293ae386f6667 ./web/js/events.js
|
||||
0f63a4172fd76eed70f44ff1edbe9cdd0c20e31cfff5de15f17ed2e591fda9b4 ./web/js/flows.js
|
||||
63d508dc7bee43935086e8400366c271d7ebd9e8782e058296b449237399bf61 ./web/js/forms.js
|
||||
5ded2b5533700e266d30fc3cd3a0e8a46ec647140f16df5304eff513d6c72f82 ./web/js/history.js
|
||||
0dc879c0056251618633c9efd188fc1593eeae6965ad1bc993132dea6e171b44 ./web/js/main.js
|
||||
b08b0fe301642d1daa084b322a02e351d284d2465231505d45079a52ccb4028f ./web/js/flows.js
|
||||
8e6798405332eec8439c9ea28e967ee2b1d62b57012452c03089aff01dcd6452 ./web/js/forms.js
|
||||
593bba6062e39c22c8ed226ba238d02e026c451521c12aa188fcbd55c10cc64a ./web/js/history.js
|
||||
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js/main.js
|
||||
6bb9386707dcf601111fea15a1a366d8798313b973ca1624878618f4c3095add ./web/js/navigation.js
|
||||
219be50853c83c581cc6cf747af393a0de52eda1659092152c9ee78f36306c7b ./web/js/realtime.js
|
||||
799613c6d465cb2f92ecb57cb082d0c33729a03d88ab4ca7fa12a3cbd7e38e75 ./web/js/realtime.js
|
||||
9546ca5c5566994aff3c33d92295be46b65d51d874681764abe77470dc9c9c73 ./web/js/router.js
|
||||
b6c894e49e105c8afad8c5b21d322692cee6b3120ee10ef676bcd5e521c8956e ./web/js/select-ui.js
|
||||
cdc55061a41b7325aad552357d8359b42d881589c39b438cdc2a48bf0f690db9 ./web/js/settings-ui.js
|
||||
195e8a2ec5faa3da28e0c38e1eff429744139cb6fb859748cd931fb34378d4b4 ./web/js/select-ui.js
|
||||
c48b6101b203b40832976532d2f9c9633c0eaaf3b06cca43a0cfce1531a2b006 ./web/js/settings-ui.js
|
||||
f00c3f603eb314f755a0c46424681b579e0b890453e2550007c719273815147c ./web/js/settings.js
|
||||
3815fea7ae1694568c2f249bc8460e0563ced6bde89214f74f21b41fe8cad9a6 ./web/lang-init.js
|
||||
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
|
||||
98b2b3c78f7c8e6e284a80313131b9535497318fd75c62020f5615c4873879dd ./web/styles.css
|
||||
efea259fc5e6b078c4a0670cd6272ae86d7b7105f9be6ce449c4609ab64c1d7a ./web/styles.css
|
||||
fefdf12cbbb0a5d8a2cb89275cae28455aa42aaf179542305cc8f5e0f4825898 ./web/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# GREE Controller API reference
|
||||
|
||||
HTTP and WebSocket API for GREE Controller **0.14.10**.
|
||||
HTTP and WebSocket API for GREE Controller **0.14.11**.
|
||||
|
||||
[← Main documentation](../README.md)
|
||||
|
||||
@@ -233,7 +233,7 @@ Response:
|
||||
{
|
||||
"status": "ok",
|
||||
"name": "gree-controller",
|
||||
"version": "0.14.10",
|
||||
"version": "0.14.11",
|
||||
"uptime_seconds": 1234,
|
||||
"control_ready": true,
|
||||
"time": "2026-08-30T06:54:00Z"
|
||||
@@ -296,7 +296,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.10",
|
||||
"version": "0.14.11",
|
||||
"uptime_seconds": 1234,
|
||||
"auth_required": false,
|
||||
"control_ready": true,
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "GREE Controller API",
|
||||
"version": "0.14.10",
|
||||
"version": "0.14.11",
|
||||
"summary": "Local HTTP/WebSocket API for GREE HVAC control",
|
||||
"description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nAdministrator endpoints accept `Authorization: Bearer <APP_TOKEN>` or `x-api-token: <APP_TOKEN>` when configured. Restricted Home Assistant control endpoints also accept generated integration tokens.\n\n### Local and GREE Cloud\nDevices use one API model and one command surface. `connection_type=local` is dispatched only to the existing LAN/UDP provider; `connection_type=gree_cloud` is dispatched only to the GREE Cloud provider. No automatic transport fallback is performed.\n\n### GREE Cloud security\nCloud passwords, REST/MQTT tokens, Authorization values and device cipher keys are never returned in normal API responses or Cloud diagnostics. MQTT uses TLS with certificate and hostname validation.\n\n### Energy\nGREE Cloud `ElcAll` and selected Home Assistant cumulative energy sensors are normalized to kWh and converted to consumption deltas before aggregation. Raw cumulative values are retained only as meter samples/baselines.\n\n### 0.14.10\nNight mode and Home Assistant / Sensors standalone settings now use the full available page width, matching the other Web UI sections.\n\n### 0.14.9\nPower controls use an embedded SVG icon instead of a font-dependent Unicode glyph. Home Assistant room-sensor stale/error events identify the affected entity and zone, and historical event rows can recover that context from metadata.\n\n### 0.14.8\nRemaining Home Assistant and notification diagnostic errors no longer use device-transport 502 responses. Missing/invalid integration configuration returns 400, configured external dependency failures return 424, and 502 is reserved for Local/LAN GREE device communication.\n\n### 0.14.7\nHome Assistant energy-sensor discovery now returns an empty optional result when HA is not configured, uses 424 for configured external-dependency failures, and custom select popovers use their rendered height when positioning above a field.\n\n\n### 0.14.6\nFixes dynamic custom-select duplication in History → Energy and hides Energy history controls when no energy source is configured.\n\n### 0.14.5\nWeb UI single-choice controls now use one consistent custom dropdown, language packs are loaded on demand, and the dashboard outdoor temperature opens a 24-hour history modal.\n\n### 0.14.4\nHome Assistant connection tests validate only the configured server/API token and require no entity. `outdoor_entity_id` is the global Home Assistant outdoor-temperature sensor; zones can override it with `ha_outdoor_entity_id`. Room-temperature Home Assistant sensors remain configured per zone with `ha_entity_id`. Global/per-zone outdoor sensors are integrated with aliases, metrics/history and Visual Flow entity suggestions.\n\n### 0.14.2\nAdds independent vertical/horizontal swing controls across manual device control, thermostat zones, legacy automations, Visual Flow and Home Assistant zone climates. Swing is treated as an auxiliary one-shot unit setting and does not replace thermostat ownership.\n\n### 0.14.1\nAdds split/multisplit installation groups with shared energy and outdoor-temperature sources, multi-target energy charts with period comparison, cleaner Cloud status/alerts and safe MQTT cleanup after the last Cloud device is removed.\n\n### 0.14.0\nAdds first-class GREE Cloud MQTT status/control, reconnect/resubscribe lifecycle, Cloud diagnostics, per-device capabilities, Cloud/HA energy sources and energy-history aggregation while preserving the LAN transport contract.",
|
||||
"description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nAdministrator endpoints accept `Authorization: Bearer <APP_TOKEN>` or `x-api-token: <APP_TOKEN>` when configured. Restricted Home Assistant control endpoints also accept generated integration tokens.\n\n### Local and GREE Cloud\nDevices use one API model and one command surface. `connection_type=local` is dispatched only to the existing LAN/UDP provider; `connection_type=gree_cloud` is dispatched only to the GREE Cloud provider. No automatic transport fallback is performed.\n\n### GREE Cloud security\nCloud passwords, REST/MQTT tokens, Authorization values and device cipher keys are never returned in normal API responses or Cloud diagnostics. MQTT uses TLS with certificate and hostname validation.\n\n### Energy\nGREE Cloud `ElcAll` and selected Home Assistant cumulative energy sensors are normalized to kWh and converted to consumption deltas before aggregation. Raw cumulative values are retained only as meter samples/baselines.\n\n### 0.14.11\nWeb UI icons now use a shared inline SVG set instead of font-dependent Unicode glyphs across navigation, dialogs, Flow, charts, controls and event categories for consistent cross-platform rendering.\n\n\n### 0.14.10\nNight mode and Home Assistant / Sensors standalone settings now use the full available page width, matching the other Web UI sections.\n\n### 0.14.9\nPower controls use an embedded SVG icon instead of a font-dependent Unicode glyph. Home Assistant room-sensor stale/error events identify the affected entity and zone, and historical event rows can recover that context from metadata.\n\n### 0.14.8\nRemaining Home Assistant and notification diagnostic errors no longer use device-transport 502 responses. Missing/invalid integration configuration returns 400, configured external dependency failures return 424, and 502 is reserved for Local/LAN GREE device communication.\n\n### 0.14.7\nHome Assistant energy-sensor discovery now returns an empty optional result when HA is not configured, uses 424 for configured external-dependency failures, and custom select popovers use their rendered height when positioning above a field.\n\n\n### 0.14.6\nFixes dynamic custom-select duplication in History → Energy and hides Energy history controls when no energy source is configured.\n\n### 0.14.5\nWeb UI single-choice controls now use one consistent custom dropdown, language packs are loaded on demand, and the dashboard outdoor temperature opens a 24-hour history modal.\n\n### 0.14.4\nHome Assistant connection tests validate only the configured server/API token and require no entity. `outdoor_entity_id` is the global Home Assistant outdoor-temperature sensor; zones can override it with `ha_outdoor_entity_id`. Room-temperature Home Assistant sensors remain configured per zone with `ha_entity_id`. Global/per-zone outdoor sensors are integrated with aliases, metrics/history and Visual Flow entity suggestions.\n\n### 0.14.2\nAdds independent vertical/horizontal swing controls across manual device control, thermostat zones, legacy automations, Visual Flow and Home Assistant zone climates. Swing is treated as an auxiliary one-shot unit setting and does not replace thermostat ownership.\n\n### 0.14.1\nAdds split/multisplit installation groups with shared energy and outdoor-temperature sources, multi-target energy charts with period comparison, cleaner Cloud status/alerts and safe MQTT cleanup after the last Cloud device is removed.\n\n### 0.14.0\nAdds first-class GREE Cloud MQTT status/control, reconnect/resubscribe lifecycle, Cloud diagnostics, per-device capabilities, Cloud/HA energy sources and energy-history aggregation while preserving the LAN transport contract.",
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
}
|
||||
@@ -5520,7 +5520,7 @@
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "0.14.10"
|
||||
"example": "0.14.11"
|
||||
},
|
||||
"uptime_seconds": {
|
||||
"type": "integer",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.14.10",
|
||||
"version": "0.14.11",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 0.14.11
|
||||
|
||||
- Replaces font-dependent Unicode glyphs used as Web UI icons with a shared inline SVG icon set.
|
||||
- Covers navigation, theme/refresh controls, dialogs, menus, Flow editor/statuses, chart zoom/fullscreen controls, thermostat/device +/- controls, custom-select checks/chevrons and event-category icons.
|
||||
- Leaves semantic text symbols such as `°C`, `±`, comparison operators, mathematical multiplication and Flow expression notation as text.
|
||||
- Keeps the 0.14.10 full-width Night mode and Home Assistant / Sensors layout fix.
|
||||
|
||||
## 0.14.10
|
||||
|
||||
- Makes the standalone Night mode and Home Assistant / Sensors forms use the full available page width, matching the other application sections on desktop and responsive layouts.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: "GREE Controller"
|
||||
version: "0.14.10"
|
||||
version: "0.14.11"
|
||||
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/"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.14.10.
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.14.11.
|
||||
|
||||
Default mode is read-only and safe to run against a real controller.
|
||||
Use --settings-write to additionally round-trip all split settings resources and
|
||||
@@ -26,7 +26,7 @@ from typing import Any, Callable, Iterable, Optional
|
||||
|
||||
DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787")
|
||||
DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "")
|
||||
DEFAULT_EXPECTED_VERSION = "0.14.10"
|
||||
DEFAULT_EXPECTED_VERSION = "0.14.11"
|
||||
|
||||
SETTINGS_PATHS: dict[str, set[str]] = {
|
||||
"/api/settings/application": {"simulator_enabled"},
|
||||
@@ -593,7 +593,7 @@ def run_suite(args: argparse.Namespace) -> int:
|
||||
print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n")
|
||||
|
||||
runner.run("public health", lambda: test_health(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.14.10 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.14.11 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client))
|
||||
runner.run("protected API requires auth", lambda: test_protected_auth(client))
|
||||
|
||||
@@ -662,7 +662,7 @@ def run_suite(args: argparse.Namespace) -> int:
|
||||
|
||||
def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Developer smoke/integration tests for GREE Controller API 0.14.10.",
|
||||
description="Developer smoke/integration tests for GREE Controller API 0.14.11.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""Examples:
|
||||
python3 scripts/api_dev_test.py
|
||||
|
||||
@@ -27,7 +27,7 @@ async fn export_configuration(
|
||||
|
||||
fn validate_configuration_header(export: &ConfigurationExport) -> Result<(), AppError> {
|
||||
if export.format_version != 3 {
|
||||
return Err(AppError::BadRequest("unsupported configuration export version; version 3 is required by GREE Controller 0.14.10".into()));
|
||||
return Err(AppError::BadRequest("unsupported configuration export version; version 3 is required by GREE Controller 0.14.11".into()));
|
||||
}
|
||||
if export.settings.control_strategy != "setpoint" {
|
||||
return Err(AppError::BadRequest(
|
||||
|
||||
+63
-62
@@ -24,15 +24,15 @@
|
||||
<div class="top-actions">
|
||||
<div class="toolbar-picker" data-i18n-title="controls.language" title="Language">
|
||||
<span class="toolbar-picker-value" id="languageLabel" aria-hidden="true">EN</span>
|
||||
<span class="toolbar-picker-chevron" aria-hidden="true">⌄</span>
|
||||
<span class="toolbar-picker-chevron" data-ui-icon="chevron-down" aria-hidden="true"></span>
|
||||
<select class="toolbar-picker-select" id="languageSelect" data-i18n-aria="controls.language"
|
||||
aria-label="Language">
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="toolbar-picker" data-i18n-title="controls.theme" title="Theme">
|
||||
<span class="toolbar-picker-value theme-picker-value" id="themeIcon" aria-hidden="true">◐</span>
|
||||
<span class="toolbar-picker-chevron" aria-hidden="true">⌄</span>
|
||||
<span class="toolbar-picker-value theme-picker-value" id="themeIcon" data-ui-icon="system" aria-hidden="true"></span>
|
||||
<span class="toolbar-picker-chevron" data-ui-icon="chevron-down" aria-hidden="true"></span>
|
||||
<select class="toolbar-picker-select" id="themeSelect" data-i18n-aria="controls.theme" aria-label="Theme">
|
||||
<option value="system" data-i18n="theme.system">System</option>
|
||||
<option value="light" data-i18n="theme.light">Light</option>
|
||||
@@ -40,7 +40,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<button class="icon-button toolbar-icon-control" id="refreshButton" data-i18n-title="actions.refresh"
|
||||
data-i18n-aria="actions.refresh" title="Refresh" aria-label="Refresh">↻</button>
|
||||
data-i18n-aria="actions.refresh" title="Refresh" aria-label="Refresh" data-ui-icon="refresh"></button>
|
||||
</div>
|
||||
<div class="connection-banner" id="connectionBanner" role="status" aria-live="polite" hidden>
|
||||
<span class="connection-banner-dot" aria-hidden="true"></span>
|
||||
@@ -844,14 +844,14 @@
|
||||
<section id="flowEditor" class="flow-editor" hidden aria-label="Flow editor">
|
||||
<header class="flow-editor-bar">
|
||||
<div class="flow-editor-title"><button type="button" class="icon-button" data-action="close-flow-editor"
|
||||
aria-label="Back">←</button>
|
||||
aria-label="Back" data-ui-icon="arrow-left"></button>
|
||||
<div><span class="eyebrow" data-i18n="flow.editorEyebrow">Flow editor</span>
|
||||
<div class="flow-name-wrap">
|
||||
<div id="flowNameView" class="flow-name-view" hidden><button id="flowNameText" class="flow-name-text"
|
||||
type="button" data-action="edit-flow-name" data-i18n-title="flow.editName"
|
||||
title="Edit Flow name"></button><button class="flow-name-edit" type="button"
|
||||
data-action="edit-flow-name" data-i18n-title="flow.editName" data-i18n-aria="flow.editName"
|
||||
title="Edit Flow name" aria-label="Edit Flow name">✎</button></div><input id="flowName" maxlength="100"
|
||||
title="Edit Flow name" aria-label="Edit Flow name" data-ui-icon="edit"></button></div><input id="flowName" maxlength="100"
|
||||
data-i18n-placeholder="flow.namePlaceholder" data-i18n-title="flow.nameAliasHint"
|
||||
placeholder="My automation">
|
||||
</div>
|
||||
@@ -929,7 +929,7 @@
|
||||
<div class="flow-workspace-wrap">
|
||||
<div class="flow-workspace-toolbar">
|
||||
<div class="flow-workspace-primary-actions"><button type="button" class="primary flow-add-block-button"
|
||||
data-action="flow-add-block"><span aria-hidden="true">+</span><span data-i18n="flow.addBlock">Add
|
||||
data-action="flow-add-block"><span data-ui-icon="plus" aria-hidden="true"></span><span data-i18n="flow.addBlock">Add
|
||||
block</span></button><button type="button" class="secondary flow-selection-action"
|
||||
data-action="flow-select-all" data-i18n="flow.selectAll">Zaznacz wszystko</button><button type="button"
|
||||
class="secondary flow-selection-action" data-action="flow-clear-selection"
|
||||
@@ -937,11 +937,12 @@
|
||||
class="badge"></span></div>
|
||||
<div class="flow-zoom-controls" role="group" data-i18n-aria="flow.zoomControls" aria-label="Canvas zoom">
|
||||
<button type="button" class="secondary icon-button" data-action="flow-zoom-out"
|
||||
data-i18n-title="flow.zoomOut" title="Zoom out">−</button><span id="flowZoomLabel"
|
||||
data-i18n-title="flow.zoomOut" title="Zoom out" data-ui-icon="minus"></button><span id="flowZoomLabel"
|
||||
aria-live="polite">100%</span><button type="button" class="secondary icon-button"
|
||||
data-action="flow-zoom-in" data-i18n-title="flow.zoomIn" title="Zoom in">+</button><button type="button"
|
||||
class="secondary flow-fit-button" data-action="flow-fit" data-i18n="flow.fitView"
|
||||
data-i18n-title="flow.fitView" title="Fit">Fit</button>
|
||||
data-action="flow-zoom-in" data-i18n-title="flow.zoomIn" title="Zoom in" data-ui-icon="plus"></button><button type="button"
|
||||
class="secondary flow-fit-button" data-action="flow-fit" data-i18n-title="flow.fitView" title="Fit"><span
|
||||
class="flow-fit-button-icon" data-ui-icon="fit" aria-hidden="true"></span><span
|
||||
class="flow-fit-button-label" data-i18n="flow.fitView">Fit</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="flowWorkspace" class="flow-workspace" tabindex="0">
|
||||
@@ -956,7 +957,7 @@
|
||||
<div class="flow-natural-preview" id="flowNaturalPreview">
|
||||
<button type="button" class="flow-preview-toggle" data-action="flow-preview-toggle"
|
||||
aria-expanded="false"><strong data-i18n="flow.previewDetails">Interpretation and Flow cycle</strong><span
|
||||
class="flow-preview-toggle-icon" aria-hidden="true">⌄</span></button>
|
||||
class="flow-preview-toggle-icon" data-ui-icon="chevron-down" aria-hidden="true"></span></button>
|
||||
<div class="flow-preview-details">
|
||||
<div class="flow-preview-item flow-preview-interpretation"><strong
|
||||
data-i18n="flow.interpretation">Interpretation</strong><span id="flowInterpretation"></span></div>
|
||||
@@ -976,7 +977,7 @@
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="flow.blocks">Blocks</span>
|
||||
<h2 data-i18n="flow.addBlock">Add block</h2>
|
||||
</div><button type="button" data-close aria-label="Close">×</button>
|
||||
</div><button type="button" data-close aria-label="Close" data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="flow-block-search"><input id="flowBlockSearch" type="search" autocomplete="off"
|
||||
data-i18n-placeholder="flow.searchBlocks" placeholder="Search blocks…"></div>
|
||||
@@ -985,16 +986,16 @@
|
||||
|
||||
<dialog id="flowEditorActionsDialog" class="sheet flow-editor-actions-dialog">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="flow.actions">Actions</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="flow.actions">Actions</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="menu-list">
|
||||
<button type="button" data-action="flow-templates"><span
|
||||
data-i18n="flow.templates">Templates</span><span>›</span></button>
|
||||
<button type="button" data-action="import-flow"><span data-i18n="flow.import">Import</span><span>›</span></button>
|
||||
<button type="button" data-action="export-flow"><span data-i18n="flow.export">Export</span><span>›</span></button>
|
||||
data-i18n="flow.templates">Templates</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="import-flow"><span data-i18n="flow.import">Import</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="export-flow"><span data-i18n="flow.export">Export</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="flow-dry-run"><span
|
||||
data-i18n="flow.dryRun">Dry-run</span><span>›</span></button>
|
||||
<button type="button" data-action="flow-logs"><span data-i18n="flow.logs">Logs</span><span>›</span></button>
|
||||
data-i18n="flow.dryRun">Dry-run</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="flow-logs"><span data-i18n="flow.logs">Logs</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -1005,7 +1006,7 @@
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="flow.sharedInputEyebrow">Wspólne dane</span>
|
||||
<h2 id="flowSharedInputDialogTitle" data-i18n="flow.sharedInputTitle">Wspólne wejście Flow</h2>
|
||||
</div><button type="button" data-close>×</button>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<input type="hidden" name="id">
|
||||
<label><span data-i18n="common.name">Nazwa</span><input name="name" maxlength="100" required
|
||||
@@ -1034,7 +1035,7 @@
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="flow.templateLibrary">Flow library</span>
|
||||
<h2 data-i18n="flow.templates">Templates</h2>
|
||||
</div><button type="button" data-close>×</button>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="flow.templatesHint">Ready-made layouts create an editable graph using existing
|
||||
zones, devices and groups.</p>
|
||||
@@ -1060,7 +1061,7 @@
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="flow.testMode">Flow diagnostics</span>
|
||||
<h2 id="flowTestTitle" data-i18n="flow.dryRun">Dry-run</h2>
|
||||
</div><button type="button" data-close>×</button>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div id="flowSimulationControls" class="flow-simulation-controls">
|
||||
<p class="field-note" data-i18n="flow.dryRunHint">Dry-run does not change thermostat, device, group, schedule or
|
||||
@@ -1081,53 +1082,53 @@
|
||||
|
||||
<nav class="bottom-nav" data-i18n-aria="nav.navigation" aria-label="Navigation">
|
||||
<span class="nav-section-label desktop-nav-only" data-i18n="nav.sectionControl">Sterowanie</span>
|
||||
<button class="active" data-nav="dashboard"><span>⌂</span><b data-i18n="nav.dashboard">Dashboard</b></button>
|
||||
<button data-nav="zones"><span>◎</span><b data-i18n="nav.zones">Zones</b></button>
|
||||
<button class="desktop-nav-only" data-nav="groups"><span>◉</span><b data-i18n="nav.groups">Groups</b></button>
|
||||
<button class="active" data-nav="dashboard"><span data-ui-icon="home" aria-hidden="true"></span><b data-i18n="nav.dashboard">Dashboard</b></button>
|
||||
<button data-nav="zones"><span data-ui-icon="zones" aria-hidden="true"></span><b data-i18n="nav.zones">Zones</b></button>
|
||||
<button class="desktop-nav-only" data-nav="groups"><span data-ui-icon="groups" aria-hidden="true"></span><b data-i18n="nav.groups">Groups</b></button>
|
||||
<span class="nav-section-label desktop-nav-only" data-i18n="nav.sectionAutomation">Automatyzacja</span>
|
||||
<button data-nav="flows"><span>◇</span><b data-i18n="nav.flows">Flow</b></button>
|
||||
<button class="desktop-nav-only" data-nav="schedules"><span>◷</span><b
|
||||
<button data-nav="flows"><span data-ui-icon="flow" aria-hidden="true"></span><b data-i18n="nav.flows">Flow</b></button>
|
||||
<button class="desktop-nav-only" data-nav="schedules"><span data-ui-icon="schedule" aria-hidden="true"></span><b
|
||||
data-i18n="nav.schedules">Schedules</b></button>
|
||||
<button class="desktop-nav-only" data-nav="automations"><span>⌁</span><b
|
||||
<button class="desktop-nav-only" data-nav="automations"><span data-ui-icon="automation" aria-hidden="true"></span><b
|
||||
data-i18n="nav.automations">Automations</b></button>
|
||||
<span class="nav-section-label desktop-nav-only" data-i18n="nav.sectionData">Dane i system</span>
|
||||
<button data-nav="history"><span>⌁</span><b data-i18n="nav.history">History</b></button>
|
||||
<button class="desktop-nav-only" data-nav="devices"><span>▣</span><b data-i18n="nav.devices">Devices</b></button>
|
||||
<button data-nav="history"><span data-ui-icon="history" aria-hidden="true"></span><b data-i18n="nav.history">History</b></button>
|
||||
<button class="desktop-nav-only" data-nav="devices"><span data-ui-icon="devices" aria-hidden="true"></span><b data-i18n="nav.devices">Devices</b></button>
|
||||
<button class="desktop-nav-only" data-nav="homeassistant"><span>HA</span><b data-i18n="nav.homeAssistantShort">Home
|
||||
Assistant</b></button>
|
||||
<button class="desktop-nav-only" data-nav="simulation"><span>▶</span><b
|
||||
<button class="desktop-nav-only" data-nav="simulation"><span data-ui-icon="play" aria-hidden="true"></span><b
|
||||
data-i18n="nav.simulation">Simulator</b></button>
|
||||
<button class="desktop-nav-only" data-nav="night"><span>☾</span><b data-i18n="nav.nightMode">Night mode</b></button>
|
||||
<button class="desktop-nav-only" data-nav="logs"><span>≡</span><b data-i18n="nav.logs">Events</b></button>
|
||||
<button class="desktop-nav-only" data-nav="settings"><span>⚙</span><b data-i18n="nav.settings">Settings</b></button>
|
||||
<button class="mobile-nav-only" data-nav="more"><span>•••</span><b data-i18n="nav.more">More</b></button>
|
||||
<button class="desktop-nav-only" data-nav="night"><span data-ui-icon="moon" aria-hidden="true"></span><b data-i18n="nav.nightMode">Night mode</b></button>
|
||||
<button class="desktop-nav-only" data-nav="logs"><span data-ui-icon="events" aria-hidden="true"></span><b data-i18n="nav.logs">Events</b></button>
|
||||
<button class="desktop-nav-only" data-nav="settings"><span data-ui-icon="settings" aria-hidden="true"></span><b data-i18n="nav.settings">Settings</b></button>
|
||||
<button class="mobile-nav-only" data-nav="more"><span data-ui-icon="more-horizontal" aria-hidden="true"></span><b data-i18n="nav.more">More</b></button>
|
||||
</nav>
|
||||
|
||||
<dialog id="moreDialog" class="sheet">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="nav.more">More</h2><button data-close>×</button>
|
||||
<h2 data-i18n="nav.more">More</h2><button data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="menu-list more-menu-list">
|
||||
<span class="menu-section-label" data-i18n="nav.sectionControl">Sterowanie</span>
|
||||
<button data-go="devices"><span data-i18n="nav.devices">Devices</span><span>›</span></button>
|
||||
<button data-go="groups"><span data-i18n="nav.groups">Groups</span><span>›</span></button>
|
||||
<button data-go="devices"><span data-i18n="nav.devices">Devices</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="groups"><span data-i18n="nav.groups">Groups</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<span class="menu-section-label" data-i18n="nav.sectionAutomation">Automatyzacja</span>
|
||||
<button data-go="schedules"><span data-i18n="nav.schedules">Schedules</span><span>›</span></button>
|
||||
<button data-go="automations"><span data-i18n="nav.automations">Automations</span><span>›</span></button>
|
||||
<button data-go="schedules"><span data-i18n="nav.schedules">Schedules</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="automations"><span data-i18n="nav.automations">Automations</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<span class="menu-section-label" data-i18n="nav.sectionData">Dane i system</span>
|
||||
<button data-go="simulation"><span data-i18n="nav.simulation">Simulator</span><span>›</span></button>
|
||||
<button data-go="night"><span data-i18n="nav.nightMode">Night mode</span><span>›</span></button>
|
||||
<button data-go="simulation"><span data-i18n="nav.simulation">Simulator</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="night"><span data-i18n="nav.nightMode">Night mode</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="homeassistant"><span data-i18n="nav.homeAssistant">Home Assistant /
|
||||
Sensors</span><span>›</span></button>
|
||||
<button data-go="settings"><span data-i18n="nav.settings">Settings</span><span>›</span></button>
|
||||
<button data-go="logs"><span data-i18n="nav.logsAndEvents">Events and logs</span><span>›</span></button>
|
||||
Sensors</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="settings"><span data-i18n="nav.settings">Settings</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="logs"><span data-i18n="nav.logsAndEvents">Events and logs</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="deviceDialog">
|
||||
<form method="dialog" id="deviceForm" class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="devices.add">Add device</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="devices.add">Add device</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required
|
||||
data-i18n-placeholder="placeholder.livingRoom" placeholder="Living room"></label>
|
||||
@@ -1154,7 +1155,7 @@
|
||||
|
||||
<dialog id="cloudDiscoveryDialog" class="dialog-wide">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head"><div><span class="eyebrow">GREE Cloud</span><h2 data-i18n="devices.cloudDiscoveryTitle">Devices on account</h2></div><button type="button" data-close>×</button></div>
|
||||
<div class="dialog-head"><div><span class="eyebrow">GREE Cloud</span><h2 data-i18n="devices.cloudDiscoveryTitle">Devices on account</h2></div><button type="button" data-close data-ui-icon="close"></button></div>
|
||||
<p class="field-note" data-i18n="devices.cloudDiscoveryHint">Devices are loaded from the saved GREE Cloud account. Nothing is added automatically; choose the units you want to register.</p>
|
||||
<div id="cloudDiscoveryList" class="discovery-name-list"></div>
|
||||
<div class="form-actions"><button type="button" class="secondary" id="cloudDiscoveryRefresh" data-i18n="actions.refresh">Refresh</button><button type="button" class="primary" data-close data-i18n="actions.done">Done</button></div>
|
||||
@@ -1164,7 +1165,7 @@
|
||||
<dialog id="discoverDialog">
|
||||
<form method="dialog" id="discoverForm" class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="discovery.title">Discover GREE devices</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="discovery.title">Discover GREE devices</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="discovery.help">Auto searches both AES-ECB and AES-GCM devices. Multiple passes
|
||||
improve discovery when several Wi-Fi modules answer the same broadcast.</p>
|
||||
@@ -1185,7 +1186,7 @@
|
||||
<dialog id="discoveryNamesDialog">
|
||||
<form method="dialog" id="discoveryNamesForm" class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="discovery.nameDevices">Name discovered devices</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="discovery.nameDevices">Name discovered devices</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="discovery.nameDevicesHelp">Give each new unit a friendly room name. The technical
|
||||
model and MAC remain available in diagnostics.</p>
|
||||
@@ -1200,7 +1201,7 @@
|
||||
<form method="dialog" id="renameDeviceForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="devices.technicalConfig">Technical configuration</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="devices.technicalConfig">Technical configuration</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required maxlength="80"></label>
|
||||
<div class="two"><label><span data-i18n="devices.ipAddress">IP address</span><input name="ip"
|
||||
@@ -1226,7 +1227,7 @@
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="devices.cloudDetails">Cloud details</span><h2 id="deviceDetailsTitle">Device details</h2></div>
|
||||
<button type="button" data-close>×</button>
|
||||
<button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required maxlength="80"></label>
|
||||
<div class="technical-device-grid" id="deviceDetailsMeta"></div>
|
||||
@@ -1249,7 +1250,7 @@
|
||||
|
||||
<dialog id="deviceGroupsDialog" class="dialog-wide">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head"><div><span class="eyebrow" data-i18n="devices.installation">Installation</span><h2 data-i18n="devices.installations">Split / multisplit installations</h2></div><button type="button" data-close>×</button></div>
|
||||
<div class="dialog-head"><div><span class="eyebrow" data-i18n="devices.installation">Installation</span><h2 data-i18n="devices.installations">Split / multisplit installations</h2></div><button type="button" data-close data-ui-icon="close"></button></div>
|
||||
<p class="field-note" data-i18n="devices.installationsHint">Group indoor units that share one outdoor unit. Energy and outdoor temperature can use one shared source.</p>
|
||||
<div id="deviceGroupsList" class="installation-list"></div>
|
||||
<form id="deviceGroupForm" class="installation-form">
|
||||
@@ -1271,7 +1272,7 @@
|
||||
|
||||
<dialog id="cloudDiagnosticsDialog" class="dialog-wide">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head"><div><span class="eyebrow">GREE Cloud</span><h2 data-i18n="devices.cloudDiagnostics">Cloud diagnostics</h2></div><button type="button" data-close>×</button></div>
|
||||
<div class="dialog-head"><div><span class="eyebrow">GREE Cloud</span><h2 data-i18n="devices.cloudDiagnostics">Cloud diagnostics</h2></div><button type="button" data-close data-ui-icon="close"></button></div>
|
||||
<p class="field-note" data-i18n="devices.cloudDiagnosticsHint">Decrypted and sanitized protocol data. Passwords, tokens and cipher keys are never included.</p>
|
||||
<pre id="cloudDiagnosticsPayload" class="mono cloud-diagnostics-payload">—</pre>
|
||||
<div class="form-actions"><button type="button" class="secondary" id="cloudDiagnosticsRefresh" data-i18n="actions.refresh">Refresh</button><button type="button" class="primary" data-close data-i18n="actions.done">Done</button></div>
|
||||
@@ -1284,7 +1285,7 @@
|
||||
<div><span class="eyebrow" data-i18n="devices.diagnostics">Diagnostics</span>
|
||||
<h2 data-i18n="devices.pingLive">Live ping</h2>
|
||||
</div>
|
||||
<button type="button" data-close>×</button>
|
||||
<button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="devices.pingHint">Measures a minimal GREE round-trip without changing device
|
||||
state, error counters or automation ownership. The chart refreshes while monitoring is enabled.</p>
|
||||
@@ -1305,7 +1306,7 @@
|
||||
<form method="dialog" id="zoneForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="common.zone">Zone</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="common.zone">Zone</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required
|
||||
data-i18n-placeholder="placeholder.livingRoom" placeholder="Living room"></label>
|
||||
@@ -1391,7 +1392,7 @@
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="zones.queueEyebrow">Compressor protection</span>
|
||||
<h2 data-i18n="zones.queueTitle">Command queue</h2>
|
||||
</div><button type="button" data-close>×</button>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="zones.queueHint">Commands waiting for the compressor protection window to end.
|
||||
You can cancel one task or clear the whole queue.</p>
|
||||
@@ -1411,7 +1412,7 @@
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="zones.quickThermostat">Szybki termostat</span>
|
||||
<h2 data-i18n="zones.temporaryTitle">Włącz czasowo</h2>
|
||||
</div><button type="button" data-close>×</button>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div id="temporaryThermostatActive" class="temporary-active-summary" hidden>
|
||||
<div><small id="temporaryThermostatActiveLabel" data-i18n="zones.temporaryActive">Aktywny tryb
|
||||
@@ -1498,7 +1499,7 @@
|
||||
<form method="dialog" id="groupForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="groups.group">Group</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="groups.group">Group</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required
|
||||
data-i18n-placeholder="groups.namePlaceholder" placeholder="Upstairs"></label>
|
||||
@@ -1518,7 +1519,7 @@
|
||||
<form method="dialog" id="scheduleForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="common.schedule">Schedule</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="common.schedule">Schedule</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required
|
||||
data-i18n-placeholder="placeholder.night" placeholder="Night"></label>
|
||||
@@ -1557,7 +1558,7 @@
|
||||
<form method="dialog" id="automationForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="common.automation">Automation</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="common.automation">Automation</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="copy-settings-row"><select id="automationPreset">
|
||||
<option value="" data-i18n="automations.preset">Preset…</option>
|
||||
@@ -1638,7 +1639,7 @@
|
||||
<dialog id="generatedTokenDialog" class="auth-dialog">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="settings.tokenCreated">Token created</h2><button type="button" data-close>×</button>
|
||||
<h2 data-i18n="settings.tokenCreated">Token created</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="settings.tokenCreatedHint">Copy this token now. It will not be shown again.</p>
|
||||
<label><span data-i18n="auth.token">Access token</span><input id="generatedAccessToken" class="mono"
|
||||
@@ -1653,7 +1654,7 @@
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="history.24h">24 hours</span><h2 data-i18n="history.allOutdoor">Outdoor temperatures</h2></div>
|
||||
<button type="button" data-close data-i18n-aria="actions.close" aria-label="Close">×</button>
|
||||
<button type="button" data-close data-i18n-aria="actions.close" aria-label="Close" data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="outdoor-history-current"><small data-i18n="house.outdoor">Outdoor</small><strong id="outdoorHistoryCurrent">--</strong></div>
|
||||
<div id="outdoorHistoryChartHost"></div>
|
||||
|
||||
+2
-2
@@ -255,7 +255,7 @@ function updateChartFullscreenButton(card) {
|
||||
const button = card.querySelector('[data-chart-fullscreen]');
|
||||
if (!button) return;
|
||||
const active = chartCardIsFullscreen(card);
|
||||
button.textContent = active ? '×' : '⛶';
|
||||
button.innerHTML = uiIcon(active ? 'close' : 'fullscreen');
|
||||
button.title = tr(active ? 'history.exitFullscreen' : 'history.fullscreen');
|
||||
button.setAttribute('aria-label', button.title);
|
||||
}
|
||||
@@ -724,7 +724,7 @@ function renderLegend(host, series) {
|
||||
function historyChartMarkup(id, title, hint, compact = false) {
|
||||
const zoom = clamp(Number(app.chartZooms[id] || 1), 1, MAX_CHART_ZOOM);
|
||||
const percent = Math.round(zoom * 100);
|
||||
return `<div class="panel chart-panel history-chart-card"><div class="chart-title-row"><div><h3>${esc(title)}</h3><p>${esc(hint || '')}</p><small class="chart-hover-hint">${esc(tr('history.hoverHint'))}</small></div><div class="chart-title-actions"><div class="chart-zoom-controls" aria-label="${esc(tr('history.zoomControls'))}"><button type="button" data-chart-zoom="out" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomOut'))}" ${zoom <= 1 ? 'disabled' : ''}>−</button><button type="button" class="chart-zoom-reset" data-chart-zoom="reset" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomReset'))}">${percent}%</button><button type="button" data-chart-zoom="in" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomIn'))}" ${zoom >= MAX_CHART_ZOOM ? 'disabled' : ''}>+</button></div><button type="button" class="chart-fullscreen-button" data-chart-fullscreen="${esc(id)}" title="${esc(tr('history.fullscreen'))}" aria-label="${esc(tr('history.fullscreen'))}">⛶</button></div></div><div class="chart-wrap ${compact ? 'compact-chart' : ''}"><canvas id="${esc(id)}" data-chart-zoom="${zoom}" width="1000" height="${compact ? 300 : 420}" tabindex="0" aria-label="${esc(title)}"></canvas><div class="chart-hover-line" hidden></div><div class="chart-selection" hidden></div><div class="chart-tooltip" hidden></div></div><div class="legend" id="${esc(id)}Legend"></div></div>`;
|
||||
return `<div class="panel chart-panel history-chart-card"><div class="chart-title-row"><div><h3>${esc(title)}</h3><p>${esc(hint || '')}</p><small class="chart-hover-hint">${esc(tr('history.hoverHint'))}</small></div><div class="chart-title-actions"><div class="chart-zoom-controls" aria-label="${esc(tr('history.zoomControls'))}"><button type="button" data-chart-zoom="out" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomOut'))}" ${zoom <= 1 ? 'disabled' : ''}>${uiIcon('minus')}</button><button type="button" class="chart-zoom-reset" data-chart-zoom="reset" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomReset'))}">${percent}%</button><button type="button" data-chart-zoom="in" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomIn'))}" ${zoom >= MAX_CHART_ZOOM ? 'disabled' : ''}>${uiIcon('plus')}</button></div><button type="button" class="chart-fullscreen-button" data-chart-fullscreen="${esc(id)}" title="${esc(tr('history.fullscreen'))}" aria-label="${esc(tr('history.fullscreen'))}">${uiIcon('fullscreen')}</button></div></div><div class="chart-wrap ${compact ? 'compact-chart' : ''}"><canvas id="${esc(id)}" data-chart-zoom="${zoom}" width="1000" height="${compact ? 300 : 420}" tabindex="0" aria-label="${esc(title)}"></canvas><div class="chart-hover-line" hidden></div><div class="chart-selection" hidden></div><div class="chart-tooltip" hidden></div></div><div class="legend" id="${esc(id)}Legend"></div></div>`;
|
||||
}
|
||||
|
||||
function historySeriesColor(index) {
|
||||
|
||||
+55
-2
@@ -43,6 +43,59 @@ const $ = (selector, root = document) => root.querySelector(selector);
|
||||
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
||||
const esc = value => String(value ?? '').replace(/[&<>'"]/g, char => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char]));
|
||||
const UI_ICON_PATHS = Object.freeze({
|
||||
'arrow-left': '<path d="M19 12H5"/><path d="m12 19-7-7 7-7"/>',
|
||||
'automation': '<path d="M6 3v12"/><path d="M18 9v12"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="6" r="3"/><path d="M9 18h3a6 6 0 0 0 6-6V9"/>',
|
||||
'check': '<path d="m5 12 4 4L19 6"/>',
|
||||
'chevron-down': '<path d="m6 9 6 6 6-6"/>',
|
||||
'chevron-right': '<path d="m9 6 6 6-6 6"/>',
|
||||
'circle': '<circle cx="12" cy="12" r="7"/>',
|
||||
'close': '<path d="M6 6l12 12M18 6 6 18"/>',
|
||||
'devices': '<rect x="4" y="3" width="16" height="7" rx="2"/><rect x="4" y="14" width="16" height="7" rx="2"/><path d="M8 6.5h.01M8 17.5h.01"/>',
|
||||
'edit': '<path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L8 18l-4 1 1-4Z"/>',
|
||||
'events': '<path d="M8 6h12M8 12h12M8 18h12"/><circle cx="4" cy="6" r="1" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1" fill="currentColor" stroke="none"/>',
|
||||
'expand-vertical': '<path d="m8 7 4-4 4 4M12 3v18M8 17l4 4 4-4"/>',
|
||||
'fit': '<path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5"/><path d="m3 8 5-5M21 8l-5-5M3 16l5 5M21 16l-5 5"/>',
|
||||
'flow': '<circle cx="5" cy="6" r="2"/><circle cx="19" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M7 6h10M6.5 7.5l4.3 8.7M17.5 7.5l-4.3 8.7"/>',
|
||||
'fullscreen': '<path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5"/>',
|
||||
'groups': '<circle cx="9" cy="8" r="3"/><circle cx="17" cy="9" r="2.5"/><path d="M3 20v-1a6 6 0 0 1 12 0v1M14 15.5a5 5 0 0 1 7 4.5"/>',
|
||||
'history': '<path d="M4 19V5"/><path d="M4 19h16"/><path d="m7 15 4-4 3 2 5-6"/>',
|
||||
'home': '<path d="m3 11 9-8 9 8"/><path d="M5 10v11h14V10M9 21v-6h6v6"/>',
|
||||
'integration': '<path d="M7 7h11l-3-3M17 17H6l3 3"/><path d="m18 7-3 3M6 17l3-3"/>',
|
||||
'menu': '<path d="M4 6h16M4 12h16M4 18h16"/>',
|
||||
'minus': '<path d="M5 12h14"/>',
|
||||
'moon': '<path d="M20 15.5A8 8 0 0 1 8.5 4 8 8 0 1 0 20 15.5Z"/>',
|
||||
'more-horizontal': '<circle cx="5" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.5" fill="currentColor" stroke="none"/>',
|
||||
'play': '<path d="m8 5 11 7-11 7Z" fill="currentColor" stroke="none"/>',
|
||||
'plus': '<path d="M12 5v14M5 12h14"/>',
|
||||
'power': '<path d="M12 2.75v8.5"/><path d="M7.12 5.12a7.25 7.25 0 1 0 9.76 0"/>',
|
||||
'refresh': '<path d="M20 6v5h-5"/><path d="M19 11a7 7 0 1 0 1 5"/>',
|
||||
'schedule': '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
||||
'settings': '<circle cx="12" cy="12" r="3"/><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.09a2 2 0 0 1 1 1.74v.5a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.09a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2Z"/>',
|
||||
'sliders': '<path d="M4 6h10M18 6h2M4 12h3M11 12h9M4 18h7M15 18h5"/><circle cx="16" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="13" cy="18" r="2"/>',
|
||||
'star': '<path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-3-5.6 3 1.1-6.2L3 9.6l6.2-.9Z"/>',
|
||||
'star-filled': '<path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-3-5.6 3 1.1-6.2L3 9.6l6.2-.9Z" fill="currentColor" stroke="none"/>',
|
||||
'status-dot': '<circle cx="12" cy="12" r="5" fill="currentColor" stroke="none"/>',
|
||||
'sun': '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.42 1.42M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.42-1.42M17.66 6.34l1.41-1.41"/>',
|
||||
'system': '<circle cx="12" cy="12" r="8"/><path d="M12 4a8 8 0 0 1 0 16Z" fill="currentColor" stroke="none"/>',
|
||||
'system-info': '<circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7h.01"/>',
|
||||
'zones': '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4"/><circle cx="12" cy="12" r="1" fill="currentColor" stroke="none"/>'
|
||||
});
|
||||
|
||||
function uiIcon(name, className = '') {
|
||||
const body = UI_ICON_PATHS[name];
|
||||
if (!body) return '';
|
||||
const classes = ['ui-icon', className].filter(Boolean).join(' ');
|
||||
return `<svg class="${esc(classes)}" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
|
||||
}
|
||||
|
||||
function hydrateUiIcons(root = document) {
|
||||
$$('[data-ui-icon]', root).forEach(node => {
|
||||
const name = node.dataset.uiIcon;
|
||||
const className = node.dataset.uiIconClass || '';
|
||||
node.innerHTML = uiIcon(name, className);
|
||||
});
|
||||
}
|
||||
const locale = () => app.locales[app.language] || app.locales[DEFAULT_LANGUAGE] || 'en-GB';
|
||||
const tr = (key, params = {}) => {
|
||||
const template = app.translations[app.language]?.[key] ?? app.translations[DEFAULT_LANGUAGE]?.[key] ?? key;
|
||||
@@ -224,8 +277,8 @@ function updateToolbarControls() {
|
||||
if (languageLabel) languageLabel.textContent = String(app.language || DEFAULT_LANGUAGE).toUpperCase();
|
||||
const themeSelect = $('#themeSelect');
|
||||
const themeIcon = $('#themeIcon');
|
||||
const icons = { system: '◐', light: '☀', dark: '☾' };
|
||||
if (themeIcon) themeIcon.textContent = icons[app.theme] || icons.system;
|
||||
const iconName = { system: 'system', light: 'sun', dark: 'moon' }[app.theme] || 'system';
|
||||
if (themeIcon) themeIcon.innerHTML = uiIcon(iconName);
|
||||
if (themeSelect) {
|
||||
themeSelect.value = app.theme;
|
||||
const label = `${tr('controls.theme')}: ${tr(`theme.${app.theme}`)}`;
|
||||
|
||||
+5
-5
@@ -377,11 +377,11 @@ function renderSimulationPage() {
|
||||
return `<path class="flow-link ${cls}" d="M ${ax} ${ay} C ${ax + bend} ${ay}, ${bx - bend} ${by}, ${bx} ${by}"/>`;
|
||||
};
|
||||
const verticalLink = (ax, ay, bx, by, cls = 'bus') => `<path class="flow-link ${cls}" d="M ${ax} ${ay} C ${ax} ${ay + 35}, ${bx} ${by - 35}, ${bx} ${by}"/>`;
|
||||
const node = ({ left, top, kind, eyebrow, title, value, meta = '', badge = '', badgeClass = '' }) => `<article class="flow-node ${kind}" style="left:${left}px;top:${top}px;width:${nodeW}px;min-height:${nodeH}px"><div class="flow-node-top"><span>${esc(eyebrow)}</span>${badge ? `<b class="flow-node-badge ${badgeClass}">${esc(badge)}</b>` : ''}</div><h3>${esc(title)}</h3><strong>${esc(value)}</strong>${meta ? `<p>${esc(meta)}</p>` : ''}<i class="flow-port in"></i><i class="flow-port out"></i></article>`;
|
||||
const node = ({ left, top, kind, eyebrow, title, value, meta = '', badge = '', badgeIcon = '', badgeClass = '' }) => `<article class="flow-node ${kind}" style="left:${left}px;top:${top}px;width:${nodeW}px;min-height:${nodeH}px"><div class="flow-node-top"><span>${esc(eyebrow)}</span>${badge || badgeIcon ? `<b class="flow-node-badge ${badgeClass}">${badgeIcon ? uiIcon(badgeIcon) : esc(badge)}</b>` : ''}</div><h3>${esc(title)}</h3><strong>${esc(value)}</strong>${meta ? `<p>${esc(meta)}</p>` : ''}<i class="flow-port in"></i><i class="flow-port out"></i></article>`;
|
||||
|
||||
const globalHouseLeft = 305, globalNightLeft = 565, globalTop = 28;
|
||||
nodes.push(node({ left: globalHouseLeft, top: globalTop, kind: 'logic global', eyebrow: tr('simulation.globalInput'), title: tr('simulation.houseMode'), value: houseModeLabel(plan.house_mode || 'off'), meta: tr('simulation.strategy', { strategy: (plan.control_strategy || 'setpoint') === 'setpoint' ? tr('plan.strategySetpoint') : (plan.control_strategy || 'setpoint') }), badge: tr('simulation.house') }));
|
||||
nodes.push(node({ left: globalNightLeft, top: globalTop, kind: `logic global ${nightOn ? 'night-active' : ''}`, eyebrow: tr('simulation.globalInput'), title: tr('settings.nightMode'), value: nightOn ? tr('common.active') : (app.settings?.night_mode?.enabled ? tr('simulation.scheduled') : tr('common.disabled')), meta: `${plan.night_mode_start || '22:00'} → ${plan.night_mode_end || '06:00'} · ${fanLabel(plan.night_mode_max_fan_speed || 1)}`, badge: nightOn ? '☾' : '○', badgeClass: nightOn ? 'active' : '' }));
|
||||
nodes.push(node({ left: globalNightLeft, top: globalTop, kind: `logic global ${nightOn ? 'night-active' : ''}`, eyebrow: tr('simulation.globalInput'), title: tr('settings.nightMode'), value: nightOn ? tr('common.active') : (app.settings?.night_mode?.enabled ? tr('simulation.scheduled') : tr('common.disabled')), meta: `${plan.night_mode_start || '22:00'} → ${plan.night_mode_end || '06:00'} · ${fanLabel(plan.night_mode_max_fan_speed || 1)}`, badgeIcon: nightOn ? 'moon' : 'circle', badgeClass: nightOn ? 'active' : '' }));
|
||||
|
||||
zones.forEach((planZone, index) => {
|
||||
const zone = app.zones.find(item => item.id === planZone.zone_id);
|
||||
@@ -401,12 +401,12 @@ function renderSimulationPage() {
|
||||
const eventMeta = next ? `${next.label}${next.target_temperature == null ? '' : ` · ${fmtTemp(next.target_temperature)}`}` : tr('plan.noEvents');
|
||||
|
||||
nodes.push(`<div class="flow-lane-label" style="top:${y - 27}px"><strong>${esc(planZone.zone_name)}</strong><span>${esc(simulationLaneContext(planZone))}</span></div>`);
|
||||
nodes.push(node({ left: x.sensor, top: y, kind: 'input', eyebrow: tr('simulation.stepRoom'), title: sensorName, value: fmtTemp(sim.current), meta: source, badge: '●', badgeClass: sim.current == null ? '' : 'active' }));
|
||||
nodes.push(node({ left: x.sensor, top: y, kind: 'input', eyebrow: tr('simulation.stepRoom'), title: sensorName, value: fmtTemp(sim.current), meta: source, badgeIcon: 'status-dot', badgeClass: sim.current == null ? '' : 'active' }));
|
||||
nodes.push(node({ left: x.thermostat, top: y, kind: 'logic', eyebrow: tr('simulation.thermostat'), title: planZone.zone_name, value: sim.target == null ? '—' : fmtTemp(sim.target), meta: `${zonePresetLabel(planZone.preset)} · ${tr('simulation.hysteresis', { value: sim.hysteresis.toFixed(1) })}`, badge: houseModeLabel(sim.mode) }));
|
||||
nodes.push(node({ left: x.decision, top: y, kind: `logic decision ${sim.demand ? 'demand' : 'satisfied'}`, eyebrow: tr('simulation.stepDecision'), title: status.label, value: sim.demand ? tr('simulation.callForComfort') : tr('simulation.standby'), meta: sim.reasoning, badge: sim.demand ? '▶' : '✓', badgeClass: status.badge }));
|
||||
nodes.push(node({ left: x.decision, top: y, kind: `logic decision ${sim.demand ? 'demand' : 'satisfied'}`, eyebrow: tr('simulation.stepDecision'), title: status.label, value: sim.demand ? tr('simulation.callForComfort') : tr('simulation.standby'), meta: sim.reasoning, badgeIcon: sim.demand ? 'play' : 'check', badgeClass: status.badge }));
|
||||
const sleepMeta = sim.nativeSleep ? ` · ${tr('devices.sleep')}: ${tr('common.on')}` : '';
|
||||
nodes.push(node({ left: x.unit, top: y, kind: 'action', eyebrow: tr('simulation.stepCommand'), title: planZone.device_name || tr('common.device'), value: command, meta: `${tr('devices.quiet')}: ${quietText}${sleepMeta}`, badge: sim.mode === 'off' ? tr('simulation.manual') : (sim.demand ? tr('simulation.running') : tr('simulation.idle')), badgeClass: sim.demand ? 'active' : '' }));
|
||||
nodes.push(node({ left: x.event, top: y, kind: 'event', eyebrow: tr('simulation.nextEvent'), title: eventValue, value: next?.preset ? zonePresetLabel(next.preset) : tr('simulation.schedule'), meta: eventMeta, badge: '›' }));
|
||||
nodes.push(node({ left: x.event, top: y, kind: 'event', eyebrow: tr('simulation.nextEvent'), title: eventValue, value: next?.preset ? zonePresetLabel(next.preset) : tr('simulation.schedule'), meta: eventMeta, badgeIcon: 'chevron-right' }));
|
||||
|
||||
links.push(pathBetween(x.sensor + nodeW, mid, x.thermostat, mid, 'input-link'));
|
||||
links.push(pathBetween(x.thermostat + nodeW, mid, x.decision, mid, 'logic-link'));
|
||||
|
||||
+14
-14
@@ -127,7 +127,7 @@ function manualDeviceProblem(device) {
|
||||
}
|
||||
|
||||
function powerIconMarkup() {
|
||||
return '<svg class="power-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.75v8.5"/><path d="M7.12 5.12a7.25 7.25 0 1 0 9.76 0"/></svg>';
|
||||
return uiIcon('power', 'power-icon');
|
||||
}
|
||||
|
||||
function manualLocalDeviceCard(device) {
|
||||
@@ -145,9 +145,9 @@ function manualLocalDeviceCard(device) {
|
||||
${warning}
|
||||
${manualDeviceProblem(device)}
|
||||
<div class="temperature-control">
|
||||
<button data-action="temperature" data-delta="-1" data-device="${esc(device.id)}">−</button>
|
||||
<button data-action="temperature" data-delta="-1" data-device="${esc(device.id)}">${uiIcon('minus')}</button>
|
||||
<div class="target-temp editable-target" data-temperature-kind="device" data-id="${esc(device.id)}" data-value="${Number(device.target_temperature)}" data-editable="true" tabindex="0" role="button" title="${esc(tr('common.clickTemperatureToEdit'))}">${Number(device.target_temperature).toFixed(1)}<small>°C</small></div>
|
||||
<button data-action="temperature" data-delta="1" data-device="${esc(device.id)}">+</button>
|
||||
<button data-action="temperature" data-delta="1" data-device="${esc(device.id)}">${uiIcon('plus')}</button>
|
||||
</div>
|
||||
<div class="current-line">${esc(tr('devices.currentTemperature'))}: <strong>${fmtTemp(device.current_temperature)}</strong>${effectiveDeviceOutdoorTemperature(device) == null ? '' : ` · ${esc(tr('devices.outdoor'))} ${fmtTemp(effectiveDeviceOutdoorTemperature(device))}`}</div>
|
||||
${compressorQueuePanel(managedZone)}
|
||||
@@ -184,9 +184,9 @@ function manualCloudDeviceCard(device) {
|
||||
</div>
|
||||
${warning}
|
||||
<div class="temperature-control">
|
||||
<button data-action="temperature" data-delta="${-tempStep}" data-device="${esc(device.id)}">−</button>
|
||||
<button data-action="temperature" data-delta="${-tempStep}" data-device="${esc(device.id)}">${uiIcon('minus')}</button>
|
||||
<div class="target-temp editable-target" data-temperature-kind="device" data-id="${esc(device.id)}" data-value="${Number(device.target_temperature)}" data-temp-step="${tempStep}" data-temp-min="${minTemp}" data-temp-max="${maxTemp}" data-editable="true" tabindex="0" role="button" title="${esc(tr('common.clickTemperatureToEdit'))}">${Number(device.target_temperature).toFixed(1)}<small>°C</small></div>
|
||||
<button data-action="temperature" data-delta="${tempStep}" data-device="${esc(device.id)}">+</button>
|
||||
<button data-action="temperature" data-delta="${tempStep}" data-device="${esc(device.id)}">${uiIcon('plus')}</button>
|
||||
</div>
|
||||
<div class="current-line">${esc(tr('devices.currentTemperature'))}: <strong>${fmtTemp(device.current_temperature)}</strong>${effectiveDeviceOutdoorTemperature(device) == null ? '' : ` · ${esc(tr('devices.outdoor'))} ${fmtTemp(effectiveDeviceOutdoorTemperature(device))}`}</div>
|
||||
${compressorQueuePanel(managedZone)}
|
||||
@@ -236,7 +236,7 @@ function technicalDeviceCard(device) {
|
||||
<button type="button" data-action="rename-device" data-device="${esc(device.id)}">${esc(tr('devices.technicalConfig'))}</button>
|
||||
<button type="button" data-action="energy-config" data-device="${esc(device.id)}">${esc(tr('energy.title'))}</button>
|
||||
${device.simulated || device.connection_type === 'gree_cloud' ? '' : `<button type="button" data-action="bind" data-device="${esc(device.id)}">${esc(tr('actions.bind'))}</button>`}
|
||||
<details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-device" data-device="${esc(device.id)}">${esc(tr('actions.delete'))}</button></div></details>
|
||||
<details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-device" data-device="${esc(device.id)}">${esc(tr('actions.delete'))}</button></div></details>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
@@ -272,7 +272,7 @@ function cloudTechnicalDeviceCard(device) {
|
||||
<button type="button" data-action="poll" data-device="${esc(device.id)}">${esc(tr('devices.readStatus'))}</button>
|
||||
<button type="button" data-action="cloud-details" data-device="${esc(device.id)}">${esc(tr('devices.cloudDetails'))}</button>
|
||||
<button type="button" data-action="cloud-diagnostics" data-device="${esc(device.id)}">${esc(tr('devices.cloudDiagnostics'))}</button>
|
||||
<details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-device" data-device="${esc(device.id)}">${esc(tr('actions.delete'))}</button></div></details>
|
||||
<details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-device" data-device="${esc(device.id)}">${esc(tr('actions.delete'))}</button></div></details>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
@@ -395,7 +395,7 @@ function renderCompressorQueueModal() {
|
||||
cancelAll.hidden = pending.length === 0;
|
||||
cancelAll.textContent = pending.length ? `${tr('zones.cancelAllQueued')} (${pending.length})` : tr('zones.cancelAllQueued');
|
||||
if (!pending.length) {
|
||||
list.innerHTML = `<div class="compressor-queue-empty"><span aria-hidden="true">✓</span><strong>${esc(tr('zones.queueEmptyTitle'))}</strong><p>${esc(tr('zones.queueEmptyText'))}</p></div>`;
|
||||
list.innerHTML = `<div class="compressor-queue-empty"><span aria-hidden="true">${uiIcon('check')}</span><strong>${esc(tr('zones.queueEmptyTitle'))}</strong><p>${esc(tr('zones.queueEmptyText'))}</p></div>`;
|
||||
return;
|
||||
}
|
||||
list.innerHTML = pending.map((zone, index) => {
|
||||
@@ -537,7 +537,7 @@ function zoneCard(zone, detailed = true) {
|
||||
const groupText = groupNames.length ? groupNames.join(' · ') : tr('zones.noGroup');
|
||||
const policy = zone.inherit_house_mode ? tr('zones.followHouse') : tr(zone.mode === 'heat' ? 'zones.heatOnly' : 'zones.coolOnly');
|
||||
return `<article class="list-card zone-config-card ${workingDemand ? 'demanding' : ''} ${waitingDemand ? 'lockout-waiting' : ''} ${zone.enabled ? '' : 'zone-disabled'} ${deviceManualOverride ? 'manual-takeover' : ''} ${controlGroup ? 'group-controlled' : ''}" data-zone-config="${esc(zone.id)}"${groupControlStyle(visualGroup)}>
|
||||
<div class="list-card-head"><div><span class="eyebrow">${esc(tr('zones.configuration'))}</span><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(tr('groups.group'))}: ${esc(groupText)}</p></div><button type="button" class="zone-enable-toggle ${zone.enabled ? 'active' : ''}" data-action="zone-enabled" data-id="${esc(zone.id)}" data-value="${zone.enabled ? 'false' : 'true'}" aria-label="${esc(tr(zone.enabled ? 'zones.disable' : 'zones.enable'))}" title="${esc(tr('zones.automationToggleHint'))}"><span>${zone.enabled ? '✓' : '○'}</span>${esc(state)}</button></div>
|
||||
<div class="list-card-head"><div><span class="eyebrow">${esc(tr('zones.configuration'))}</span><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(tr('groups.group'))}: ${esc(groupText)}</p></div><button type="button" class="zone-enable-toggle ${zone.enabled ? 'active' : ''}" data-action="zone-enabled" data-id="${esc(zone.id)}" data-value="${zone.enabled ? 'false' : 'true'}" aria-label="${esc(tr(zone.enabled ? 'zones.disable' : 'zones.enable'))}" title="${esc(tr('zones.automationToggleHint'))}"><span>${uiIcon(zone.enabled ? 'check' : 'circle')}</span>${esc(state)}</button></div>
|
||||
<div class="zone-config-status">
|
||||
<div class="zone-config-temperature"><small>${esc(tr('zones.currentStatus'))}</small><div><span>${fmtTemp(roomTemperature)}</span><b>→</b><strong>${Number.isFinite(target) ? `${target.toFixed(1)}°C` : '—'}</strong></div></div>
|
||||
<div class="zone-config-runtime"><span class="badge ${workingDemand ? 'active' : ''}">${esc(zoneRuntimeStatusLabel(zone, effectiveMode, device))}</span><span>${esc(houseModeLabel(effectiveMode))} · ${esc(zonePresetLabel(displayPreset))}</span><small>${esc(override)}</small><small><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}${zoneControlOwnerMeta(zone) ? ` · ${esc(zoneControlOwnerMeta(zone))}` : ''}</small></div>
|
||||
@@ -551,7 +551,7 @@ function zoneCard(zone, detailed = true) {
|
||||
<div class="sensor-detail">${esc(sensorDetails)}</div>
|
||||
${compressorQueuePanel(zone)}
|
||||
${manualTakeover}
|
||||
<div class="card-footer"><small>${esc(tr('zones.controlOnDashboard'))}</small><div class="card-menu"><button class="primary" data-action="zone-go-control" data-id="${esc(zone.id)}">${esc(tr('zones.controlNow'))}</button><button data-action="edit-zone" data-id="${esc(zone.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-zone" data-id="${esc(zone.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
|
||||
<div class="card-footer"><small>${esc(tr('zones.controlOnDashboard'))}</small><div class="card-menu"><button class="primary" data-action="zone-go-control" data-id="${esc(zone.id)}">${esc(tr('zones.controlNow'))}</button><button data-action="edit-zone" data-id="${esc(zone.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-zone" data-id="${esc(zone.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
@@ -559,7 +559,7 @@ function zoneCard(zone, detailed = true) {
|
||||
return `<article class="list-card zone-thermostat quick-control-card quick-thermostat-control ${workingDemand ? 'demanding' : ''} ${waitingDemand ? 'lockout-waiting' : ''} ${zone.enabled ? '' : 'zone-disabled'} ${deviceManualOverride ? 'manual-takeover' : ''} ${controlGroup ? 'group-controlled' : ''}" data-zone-card="${esc(zone.id)}"${groupControlStyle(visualGroup)}>
|
||||
<div class="list-card-head"><div><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(displayPreset))}</p></div><div class="zone-quick-actions">${localThermostatPowerControl}${temporaryThermostatControl}</div></div>
|
||||
${disabledManualControlNotice}
|
||||
<div class="thermostat-main"><div><small>${esc(tr('zones.measurement'))}</small><strong>${fmtTemp(roomTemperature)}</strong></div><div class="temperature-control compact"><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="-0.5">−</button><div class="target-temp compact editable-target" data-temperature-kind="zone" data-id="${esc(zone.id)}" data-value="${Number.isFinite(target) ? target : ''}" tabindex="0" role="button" title="${esc(tr('common.clickTemperatureToEdit'))}">${Number.isFinite(target) ? target.toFixed(1) : '--'}<small>°C</small></div><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="0.5">+</button></div><div><small>${esc(tr('zones.deviceTarget'))}</small><strong>${fmtTemp(zone.device_setpoint)}</strong></div></div>
|
||||
<div class="thermostat-main"><div><small>${esc(tr('zones.measurement'))}</small><strong>${fmtTemp(roomTemperature)}</strong></div><div class="temperature-control compact"><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="-0.5">${uiIcon('minus')}</button><div class="target-temp compact editable-target" data-temperature-kind="zone" data-id="${esc(zone.id)}" data-value="${Number.isFinite(target) ? target : ''}" tabindex="0" role="button" title="${esc(tr('common.clickTemperatureToEdit'))}">${Number.isFinite(target) ? target.toFixed(1) : '--'}<small>°C</small></div><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="0.5">${uiIcon('plus')}</button></div><div><small>${esc(tr('zones.deviceTarget'))}</small><strong>${fmtTemp(zone.device_setpoint)}</strong></div></div>
|
||||
<div class="preset-row quick-control-row quick-control-row-4">
|
||||
${['auto', 'comfort', 'sleep', 'away'].map(preset => `<button class="${manual === preset ? 'active' : ''}" data-action="zone-preset" data-id="${esc(zone.id)}" data-value="${preset}">${esc(preset === 'sleep' ? tr('zones.sleepNow') : zonePresetLabel(preset))}</button>`).join('')}
|
||||
</div>
|
||||
@@ -635,7 +635,7 @@ function groupCard(group, detailed = false) {
|
||||
<div class="group-control-block"><small>${esc(tr('groups.mode'))}</small><div class="mode-row group-mode-row">${['house', 'heat', 'cool'].map(mode => `<button class="${state.mode === mode ? 'active' : ''}" data-action="group-mode" data-id="${esc(group.id)}" data-value="${mode}"${climateControlsDisabled}>${esc(mode === 'house' ? tr('groups.followHouse') : modeLabel(mode))}</button>`).join('')}</div></div>
|
||||
<div class="group-control-block"><small>${esc(tr('groups.profile'))}</small><div class="preset-row group-preset-row">${['auto', 'comfort', 'sleep', 'away'].map(preset => `<button class="${state.preset === preset ? 'active' : ''}" data-action="group-preset" data-id="${esc(group.id)}" data-value="${preset}"${climateControlsDisabled}>${esc(zonePresetLabel(preset))}</button>`).join('')}<button class="${state.preset === 'custom' ? 'active' : (customDraft?.open === true ? 'editing' : '')}" data-action="group-custom-open" data-id="${esc(group.id)}" title="${esc(tr('groups.customOpenHint'))}"${climateControlsDisabled}>${esc(tr('preset.custom'))}</button></div>
|
||||
<div class="group-custom-temperature" data-group-custom-editor="${esc(group.id)}" ${customEditorVisible ? '' : 'hidden'}><label><span>${esc(tr('groups.customTemperature'))}</span><div class="group-custom-temperature-row"><input type="text" inputmode="decimal" min="8" max="30" step="0.1" value="${customTarget.toFixed(1)}" data-group-custom-temperature="${esc(group.id)}" aria-label="${esc(tr('groups.customTemperature'))}" title="${esc(tr('groups.customTemperatureHint'))}"><span>°C</span><button type="button" class="primary" data-action="group-custom-temperature" data-id="${esc(group.id)}" title="${esc(tr('groups.applyCustomTemperatureHint'))}">${esc(tr('actions.apply'))}</button><button type="button" class="secondary" data-action="group-custom-cancel" data-id="${esc(group.id)}">${esc(tr('actions.cancel'))}</button></div></label>${customDraftPending ? `<small class="group-custom-draft-note">${esc(tr('groups.customDraftPending'))}</small>` : ''}</div></div>
|
||||
${detailed ? `<div class="card-footer"><small>${esc(tr('groups.memberCount', { count: state.zones.length }))}</small><div class="card-menu"><button data-action="edit-group" data-id="${esc(group.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-group" data-id="${esc(group.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>` : ''}
|
||||
${detailed ? `<div class="card-footer"><small>${esc(tr('groups.memberCount', { count: state.zones.length }))}</small><div class="card-menu"><button data-action="edit-group" data-id="${esc(group.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-group" data-id="${esc(group.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>` : ''}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
@@ -700,7 +700,7 @@ function renderSchedules() {
|
||||
const days = item.weekdays.map(day => dayNames[day - 1]).join(', ');
|
||||
return `<article class="list-card ${item.flow_id ? 'flow-generated-card' : ''}"><div class="list-card-head"><div><h3>${esc(item.name)}</h3><p>${esc(zone?.name || tr('common.noZone'))} · ${esc(days)}</p></div><span class="badge ${item.enabled ? 'active' : ''}">${esc(item.enabled ? tr('schedules.enabledState') : tr('schedules.disabledState'))}</span></div>
|
||||
<div class="card-stats"><div class="card-stat"><small>${esc(tr('common.from'))}</small><strong>${esc(item.start_time)}</strong></div><div class="card-stat"><small>${esc(tr('common.to'))}</small><strong>${esc(item.end_time)}</strong></div><div class="card-stat"><small>${esc(tr('schedules.profile'))}</small><strong>${esc(item.preset === 'custom' ? fmtTemp(item.setpoint) : zonePresetLabel(item.preset))}</strong></div></div>
|
||||
<div class="card-footer"><small>${esc(item.flow_id ? tr('flow.generatedReadOnly') : tr('schedules.crossMidnight'))}</small><div class="card-menu">${item.flow_id ? `<button data-action="edit-flow" data-id="${esc(item.flow_id)}">${esc(tr('flow.openEditor'))}</button>` : `<button data-action="edit-schedule" data-id="${esc(item.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-schedule" data-id="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div></details>`}</div></div></article>`;
|
||||
<div class="card-footer"><small>${esc(item.flow_id ? tr('flow.generatedReadOnly') : tr('schedules.crossMidnight'))}</small><div class="card-menu">${item.flow_id ? `<button data-action="edit-flow" data-id="${esc(item.flow_id)}">${esc(tr('flow.openEditor'))}</button>` : `<button data-action="edit-schedule" data-id="${esc(item.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-schedule" data-id="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div></details>`}</div></div></article>`;
|
||||
}).join('') : `<div class="empty"><strong>${esc(tr('schedules.emptyTitle'))}</strong>${esc(tr('schedules.emptyText'))}</div>`;
|
||||
}
|
||||
|
||||
@@ -714,7 +714,7 @@ function renderAutomations() {
|
||||
const preset = item.action_zone_preset ? zonePresetLabel(item.action_zone_preset) : (item.action_preset ? zonePresetLabel(item.action_preset) : '—');
|
||||
return `<article class="list-card ${item.flow_id ? 'flow-generated-card' : ''}"><div class="list-card-head"><div><h3>${esc(item.name)}</h3><p>${esc(tr('automations.triggerSummary', { trigger: automationTriggerLabel(item), device: targetName }))}</p></div><span class="badge ${item.enabled ? 'active' : ''}">${esc(item.enabled ? tr('common.active') : tr('common.disabled'))}</span></div>
|
||||
<div class="card-stats"><div class="card-stat"><small>${esc(tr('common.power'))}</small><strong>${item.action.power == null ? '—' : item.action.power ? tr('common.on') : tr('common.off')}</strong></div><div class="card-stat"><small>${esc(tr('common.mode'))}</small><strong>${esc(mode)}</strong></div><div class="card-stat"><small>${esc(tr('groups.profile'))}</small><strong>${esc(preset)}</strong></div><div class="card-stat"><small>${esc(tr('automations.last'))}</small><strong>${item.last_fired_at ? new Date(item.last_fired_at).toLocaleTimeString(locale(), { hour: '2-digit', minute: '2-digit' }) : '—'}</strong></div></div>
|
||||
<div class="card-footer"><small>${esc(item.flow_id ? tr('flow.generatedReadOnly') : (actionGroup ? `${tr('groups.group')}: ${targetName}` : `${tr('common.device')}: ${targetName}`))} · ${esc(tr('common.cooldown'))} ${item.cooldown_seconds}s</small><div class="card-menu">${item.flow_id ? `<button data-action="edit-flow" data-id="${esc(item.flow_id)}">${esc(tr('flow.openEditor'))}</button>` : `<button data-action="edit-automation" data-id="${esc(item.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-automation" data-id="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div></details>`}</div></div></article>`;
|
||||
<div class="card-footer"><small>${esc(item.flow_id ? tr('flow.generatedReadOnly') : (actionGroup ? `${tr('groups.group')}: ${targetName}` : `${tr('common.device')}: ${targetName}`))} · ${esc(tr('common.cooldown'))} ${item.cooldown_seconds}s</small><div class="card-menu">${item.flow_id ? `<button data-action="edit-flow" data-id="${esc(item.flow_id)}">${esc(tr('flow.openEditor'))}</button>` : `<button data-action="edit-automation" data-id="${esc(item.id)}">${esc(tr('actions.edit'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-automation" data-id="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div></details>`}</div></div></article>`;
|
||||
}).join('') : `<div class="empty"><strong>${esc(tr('automations.emptyTitle'))}</strong>${esc(tr('automations.emptyText'))}</div>`;
|
||||
}
|
||||
|
||||
|
||||
+9
-9
@@ -98,9 +98,9 @@ function renderFlows() {
|
||||
const host = $('#flowList'); if (!host) return;
|
||||
const count = $('#flowListCount'); if (count) count.textContent = tr('flow.listCount', { count: app.flows.length });
|
||||
host.innerHTML = app.flows.length ? app.flows.map(flow => `<article class="list-card flow-card ${flow.enabled && !flow.draft ? '' : 'is-disabled'} ${flow.draft ? 'is-draft' : ''}" data-flow-card-id="${esc(flow.id)}" tabindex="0" aria-label="${esc(`${tr('flow.openEditor')}: ${flow.name}`)}">
|
||||
<div class="list-card-head"><div><h3>${esc(flow.name)}${flow.draft ? ` <span class="badge flow-draft-badge">${esc(tr('flow.draft'))}</span>` : ''}</h3><p>${esc(flow.description || tr('flow.defaultDescription'))}</p></div>${flow.draft ? `<button type="button" class="zone-enable-toggle" disabled title="${esc(tr('flow.draftDisabledHint'))}"><span>✎</span>${esc(tr('flow.disabledState'))}</button>` : `<button type="button" class="zone-enable-toggle ${flow.enabled ? 'active' : ''}" data-action="toggle-flow-enabled" data-id="${esc(flow.id)}" data-value="${flow.enabled ? 'false' : 'true'}" aria-label="${esc(tr(flow.enabled ? 'flow.disable' : 'flow.enable'))}" title="${esc(tr('flow.quickToggleHint'))}"><span>${flow.enabled ? '✓' : '○'}</span>${esc(flow.enabled ? tr('flow.enabledState') : tr('flow.disabledState'))}</button>`}</div>
|
||||
<div class="list-card-head"><div><h3>${esc(flow.name)}${flow.draft ? ` <span class="badge flow-draft-badge">${esc(tr('flow.draft'))}</span>` : ''}</h3><p>${esc(flow.description || tr('flow.defaultDescription'))}</p></div>${flow.draft ? `<button type="button" class="zone-enable-toggle" disabled title="${esc(tr('flow.draftDisabledHint'))}"><span>${uiIcon('edit')}</span>${esc(tr('flow.disabledState'))}</button>` : `<button type="button" class="zone-enable-toggle ${flow.enabled ? 'active' : ''}" data-action="toggle-flow-enabled" data-id="${esc(flow.id)}" data-value="${flow.enabled ? 'false' : 'true'}" aria-label="${esc(tr(flow.enabled ? 'flow.disable' : 'flow.enable'))}" title="${esc(tr('flow.quickToggleHint'))}"><span>${uiIcon(flow.enabled ? 'check' : 'circle')}</span>${esc(flow.enabled ? tr('flow.enabledState') : tr('flow.disabledState'))}</button>`}</div>
|
||||
<div class="card-stats"><div class="card-stat"><small>${esc(tr('flow.blocks'))}</small><strong>${flow.nodes?.length || 0}</strong></div><div class="card-stat"><small>${esc(tr('nav.schedules'))}</small><strong>${flow.compiled_schedule_ids?.length || 0}</strong></div><div class="card-stat"><small>${esc(tr('nav.automations'))}</small><strong>${flow.compiled_automation_ids?.length || 0}</strong></div></div>
|
||||
<div class="card-footer"><small>${esc(flow.draft ? tr('flow.draftNoExecution') : tr('flow.compileCount', { schedules: flow.compiled_schedule_ids?.length || 0, automations: flow.compiled_automation_ids?.length || 0 }))}</small><div class="card-menu"><button class="card-primary-action" data-action="edit-flow" data-id="${esc(flow.id)}">${esc(tr('flow.openEditor'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">•••</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-flow" data-id="${esc(flow.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
|
||||
<div class="card-footer"><small>${esc(flow.draft ? tr('flow.draftNoExecution') : tr('flow.compileCount', { schedules: flow.compiled_schedule_ids?.length || 0, automations: flow.compiled_automation_ids?.length || 0 }))}</small><div class="card-menu"><button class="card-primary-action" data-action="edit-flow" data-id="${esc(flow.id)}">${esc(tr('flow.openEditor'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-flow" data-id="${esc(flow.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
|
||||
</article>`).join('') : `<div class="empty"><strong>${esc(tr('flow.emptyTitle'))}</strong>${esc(tr('flow.emptyText'))}</div>`;
|
||||
}
|
||||
|
||||
@@ -445,7 +445,7 @@ function renderFlowEditor() {
|
||||
const meta = FLOW_NODE_META[node.kind] || { title: node.kind, category: 'logic' };
|
||||
return `<article class="flow-node flow-node-${esc(meta.category)} ${(app.flowSelectedNodeIds || []).includes(node.id) ? 'selected' : ''}" data-flow-node="${esc(node.id)}" style="left:${Number(node.x || 0)}px;top:${Number(node.y || 0)}px">
|
||||
<button class="flow-port flow-port-in" type="button" data-flow-input="${esc(node.id)}" title="${esc(tr('flow.connectHere'))}"></button>
|
||||
<div class="flow-node-head"><span>${esc(flowNodeTitle(meta))}</span><button type="button" data-flow-remove="${esc(node.id)}" aria-label="${esc(tr('actions.delete'))}">×</button></div>
|
||||
<div class="flow-node-head"><span>${esc(flowNodeTitle(meta))}</span><button type="button" data-flow-remove="${esc(node.id)}" aria-label="${esc(tr('actions.delete'))}">${uiIcon('close')}</button></div>
|
||||
<div class="flow-node-body">${esc(flowNodeSummary(node))}${node.kind === 'shared_input' && node.config?.input_id ? `<div class="flow-node-current" data-flow-shared-current="${esc(node.config.input_id)}"><span>${esc(tr('flow.sharedInputTestCurrent'))}</span><strong>—</strong></div>` : ''}</div>
|
||||
<button class="flow-port flow-port-out ${app.flowConnectFrom === node.id ? 'armed' : ''}" type="button" data-flow-output="${esc(node.id)}" title="${esc(tr('flow.startConnection'))}"></button>
|
||||
</article>`;
|
||||
@@ -534,7 +534,7 @@ function renderFlowInspector() {
|
||||
else if (node.kind === 'device_action') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>${flowActionFields(c, false)}`;
|
||||
else if (node.kind === 'group_action') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-flow-config="group_id">${flowSelectOptions(app.groups, c.group_id)}</select></label>${flowActionFields(c, true)}`;
|
||||
else if (node.kind === 'ha_service_action') fields = `<div class="two"><label><span>domain</span><input data-flow-config="domain" value="${esc(c.domain || 'switch')}" placeholder="switch"></label><label><span>service</span><input data-flow-config="service" value="${esc(c.service || 'turn_on')}" placeholder="turn_on"></label></div><p class="field-note">${esc(tr('flow.serviceExample'))}</p><label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="switch.gas"></label><label><span>${esc(tr('flow.serviceData'))}</span><textarea rows="5" data-flow-config="data_json">${esc(JSON.stringify(c.data || {}, null, 2))}</textarea></label><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
|
||||
host.innerHTML = `<div class="flow-inspector-head"><div><span class="eyebrow">${esc(tr('flow.blockSettings'))}</span><h3>${esc(flowNodeTitle(meta))}</h3></div><div class="flow-inspector-head-actions"><button type="button" class="secondary flow-inspector-expand" data-action="flow-toggle-inspector" aria-label="${esc(tr('flow.expandSettings'))}">↕</button><button type="button" class="secondary flow-inspector-close" data-action="flow-clear-selection" aria-label="${esc(tr('flow.clearSelection'))}">×</button><button type="button" class="danger flow-inspector-delete" data-flow-remove="${esc(node.id)}">${esc(tr('actions.delete'))}</button></div></div>${fields}<div class="flow-inspector-meta"><small>ID</small><code>${esc(node.id)}</code></div>`;
|
||||
host.innerHTML = `<div class="flow-inspector-head"><div><span class="eyebrow">${esc(tr('flow.blockSettings'))}</span><h3>${esc(flowNodeTitle(meta))}</h3></div><div class="flow-inspector-head-actions"><button type="button" class="secondary flow-inspector-expand" data-action="flow-toggle-inspector" aria-label="${esc(tr('flow.expandSettings'))}">${uiIcon('expand-vertical')}</button><button type="button" class="secondary flow-inspector-close" data-action="flow-clear-selection" aria-label="${esc(tr('flow.clearSelection'))}">${uiIcon('close')}</button><button type="button" class="danger flow-inspector-delete" data-flow-remove="${esc(node.id)}">${esc(tr('actions.delete'))}</button></div></div>${fields}<div class="flow-inspector-meta"><small>ID</small><code>${esc(node.id)}</code></div>`;
|
||||
}
|
||||
|
||||
function flowTextComparisonFields(c, numeric = false) { const options = numeric ? flowOperatorOptions(c.operator || 'eq') : [['eq', '='], ['neq', '≠']].map(([v, l]) => `<option value="${v}" ${v === (c.operator || 'eq') ? 'selected' : ''}>${l}</option>`).join(''); return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${options}</select></label><label><span>${esc(tr('flow.value'))}</span><input data-flow-config="value" value="${esc(c.value ?? '')}"></label></div>`; }
|
||||
@@ -864,9 +864,9 @@ function renderFlowPresetPreview(preset) {
|
||||
const req = flowPresetRequirements(preset);
|
||||
const favorite = flowPresetFavoriteIds().has(preset.id);
|
||||
const reqMarkup = req.requirements.length
|
||||
? req.requirements.map(item => `<span class="flow-template-requirement ${item.ok ? 'ok' : 'missing'} ${item.info ? 'info' : ''}">${item.ok ? '✓' : '!'} ${esc(item.label)}</span>`).join('')
|
||||
: `<span class="flow-template-requirement ok">✓ ${esc(tr('flow.templateRequirementsReady'))}</span>`;
|
||||
host.innerHTML = `<div class="flow-template-preview-head"><div><span class="eyebrow">${esc(tr('flow.templatePreview'))}</span><h3>${esc(flowPresetText(preset.name) || preset.id)}</h3></div><button type="button" class="flow-template-favorite ${favorite ? 'active' : ''}" data-flow-template-favorite="${esc(preset.id)}" title="${esc(favorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}" aria-label="${esc(favorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}">${favorite ? '★' : '☆'}</button></div>
|
||||
? req.requirements.map(item => `<span class="flow-template-requirement ${item.ok ? 'ok' : 'missing'} ${item.info ? 'info' : ''}">${item.ok ? uiIcon('check') : '!'} ${esc(item.label)}</span>`).join('')
|
||||
: `<span class="flow-template-requirement ok">${uiIcon('check')} ${esc(tr('flow.templateRequirementsReady'))}</span>`;
|
||||
host.innerHTML = `<div class="flow-template-preview-head"><div><span class="eyebrow">${esc(tr('flow.templatePreview'))}</span><h3>${esc(flowPresetText(preset.name) || preset.id)}</h3></div><button type="button" class="flow-template-favorite ${favorite ? 'active' : ''}" data-flow-template-favorite="${esc(preset.id)}" title="${esc(favorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}" aria-label="${esc(favorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}">${uiIcon(favorite ? 'star-filled' : 'star')}</button></div>
|
||||
<p>${esc(flowPresetText(preset.description))}</p>
|
||||
<div class="flow-template-preview-stats"><span>${esc(tr('flow.templateNodesCount', { count: preset.flow.nodes.length }))}</span><span>${esc(tr('flow.templateEdgesCount', { count: preset.flow.edges.length }))}</span></div>
|
||||
${flowPresetPreviewGraph(preset)}
|
||||
@@ -902,7 +902,7 @@ function renderFlowPresetBrowser(category = '') {
|
||||
const emptyKey = query ? 'flow.templateNoResults' : flowPresetActiveCategory === 'favorites' ? 'flow.templateNoFavorites' : flowPresetActiveCategory === 'recent' ? 'flow.templateNoRecent' : 'flow.templateNoResults';
|
||||
const cards = active.map(preset => {
|
||||
const isFavorite = favorites.has(preset.id), selected = preset.id === flowPresetPreviewId;
|
||||
return `<article class="flow-template-card ${selected ? 'selected' : ''}"><button type="button" class="flow-template-card-main" data-flow-template-preview="${esc(preset.id)}"><strong>${esc(flowPresetText(preset.name) || preset.id)}</strong><span>${esc(flowPresetText(preset.description))}</span></button><button type="button" class="flow-template-favorite ${isFavorite ? 'active' : ''}" data-flow-template-favorite="${esc(preset.id)}" title="${esc(isFavorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}" aria-label="${esc(isFavorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}">${isFavorite ? '★' : '☆'}</button></article>`;
|
||||
return `<article class="flow-template-card ${selected ? 'selected' : ''}"><button type="button" class="flow-template-card-main" data-flow-template-preview="${esc(preset.id)}"><strong>${esc(flowPresetText(preset.name) || preset.id)}</strong><span>${esc(flowPresetText(preset.description))}</span></button><button type="button" class="flow-template-favorite ${isFavorite ? 'active' : ''}" data-flow-template-favorite="${esc(preset.id)}" title="${esc(isFavorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}" aria-label="${esc(isFavorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}">${uiIcon(isFavorite ? 'star-filled' : 'star')}</button></article>`;
|
||||
}).join('');
|
||||
host.innerHTML = `<section class="flow-template-category-single"><div class="flow-template-category-head"><strong>${esc(flowPresetCategoryLabel(flowPresetActiveCategory))}</strong><span>${esc(flowPresetCategoryHint(flowPresetActiveCategory))}</span></div>${cards ? `<div class="flow-template-category-grid">${cards}</div>` : `<div class="empty compact"><span>${esc(tr(emptyKey))}</span></div>`}</section>`;
|
||||
if (!active.some(preset => preset.id === flowPresetPreviewId)) flowPresetPreviewId = active[0]?.id || '';
|
||||
@@ -983,7 +983,7 @@ function openFlowDryRun() {
|
||||
function renderFlowDryRunResult(result) {
|
||||
const host = $('#flowTestResults');
|
||||
const actionName = id => flowNodeById(id) ? `${flowNodeTitle(FLOW_NODE_META[flowNodeById(id).kind])}: ${flowNodeSummary(flowNodeById(id))}` : id;
|
||||
host.innerHTML = `<div class="flow-test-summary"><strong>${esc(result.summary || tr('flow.dryRun'))}</strong><span>${esc(tr('flow.compiledPreview', result.compiled || { schedules: 0, automations: 0 }))}</span></div>${(result.actions || []).map(action => `<article class="flow-test-action ${action.would_execute ? 'pass' : 'blocked'}"><div><strong>${esc(actionName(action.node_id))}</strong><span class="badge ${action.would_execute ? 'active' : ''}">${esc(action.would_execute ? tr('flow.wouldExecute') : action.matched ? tr('flow.blockedByOwnership') : tr('flow.conditionFalse'))}</span></div>${action.blocked_reason ? `<p>${esc(tr('flow.blockReason'))}: <code>${esc(action.blocked_reason)}</code></p>` : ''}<div class="flow-trace">${(action.trace || []).map(item => `<div><span>${item.matched ? '✓' : '×'} ${esc(flowNodeTitle(FLOW_NODE_META[item.kind] || { title: item.kind }))}</span><code>${esc(JSON.stringify(item.actual))}</code></div>`).join('')}</div></article>`).join('')}`;
|
||||
host.innerHTML = `<div class="flow-test-summary"><strong>${esc(result.summary || tr('flow.dryRun'))}</strong><span>${esc(tr('flow.compiledPreview', result.compiled || { schedules: 0, automations: 0 }))}</span></div>${(result.actions || []).map(action => `<article class="flow-test-action ${action.would_execute ? 'pass' : 'blocked'}"><div><strong>${esc(actionName(action.node_id))}</strong><span class="badge ${action.would_execute ? 'active' : ''}">${esc(action.would_execute ? tr('flow.wouldExecute') : action.matched ? tr('flow.blockedByOwnership') : tr('flow.conditionFalse'))}</span></div>${action.blocked_reason ? `<p>${esc(tr('flow.blockReason'))}: <code>${esc(action.blocked_reason)}</code></p>` : ''}<div class="flow-trace">${(action.trace || []).map(item => `<div><span>${uiIcon(item.matched ? 'check' : 'close')} ${esc(flowNodeTitle(FLOW_NODE_META[item.kind] || { title: item.kind }))}</span><code>${esc(JSON.stringify(item.actual))}</code></div>`).join('')}</div></article>`).join('')}`;
|
||||
}
|
||||
async function runFlowDryRun() {
|
||||
if (!app.flowDraft) return;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ function toast(message, error = false) {
|
||||
const host = $('#toastStack'); if (!host) return;
|
||||
const item = document.createElement('div');
|
||||
item.className = `toast-item ${error ? 'error' : 'success'}`;
|
||||
item.innerHTML = `<span class="toast-icon">${error ? '!' : '✓'}</span><div class="toast-copy"><strong>${esc(error ? tr('toast.errorTitle') : tr('toast.successTitle'))}</strong><span>${esc(message)}</span></div><button class="toast-close" type="button" aria-label="${esc(tr('actions.close'))}">×</button><i class="toast-progress"></i>`;
|
||||
item.innerHTML = `<span class="toast-icon">${error ? '!' : uiIcon('check')}</span><div class="toast-copy"><strong>${esc(error ? tr('toast.errorTitle') : tr('toast.successTitle'))}</strong><span>${esc(message)}</span></div><button class="toast-close" type="button" aria-label="${esc(tr('actions.close'))}">${uiIcon('close')}</button><i class="toast-progress"></i>`;
|
||||
host.appendChild(item);
|
||||
requestAnimationFrame(() => item.classList.add('show'));
|
||||
const remove = () => { item.classList.remove('show'); item.classList.add('leaving'); setTimeout(() => item.remove(), 220); };
|
||||
|
||||
+2
-2
@@ -280,9 +280,9 @@ function renderCustomBuilder() {
|
||||
const selected = app.customChartSeries.map((key, index) => customSeriesDefinition(key, index)).filter(Boolean);
|
||||
host.innerHTML = `<div class="panel custom-chart-panel"><div class="chart-title-row"><div><h3>${esc(tr('history.customTitle'))}</h3><p>${esc(tr('history.customDescription'))}</p></div></div>
|
||||
<div class="custom-chart-add"><select id="customSeriesSelect">${options.map(([value, label]) => `<option value="${esc(value)}">${esc(label)}</option>`).join('')}</select><button class="secondary" data-history-action="add-series">${esc(tr('history.addSeries'))}</button></div>
|
||||
<div class="custom-series-list">${selected.length ? selected.map((item, index) => `<span class="custom-series-chip"><i style="--chip-color:${esc(item.color)}"></i>${esc(item.label)}<button data-history-action="remove-series" data-index="${index}" aria-label="${esc(tr('actions.remove'))}">×</button></span>`).join('') : `<span class="field-note">${esc(tr('history.noCustomSeries'))}</span>`}</div>
|
||||
<div class="custom-series-list">${selected.length ? selected.map((item, index) => `<span class="custom-series-chip"><i style="--chip-color:${esc(item.color)}"></i>${esc(item.label)}<button data-history-action="remove-series" data-index="${index}" aria-label="${esc(tr('actions.remove'))}">${uiIcon('close')}</button></span>`).join('') : `<span class="field-note">${esc(tr('history.noCustomSeries'))}</span>`}</div>
|
||||
<div class="custom-chart-save"><input id="customChartName" placeholder="${esc(tr('history.chartName'))}"><button class="secondary" data-history-action="save-chart">${esc(tr('actions.save'))}</button><button class="primary" data-history-action="copy-chart-link">${esc(tr('history.copyLink'))}</button></div>
|
||||
<div class="saved-chart-list">${app.savedCharts.length ? app.savedCharts.map(item => `<div class="saved-chart-row"><button data-history-action="load-chart" data-id="${esc(item.id)}"><strong>${esc(item.name)}</strong><small>${esc(item.series.length)} ${esc(tr('history.series'))}</small></button><button class="danger" data-history-action="delete-chart" data-id="${esc(item.id)}">×</button></div>`).join('') : `<span class="field-note">${esc(tr('history.noSavedCharts'))}</span>`}</div>
|
||||
<div class="saved-chart-list">${app.savedCharts.length ? app.savedCharts.map(item => `<div class="saved-chart-row"><button data-history-action="load-chart" data-id="${esc(item.id)}"><strong>${esc(item.name)}</strong><small>${esc(item.series.length)} ${esc(tr('history.series'))}</small></button><button class="danger" data-history-action="delete-chart" data-id="${esc(item.id)}">${uiIcon('close')}</button></div>`).join('') : `<span class="field-note">${esc(tr('history.noSavedCharts'))}</span>`}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ if ('serviceWorker' in navigator && !APP_BASE.startsWith('/api/hassio_ingress/')
|
||||
|
||||
async function startApplication() {
|
||||
try {
|
||||
hydrateUiIcons();
|
||||
observeTopbarHeight();
|
||||
applyTheme();
|
||||
await loadLanguages();
|
||||
|
||||
+13
-1
@@ -22,6 +22,18 @@ function logDisplayMessage(item) {
|
||||
return `${message} · ${tr('history.haSensor')}: ${sensor}${zoneName ? ` · ${tr('common.zone')}: ${zoneName}` : ''}`;
|
||||
}
|
||||
|
||||
|
||||
function logCategoryIcon(category) {
|
||||
return {
|
||||
device: 'devices',
|
||||
zone: 'zones',
|
||||
automation: 'automation',
|
||||
integration: 'integration',
|
||||
settings: 'sliders',
|
||||
system: 'system-info',
|
||||
}[category] || 'system-info';
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
renderLogRetention();
|
||||
try {
|
||||
@@ -29,7 +41,7 @@ async function loadLogs() {
|
||||
const level = $('#logLevelFilter')?.value || 'all', category = $('#logCategoryFilter')?.value || 'all';
|
||||
const logs = (data.events || []).filter(item => (level === 'all' || item.level === level) && (category === 'all' || logCategory(item.kind) === category));
|
||||
$('#logList').innerHTML = logs.length
|
||||
? logs.map(item => `<div class="log-row ${esc(item.level)} category-${esc(logCategory(item.kind))}"><time>${esc(new Date(item.timestamp).toLocaleTimeString(locale()))}</time><span class="log-category">${esc(logCategory(item.kind))}</span><span class="kind">${esc(item.kind)}${logNotificationBadge(item)}</span><span class="message">${esc(logDisplayMessage(item))}</span></div>`).join('')
|
||||
? logs.map(item => `<div class="log-row ${esc(item.level)} category-${esc(logCategory(item.kind))}"><time>${esc(new Date(item.timestamp).toLocaleTimeString(locale()))}</time><span class="log-category">${uiIcon(logCategoryIcon(logCategory(item.kind)))}${esc(logCategory(item.kind))}</span><span class="kind">${esc(item.kind)}${logNotificationBadge(item)}</span><span class="message">${esc(logDisplayMessage(item))}</span></div>`).join('')
|
||||
: `<div class="empty"><strong>${esc(tr('logs.emptyTitle'))}</strong>${esc(tr('logs.emptyText'))}</div>`;
|
||||
} catch (error) { toast(error.message, true); }
|
||||
}
|
||||
|
||||
+2
-2
@@ -128,7 +128,7 @@ function rebuildCustomSelectMenu(select, state) {
|
||||
text.textContent = option.textContent || option.label || option.value || '—';
|
||||
const check = document.createElement('b');
|
||||
check.setAttribute('aria-hidden', 'true');
|
||||
check.textContent = option.selected ? '✓' : '';
|
||||
check.innerHTML = option.selected ? uiIcon('check') : '';
|
||||
button.append(text, check);
|
||||
|
||||
button.addEventListener('click', event => {
|
||||
@@ -310,7 +310,7 @@ function enhanceCustomSelect(select) {
|
||||
const chevron = document.createElement('span');
|
||||
chevron.className = 'custom-select-chevron';
|
||||
chevron.setAttribute('aria-hidden', 'true');
|
||||
chevron.textContent = '⌄';
|
||||
chevron.innerHTML = uiIcon('chevron-down');
|
||||
trigger.append(value, chevron);
|
||||
wrapper.appendChild(trigger);
|
||||
select.insertAdjacentElement('afterend', wrapper);
|
||||
|
||||
@@ -47,7 +47,7 @@ function renderSensorAliases() {
|
||||
const metricEntities = metricHaEntities(), flowEntities = flowHaEntities();
|
||||
host.innerHTML = entities.length ? entities.map(entity => {
|
||||
const badges = `${metricEntities.has(entity) ? `<span class="sensor-source-badge metrics">${esc(tr('settings.sensorMetricBadge'))}</span>` : ''}${flowEntities.has(entity) ? `<span class="sensor-source-badge flow">${esc(tr('settings.sensorFlowBadge'))}</span>` : ''}`;
|
||||
return `<div class="sensor-alias-row"><div class="sensor-alias-entity"><span class="mono" title="${esc(entity)}">${esc(entity)}</span>${badges ? `<span class="sensor-source-badges">${badges}</span>` : ''}</div><input data-sensor-alias="${esc(entity)}" value="${esc(app.sensorAliases?.[entity] || '')}" placeholder="${esc(tr('settings.aliasPlaceholder'))}"><button type="button" class="sensor-alias-clear" data-clear-sensor-alias="${esc(entity)}" title="${esc(tr('actions.clear'))}">×</button></div>`;
|
||||
return `<div class="sensor-alias-row"><div class="sensor-alias-entity"><span class="mono" title="${esc(entity)}">${esc(entity)}</span>${badges ? `<span class="sensor-source-badges">${badges}</span>` : ''}</div><input data-sensor-alias="${esc(entity)}" value="${esc(app.sensorAliases?.[entity] || '')}" placeholder="${esc(tr('settings.aliasPlaceholder'))}"><button type="button" class="sensor-alias-clear" data-clear-sensor-alias="${esc(entity)}" title="${esc(tr('actions.clear'))}">${uiIcon('close')}</button></div>`;
|
||||
}).join('') : `<div class="empty compact">${esc(tr('settings.noSensorAliases'))}</div>`;
|
||||
}
|
||||
|
||||
|
||||
+30
-42
@@ -450,6 +450,15 @@ h3 {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.ui-icon {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
flex: 0 0 auto;
|
||||
vertical-align: -0.125em;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.power-icon {
|
||||
display: block;
|
||||
width: 1em;
|
||||
@@ -3516,28 +3525,24 @@ button.outside-pill:focus-visible {
|
||||
opacity: .7
|
||||
}
|
||||
|
||||
.category-device .log-category:before {
|
||||
content: '▣ '
|
||||
.log-category {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .32rem;
|
||||
}
|
||||
|
||||
.category-zone .log-category:before {
|
||||
content: '◎ '
|
||||
.log-category .ui-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.category-automation .log-category:before {
|
||||
content: '⚙ '
|
||||
.flow-fit-button-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.category-integration .log-category:before {
|
||||
content: '↔ '
|
||||
}
|
||||
|
||||
.category-settings .log-category:before {
|
||||
content: '☷ '
|
||||
}
|
||||
|
||||
.category-system .log-category:before {
|
||||
content: '• '
|
||||
.flow-fit-button .ui-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
@media(max-width:720px) {
|
||||
@@ -8149,12 +8154,6 @@ button {
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.flow-fit-button::before {
|
||||
content: "⊡";
|
||||
font-size: 17px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.flow-palette {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -9239,12 +9238,6 @@ body.flow-editor-open {
|
||||
overflow: hidden;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.flow-fit-button::before {
|
||||
content: "⊡";
|
||||
font-size: 17px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#controlPlan .plan-card {
|
||||
@@ -9321,13 +9314,6 @@ body.flow-editor-open {
|
||||
font-size: 0 !important;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.flow-fit-button::before {
|
||||
content: "⤢";
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-height: 700px) {
|
||||
@@ -9370,13 +9356,6 @@ body.flow-editor-open {
|
||||
font-size: 0 !important;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.flow-fit-button::before {
|
||||
content: "⤢";
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-height: 700px) and (min-width: 500px) {
|
||||
@@ -9906,3 +9885,12 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
max-width: calc(100vw - 16px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px), (orientation: landscape) and (max-height: 700px) {
|
||||
.flow-fit-button-icon { display: inline-grid; place-items: center; }
|
||||
.flow-fit-button-label { display: none; }
|
||||
}
|
||||
@media (orientation: landscape) and (max-height: 700px) and (min-width: 500px) {
|
||||
.flow-fit-button-icon { display: none; }
|
||||
.flow-fit-button-label { display: inline; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user