This commit is contained in:
Mateusz Gruszczyński
2026-09-01 13:16:10 +02:00
parent 7e0160834d
commit dc20267787
13 changed files with 164 additions and 38 deletions
Generated
+1 -1
View File
@@ -633,7 +633,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.9.0"
version = "0.9.1"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
version = "0.9.0"
version = "0.9.1"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+19 -19
View File
@@ -1,11 +1,11 @@
b38e05f8a00969270280b0062ee129fccee39ae9c11cb614a6ad7b2b7ae2dfc7 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
71363f41de7b4cd53b99e5c97fc6c6f8d5c1065f8fa4cbd02496c928d31be195 ./Cargo.lock
c68db6fcbaea3aee0864ed78d12d84aa6e669f1865666d97aca7ac530918d75e ./Cargo.toml
d16e2c4867f9e651053bbbb080927134e7e72df96ca1b26f62f7818f0a1f7f09 ./Cargo.lock
c180bd9e6cdd1db88d195237ccce4e8a8bef44888468e0fb1afd3b5e5dbefb6f ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
0d465ef4bc80d52bbca3357b57d61d18724ca322e930cec47274a61a9ffd5d44 ./README.md
57ed2504d5924581030d74ca4899813532165d08f6317704ee2f2002d4cc8ad0 ./README.md
ec0f495ee6a618bdec10b260ee7838f7a872d8c5b0aa91c546226c1ad818f601 ./build.rs
c5e8a97b7b35ff6c14ec3e36aca6a88a02d4791878fffb8f498fd141d65ecc9a ./docs/API.md
027a96e4254e45305f5f573d3562920d4f253cba32bcd2c96b15eaaae94b3cfd ./docs/API.md
a4571b7d31c96be8b3f540574444742f99a208fc0cdf27a14bebad70bc916ab3 ./home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py
@@ -16,7 +16,7 @@ ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assista
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
b13f95ff5b6d2fbf884142360c907ef4e26b44d5097d596624e16b1b554be05d ./home-assistant/custom_components/gree_controller/manifest.json
01df8a32e3159ce761f395b859e4b00f0485c8cf8b37cfdc844dbdf979268bce ./home-assistant/custom_components/gree_controller/manifest.json
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
1cae12876eb81085910907cf1fcf340a18dcba4f8c967ef222c9496325057849 ./home-assistant/custom_components/gree_controller/sensor.py
@@ -25,8 +25,8 @@ b13f95ff5b6d2fbf884142360c907ef4e26b44d5097d596624e16b1b554be05d ./home-assista
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
953f215e80df7896a6b5d1da8c00d8e8d954f7c8aaed488e846cbd461f688cfc ./lang/en.json
b0baaff08e70757841c238c5f689fb054774608ef70a4d43f75cb2ca66cd53d2 ./lang/pl.json
09f9b9184f09ae8439368299ae2922a04738266fa261240bcc74adba549522db ./lang/en.json
0d566784bb9c8f0a4420f24ffd86946919d7075ce867d0d78606144c26945559 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
@@ -39,7 +39,6 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
4c8b0d10d83d6130ed6e2703c233ac380b25a6c57fc23a29821f823df90331f9 ./src/api.rs
50b653f5ae1c9d24eb2e158fb4af713f5ccabe259c83053fa3cd2a310c7780da ./src/api/assets.rs
6c34b1294d76b0eba4c56bfd0bf69bdd502de0e40838104f6ddf2db4e922683d ./src/api/auth.rs
08a732a445f44d34c644109762ac389b58c96d0fe47b309ce34f4479d67c3712 ./src/api/automations.rs
@@ -57,8 +56,8 @@ d3ddc310a4033e400f521babec02ec06990092d7349c40cd271a3d8406f70f4a ./src/api/publ
74c156717b37e83d329f7c52ac622e02f690621544c4eb07c4502194db55e6e9 ./src/api/system.rs
681cccb8d09f4ad9c2e6467a4dbe2f9d991125775a5d2bfc9043daf940fecd29 ./src/api/websocket.rs
9d8ab056dbd2047fd00062e7252c3e9bb972c988a56de23daea61b8be871bd61 ./src/api/zones.rs
4c8b0d10d83d6130ed6e2703c233ac380b25a6c57fc23a29821f823df90331f9 ./src/api.rs
2b6eecead75cd4cdb98a9f74dd1c374dbf11ebfa716abbc3476aa12398af9acd ./src/config.rs
369455b5ffb125f1a80b767f62fd8c84a953adb69548dbd8adb653a6fbaaa45b ./src/db.rs
24580b279cfcd5388ba584eaba2c9bb729b8aae81e77b781c23fb804c5b5fa5d ./src/db/climate.rs
3de3fa4262be00029dde2344c4cfa41acd4acc0adec176f344761ff35ea730b6 ./src/db/configuration.rs
56b0efe84dbf6bd3ee2397b099220a68966311c187f4d5b53007a1693a5ef327 ./src/db/core_devices.rs
@@ -68,7 +67,7 @@ f3bd91d0bdb7699f319b9341d928d3cc3b25f759653fcd2095a5c5f695d08fac ./src/db/event
7e2fbce70f40aef360534ba38c6700163db77cb7288b3bf58728fce72a126435 ./src/db/schedules_automations.rs
35cbb521a8ec456756496b8aee71f9dfebad528a2cf2653ba8b48f6035be0c01 ./src/db/tests.rs
a384ea4042d1d11692b5f8693df1f54850dcc4e80726780b44b2b096bf667b78 ./src/db/zone_history.rs
952ea5906d39ef917ad71edc345f76b29c07c63079644fd5699b12aa254cb35f ./src/engine.rs
369455b5ffb125f1a80b767f62fd8c84a953adb69548dbd8adb653a6fbaaa45b ./src/db.rs
d418f27edde4a8a8ea89817899abfc48df632ec581deacf64db1dc409eef920f ./src/engine/automations.rs
910248f30ac9d697816458cd393c5f7d4bbdd3a12e48cb41cbb3ddb68a1ce395 ./src/engine/commands.rs
d91c50e68baa8f90a30fd94be1fda8cf548ed7386c1058f14f8d5b32625c0b25 ./src/engine/control_plan.rs
@@ -86,14 +85,14 @@ a9188b588b2617ce1547a45d46141f7e5f1bb0027a48675dd86b274f807d0875 ./src/engine/t
3fe9825d9ff5f41c9f988489ebe606b4700322e553cbe6cae1bec7c82affd25c ./src/engine/tests.rs
6f4541bad148d172a44cd3ddd22b49dc97e4b755554a7a8820222169090e7022 ./src/engine/zone_actions.rs
856bd291229ae9d0b3f21fb7fd47058b7af5a18bd195e48829f8162533613472 ./src/engine/zone_control.rs
952ea5906d39ef917ad71edc345f76b29c07c63079644fd5699b12aa254cb35f ./src/engine.rs
4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs
c6ff66da9ad08506f839ec56a869ead3abc824b5311e0ea9244517d11f7f4207 ./src/home_assistant.rs
6f9ef85cd53ba030b477bc3c186f0f7e762674124210eabcb0686f2806eaada1 ./src/influxdb.rs
feb50ecd60b9a9fe9de1f7d8e1e26caab5435d59347468f6266a318f431bc3d3 ./src/influxdb/codec.rs
553b44ec0321e62480059bc2451fd34ec7aabe9fc87fc717daba0c4b55679d53 ./src/influxdb/query.rs
5e4d94f8a010df05bae9acab72c518080d2dba94d054df95b87e492305357222 ./src/influxdb/write.rs
6f9ef85cd53ba030b477bc3c186f0f7e762674124210eabcb0686f2806eaada1 ./src/influxdb.rs
c3ff7232a2fa1d29428872a10cfd72ee60a3a1faaa662eff6c2d8c7dffc1ed3a ./src/main.rs
e935e1fcf0d379b627df5d5055dbdad3dd55786fde7191376918b05afdde1727 ./src/models.rs
fca124ec30b5c9286f0f44cc18e8953bb2809f5009d4d521deaebfdeb00c1bd1 ./src/models/automation.rs
60ff96965cf9531b3964add783a6a45578aa1986844e99a239f182eb4f962d5b ./src/models/control_plan.rs
8228b9b6d9e8b1d13de29ce88500beacd88c8877893b7a3e41a4bd1e237ca36c ./src/models/defaults.rs
@@ -103,9 +102,9 @@ cfbaa853a5185c4be572588577b313a2d618837f7c07a3860d0e8d2cca04ee60 ./src/models/h
b1af7f8d4c3607b1c0ae978feaab4dfa03faa0154c7328712bb4eeafc27faa02 ./src/models/runtime.rs
e77f8cdcb5fc0e51cdbf2527a69585a2f6f9089e159d1e45ec4a48735ab0e304 ./src/models/temporary_thermostat.rs
0bdc68fc1b3d1fd4d1e1253bd48b5c149cfc9b87e2fedb13288b3ea8a096af3d ./src/models/zone.rs
e935e1fcf0d379b627df5d5055dbdad3dd55786fde7191376918b05afdde1727 ./src/models.rs
9c735d7c475f21f2f388c21be784376f7184f0bf18af1886544ef51585a3b290 ./src/notifications.rs
7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs
f1f765331469f4219a551414833c19b6de413114c22fdd0468e1f417659553c9 ./src/protocol/gree.rs
c935e441933ba8709432cc3ac33ed868043a89188d0f48ea6fbfe34b1f8936a3 ./src/protocol/gree/binding.rs
563ded1758b135eb523f1fa0b1bf55fe5430ef55935b301fd6d79cbb980e1820 ./src/protocol/gree/commands.rs
5d4328199e2285016fd1941e909e5c09f7cdf9fd21ab6538c1bf8bc2f4af3ba3 ./src/protocol/gree/core.rs
@@ -115,35 +114,36 @@ a96061fbbcadac6c2df2e9708a48dcf819b951eb147f32c7d0d285ac5ae2491b ./src/protocol
45d0cd0ea40b9255bd811362547f5a667dac6bc7324074726130baf60aa41d2b ./src/protocol/gree/polling.rs
35fc903404045129e3046b7997e941b88f98ceb42485cccc872c2e4aa530c75b ./src/protocol/gree/tests.rs
55c2d615e76c9e4b08c4067ea734e81d81915144e637f3b51c19bea4fc879137 ./src/protocol/gree/transport.rs
f1f765331469f4219a551414833c19b6de413114c22fdd0468e1f417659553c9 ./src/protocol/gree.rs
a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol/mod.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
cd5d0befe9f51a7067201a6eaa8d8ed72182d359e42cbdc67c80aa12d722654b ./src/queries/entities.rs
2c5fba462b72158b04dbaaa53c8536263f8d72a1338a6c4ff06cb28a36f6ae27 ./src/queries/ha_history.rs
2b46a8cdc5efb5f930dd8ff7b9f6c0e720cbca096a51d9ae047711dd280fa3b8 ./src/queries/maintenance.rs
f688db6f440ee7b0129ccc22838de0ee0e0cf2b6376173b05f853ca2555c2c1e ./src/queries/schema.rs
f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
6ee23fcaa62231d15006db078d4f763d1b67d2365fcc2d736d8a7da8df5ea382 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
de66cab9b650cb448814bfeda6d216e168dd3e6f07a1a438e4a9de9acbe6b798 ./web/404.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
3c2a8f09e64a7bc3002aee713a0edfb1441e95711de5fb60b9d177e8014192a4 ./web/index.html
2f29bd02e3a74089b28621978882b1191e26d7ec612e1ccd306b65f204b68e99 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
b3d5cb473bf9771de78e14c7de730cb86c73f8cdbb6344da67ba7d6abac879ea ./web/js/bootstrap.js
64a43e2142a69ebddf4160c0d0d3b573fa379c650431090fd15c9ba3b62bc4e7 ./web/js/charts.js
cf8d97770985b8cf920382725fa33d5f61497a639544bfc528af72b276de0b6a ./web/js/core.js
6e421388c7f6deb2aa42f1205dfdc52839f9f178fbd672129972528b8517a870 ./web/js/dashboard.js
2ce40c25e2e0bf4dbdd77f22155fb9ed5f64a7f866dd2fe40df188cdb38183d8 ./web/js/entities.js
77fbf1ad0e0f12a5ca8ea65716fb993cba07bdfe03cd6d43966ae79efb971908 ./web/js/events.js
1bc7b2c4ab81b9942dd04b8b445de087d439f56c8dac3531fa4a62b8287d4a60 ./web/js/entities.js
e448b48ead4b63ca4c498f2743c571876789a26fd4233cb63b00cbf2e84ca4ad ./web/js/events.js
2fe6c804f86b0613a50a56c54f7a0cbee77273759edf3e6fec36ac77d35edbd7 ./web/js/forms.js
be07a341ba4b1f514a5e13553494194b22aa87108cb04de40635a0158921780b ./web/js/history.js
6d4a9f17ff241eaf44e8d2260163d9226fca3c8d6e1a287d31a2c96b976532f9 ./web/js/main.js
9aa199a9ca8d1109cc06d7a7d69f5c5ed45c53f488128ca520258e9c91c31352 ./web/js/main.js
e27d5bb4a1579fd5b1d9762a5067b953004ca8ae63506af4e8c2cad754c0c532 ./web/js/navigation.js
935a9282f8c7a0e1405752931607751cb0d314eabca74e3f1e0b5f21e1b93988 ./web/js/realtime.js
45a666dbc1cddcc0ece507130cde7719f6232c710b4288d6a16e1232791f2a3d ./web/js/router.js
f9d23d7142ebcd77085ad30b8b5bf927b754b51f10976309d5b0a3f22f3cfafb ./web/js/settings-ui.js
a189f4f7cc253087ac271b3c5b7a82751176032fc0f7040f073cdc451d354e06 ./web/js/settings.js
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
7e52b34a45d70714659e18aa92839b0059d67cbbeb2774ee3a59d452421a6139 ./web/styles.css
a447e27f1c6f5cd126e4ea08781603afaca0877c495bc38095e192618efbd942 ./web/styles.css
bc95830ec8273818ffde0a6b50eb3cc9ecbeee65a7a1d3aeff08cde86d05dc7b ./web/sw.js
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
+2 -2
View File
@@ -2,7 +2,7 @@
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
**Current release: 0.9.0**
**Current release: 0.9.1**
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
@@ -181,7 +181,7 @@ A **Zone** is the thermostat owner for a device. It decides demand from room tem
### Compressor protection
Controller-side compressor protection is enabled by default and uses a 3-minute window. The switch and duration are available under **Settings → GREE**. Thermostat, group and whole-house starts or Heat/Cool reversals that fall inside the window are kept as visible pending tasks instead of being discarded. The pending task is shown on both the thermostat and its device card, can be cancelled individually, and all pending tasks can be cancelled from the Thermostats header. Safety OFF commands remain immediate.
Controller-side compressor protection is enabled by default and uses a 3-minute window. The switch and duration are available under **Settings → GREE**. Thermostat, group and whole-house starts or Heat/Cool reversals that fall inside the window are kept as visible pending tasks instead of being discarded. The pending task is shown on both the thermostat and its device card. A permanent **Queue** button above Thermostats opens a global modal sorted by execution time, with device/zone, command, owner, protection deadline, live countdown, per-task cancellation and **Cancel all**. Safety OFF commands remain immediate.
This distinction is important: direct device control intentionally behaves differently from thermostat control.
+3 -3
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.9.0**.
HTTP and WebSocket API for GREE Controller **0.9.1**.
[← Main documentation](../README.md)
@@ -212,7 +212,7 @@ Response:
{
"status": "ok",
"name": "gree-controller",
"version": "0.9.0",
"version": "0.9.1",
"uptime_seconds": 1234,
"control_ready": true,
"time": "2026-08-30T06:54:00Z"
@@ -236,7 +236,7 @@ Returns the initial Web UI snapshot:
"settings": {},
"outdoor_temperature": null,
"system": {
"version": "0.9.0",
"version": "0.9.1",
"uptime_seconds": 1234,
"auth_required": false,
"control_ready": true,
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.9.0",
"version": "0.9.1",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
+14 -2
View File
@@ -783,7 +783,7 @@
"groups.enableControlFirst": "Turn the group on first.",
"settings.compressorProtection": "Compressor protection",
"settings.compressorProtectionTime": "Protection time (min)",
"settings.compressorProtectionHint": "Recommended: 3 minutes. Starts and Heat/Cool changes requested during protection are queued and can be cancelled from Thermostats.",
"settings.compressorProtectionHint": "Recommended: 3 minutes. Starts and Heat/Cool changes requested during protection are queued. Open the full list with the Queue button above Thermostats.",
"zones.compressorQueueTitle": "Waiting for compressor protection",
"zones.queuedPowerOn": "Queued: power on · {mode} · {temperature}°C",
"zones.queuedModeChange": "Queued: change mode to {mode} · {temperature}°C",
@@ -794,6 +794,18 @@
"zones.queuedCancelled": "Queued task cancelled.",
"zones.noQueuedTask": "There is no queued task to cancel.",
"zones.queuedCancelledAll": "Cancelled queued tasks: {count}.",
"zones.queueCancelledStatus": "Protection task cancelled — waiting for a new command"
"zones.queueCancelledStatus": "Protection task cancelled — waiting for a new command",
"zones.queueOverview": "Queue",
"zones.queueEyebrow": "Compressor protection",
"zones.queueTitle": "Command queue",
"zones.queueHint": "Commands waiting for compressor protection to end. You can cancel a single task or clear the whole queue.",
"zones.queuePendingLabel": "Waiting tasks",
"zones.queueEmptyTitle": "The queue is empty",
"zones.queueEmptyText": "There are no commands waiting for compressor protection to end.",
"zones.queueReasonProtection": "Compressor protection",
"zones.queueSource": "Source: {source}",
"zones.queueRemaining": "Remaining {time}",
"zones.queueReady": "Ready to execute",
"zones.queueCancelAllConfirm": "Cancel all tasks ({count}) waiting for compressor protection?"
}
}
+14 -2
View File
@@ -783,7 +783,7 @@
"groups.enableControlFirst": "Najpierw włącz grupę.",
"settings.compressorProtection": "Ochrona sprężarki",
"settings.compressorProtectionTime": "Czas ochrony (min)",
"settings.compressorProtectionHint": "Zalecane: 3 minuty. Uruchomienia i zmiany trybu Grzanie/Chłodzenie zlecone podczas ochrony są kolejkowane i można je anulować w Termostatach.",
"settings.compressorProtectionHint": "Zalecane: 3 minuty. Uruchomienia i zmiany trybu Grzanie/Chłodzenie zlecone podczas ochrony są kolejkowane. Pełną kolejkę otworzysz przyciskiem „Kolejka” nad Termostatami.",
"zones.compressorQueueTitle": "Oczekuje po ochronie sprężarki",
"zones.queuedPowerOn": "Zakolejkowane: włączenie · {mode} · {temperature}°C",
"zones.queuedModeChange": "Zakolejkowane: zmiana trybu na {mode} · {temperature}°C",
@@ -794,6 +794,18 @@
"zones.queuedCancelled": "Zakolejkowane zadanie anulowane.",
"zones.noQueuedTask": "Brak oczekującego zadania do anulowania.",
"zones.queuedCancelledAll": "Anulowano oczekujące zadania: {count}.",
"zones.queueCancelledStatus": "Zadanie po ochronie anulowane — oczekuje na nowe polecenie"
"zones.queueCancelledStatus": "Zadanie po ochronie anulowane — oczekuje na nowe polecenie",
"zones.queueOverview": "Kolejka",
"zones.queueEyebrow": "Ochrona sprężarki",
"zones.queueTitle": "Kolejka poleceń",
"zones.queueHint": "Polecenia czekające na zakończenie ochrony sprężarki. Możesz anulować pojedyncze zadanie albo wyczyścić całą kolejkę.",
"zones.queuePendingLabel": "Oczekujące zadania",
"zones.queueEmptyTitle": "Kolejka jest pusta",
"zones.queueEmptyText": "Nie ma poleceń oczekujących na zakończenie ochrony sprężarki.",
"zones.queueReasonProtection": "Ochrona sprężarki",
"zones.queueSource": "Źródło: {source}",
"zones.queueRemaining": "Pozostało {time}",
"zones.queueReady": "Gotowe do wykonania",
"zones.queueCancelAllConfirm": "Anulować wszystkie oczekujące zadania ({count}) ochrony sprężarki?"
}
}
+12 -2
View File
@@ -87,7 +87,7 @@
<section class="panel dashboard-section-panel">
<div class="dashboard-section-head compact-head">
<div><span class="eyebrow" data-i18n="nav.zones">Zones</span><h2 data-i18n="dashboard.quickThermostats">Thermostats</h2></div>
<div class="dashboard-section-actions"><button type="button" class="secondary" id="cancelAllCompressorTasks" data-compressor-cancel-all data-action="cancel-all-compressor-tasks" data-i18n="zones.cancelAllQueued" hidden>Cancel all waiting</button><span class="badge" id="dashboardZoneCount">0</span></div>
<div class="dashboard-section-actions"><button type="button" class="secondary queue-overview-button" data-action="open-compressor-queue"><span aria-hidden="true"></span><span data-i18n="zones.queueOverview">Queue</span><b class="queue-overview-count" data-compressor-queue-count>0</b></button><span class="badge" id="dashboardZoneCount">0</span></div>
</div>
<div class="list-grid dashboard-zones" id="dashboardZones"></div>
</section>
@@ -113,7 +113,7 @@
</section>
<section class="view" data-view="zones">
<div class="section-heading"><div><span class="eyebrow" data-i18n="zones.automation">Automation</span><h1 data-i18n="nav.zones">Zones</h1></div><div class="section-heading-actions"><button type="button" class="secondary" data-compressor-cancel-all data-action="cancel-all-compressor-tasks" data-i18n="zones.cancelAllQueued" hidden>Cancel all waiting</button><button class="secondary" data-open="zoneDialog" data-i18n="zones.new">New zone</button></div></div>
<div class="section-heading"><div><span class="eyebrow" data-i18n="zones.automation">Automation</span><h1 data-i18n="nav.zones">Zones</h1></div><div class="section-heading-actions"><button type="button" class="secondary queue-overview-button" data-action="open-compressor-queue"><span aria-hidden="true"></span><span data-i18n="zones.queueOverview">Queue</span><b class="queue-overview-count" data-compressor-queue-count>0</b></button><button class="secondary" data-open="zoneDialog" data-i18n="zones.new">New zone</button></div></div>
<p class="lead" data-i18n="zones.description">Zones define thermostat logic and sensor configuration. Use Quick thermostats on the Dashboard for current temperature changes and presets.</p>
<div class="list-grid" id="zoneList"></div>
</section>
@@ -457,6 +457,16 @@
</form>
</dialog>
<dialog id="compressorQueueDialog" class="compressor-queue-dialog">
<div class="dialog-form compressor-queue-dialog-body">
<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>
<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>
<div class="compressor-queue-summary"><span data-i18n="zones.queuePendingLabel">Waiting</span><strong id="compressorQueueModalCount">0</strong></div>
<div id="compressorQueueList" class="compressor-queue-list"></div>
<div class="form-actions compressor-queue-actions"><button type="button" class="danger" id="compressorQueueCancelAll" data-action="cancel-all-compressor-tasks" data-i18n="zones.cancelAllQueued">Cancel all waiting</button><button type="button" class="primary" data-close data-i18n="actions.done">Done</button></div>
</div>
</dialog>
<dialog id="temporaryThermostatDialog">
<form method="dialog" id="temporaryThermostatForm" class="dialog-form temporary-thermostat-form">
<input type="hidden" name="zone_id">
+65 -4
View File
@@ -126,6 +126,68 @@ function compressorQueuePanel(zone) {
return `<div class="compressor-queue-panel"><div><strong>${esc(tr('zones.compressorQueueTitle'))}</strong><span>${esc(compressorPendingDescription(zone))}</span><small>${esc(tr('zones.compressorQueueUntil', {time:when}))}</small></div><button type="button" class="secondary" data-action="cancel-compressor-task" data-id="${esc(zone.id)}">${esc(tr('actions.cancel'))}</button></div>`;
}
function compressorQueueRemainingText(until) {
const timestamp = until ? new Date(until).getTime() : NaN;
if (!Number.isFinite(timestamp)) return tr('zones.afterProtection');
const seconds = Math.max(0, Math.ceil((timestamp - Date.now()) / 1000));
return seconds > 0 ? tr('zones.queueRemaining', {time:formatExtendedCountdown(seconds)}) : tr('zones.queueReady');
}
function renderCompressorQueueModal() {
const list = $('#compressorQueueList');
const countNode = $('#compressorQueueModalCount');
const cancelAll = $('#compressorQueueCancelAll');
if (!list || !countNode || !cancelAll) return;
const pending = app.zones
.filter(zone => !!zone.compressor_pending_action)
.slice()
.sort((a,b) => {
const at = new Date(a.compressor_pending_until || 0).getTime();
const bt = new Date(b.compressor_pending_until || 0).getTime();
return (Number.isFinite(at) ? at : Number.MAX_SAFE_INTEGER) - (Number.isFinite(bt) ? bt : Number.MAX_SAFE_INTEGER);
});
countNode.textContent = String(pending.length);
cancelAll.disabled = pending.length === 0;
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>`;
return;
}
list.innerHTML = pending.map((zone,index) => {
const device = app.devices.find(item => item.id === zone.device_id);
const until = zone.compressor_pending_until || zone.lockout_until;
const group = groupControlForZone(zone);
const source = group ? `${zoneControlOwnerLabel(zone)}: ${group.name}` : zoneControlOwnerLabel(zone);
const exact = until ? dateTime(until) : tr('zones.afterProtection');
return `<article class="compressor-queue-item">
<div class="compressor-queue-index">${index + 1}</div>
<div class="compressor-queue-item-main">
<div class="compressor-queue-item-head"><div><span class="eyebrow">${esc(zone.name)}</span><h3>${esc(device?.name || tr('common.noDevice'))}</h3></div><span class="compressor-queue-reason">${esc(tr('zones.queueReasonProtection'))}</span></div>
<p class="compressor-queue-command">${esc(compressorPendingDescription(zone))}</p>
<div class="compressor-queue-meta">
<span>${esc(tr('zones.queueSource', {source}))}</span>
<span>${esc(tr('zones.compressorQueueUntil', {time:exact}))}</span>
<strong data-compressor-queue-countdown="${esc(until || '')}">${esc(compressorQueueRemainingText(until))}</strong>
</div>
</div>
<button type="button" class="secondary compressor-queue-cancel" data-action="cancel-compressor-task" data-id="${esc(zone.id)}">${esc(tr('actions.cancel'))}</button>
</article>`;
}).join('');
}
function updateCompressorQueueCountdowns() {
$$('[data-compressor-queue-countdown]').forEach(node => {
node.textContent = compressorQueueRemainingText(node.dataset.compressorQueueCountdown);
});
}
function openCompressorQueueDialog() {
renderCompressorQueueModal();
openDialog('compressorQueueDialog');
updateCompressorQueueCountdowns();
}
function zoneControlOwnerLabel(zone) {
const owner=zone.control_owner || (zone.device_manual_override ? 'direct_manual' : (zone.local_thermostat_power != null ? 'local_thermostat' : 'automation'));
const source=zone.control_source || '';
@@ -265,10 +327,9 @@ function renderZones() {
const dashboardCount = $('#dashboardZoneCount');
if (dashboardCount) dashboardCount.textContent = String(app.zones.length);
const pendingCount = app.zones.filter(zone => !!zone.compressor_pending_action).length;
$$('[data-compressor-cancel-all]').forEach(cancelAll => {
cancelAll.hidden = pendingCount === 0;
cancelAll.textContent = pendingCount ? `${tr('zones.cancelAllQueued')} (${pendingCount})` : tr('zones.cancelAllQueued');
});
$$('[data-compressor-queue-count]').forEach(node => { node.textContent = String(pendingCount); });
$$('.queue-overview-button').forEach(button => button.classList.toggle('has-items', pendingCount > 0));
renderCompressorQueueModal();
}
function groupZones(group) {
+2 -1
View File
@@ -144,8 +144,9 @@ document.addEventListener('click', async event => {
if(!Number.isFinite(value) || value<8 || value>30) return toast(tr('groups.customTemperatureRange'),true);
return sendGroupControl(button.dataset.id,{preset:'custom',setpoint:Math.round(value*10)/10});
}
if (action === 'open-compressor-queue') return openCompressorQueueDialog();
if (action === 'cancel-compressor-task') return cancelCompressorTask(button.dataset.id);
if (action === 'cancel-all-compressor-tasks') return cancelAllCompressorTasks();
if (action === 'cancel-all-compressor-tasks') { if (!confirm(tr('zones.queueCancelAllConfirm', {count:app.zones.filter(zone => !!zone.compressor_pending_action).length}))) return; return cancelAllCompressorTasks(); }
if (action === 'zone-device-power') return sendZoneLocalPower(button.dataset.id, button.dataset.value === 'true');
if (action === 'zone-open-temporary') return populateTemporaryThermostat(button.dataset.id);
if (action === 'zone-temperature') { const zone=app.zones.find(v=>v.id===button.dataset.id); if(zone) { const base=Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint); queueZoneTemperature(zone, base+Number(button.dataset.delta)); } return; }
+1
View File
@@ -17,4 +17,5 @@ async function startApplication() {
setInterval(updateLocalThermostatCountdowns, 1000);
setInterval(updateTemporaryThermostatCountdowns, 1000);
setInterval(updateCompressorQueueCountdowns, 1000);
startApplication().catch(error => console.error('Application startup failed:', error));
+29
View File
@@ -1421,3 +1421,32 @@ textarea[aria-invalid="true"] {
.compressor-queue-panel strong { font-size:.88rem; }
.compressor-queue-panel span,.compressor-queue-panel small { overflow-wrap:anywhere; }
.compressor-queue-panel button { flex:0 0 auto; }
/* v0.9.1: global compressor queue overview. */
.queue-overview-button { display:inline-flex; align-items:center; gap:7px; }
.queue-overview-count { display:inline-grid; place-items:center; min-width:23px; height:23px; padding:0 6px; border-radius:999px; background:color-mix(in srgb,var(--text) 9%,transparent); font-size:.78rem; }
.queue-overview-button.has-items { border-color:var(--warning, #b7791f); box-shadow:0 0 0 1px color-mix(in srgb,var(--warning, #b7791f) 22%,transparent); }
.queue-overview-button.has-items .queue-overview-count { background:color-mix(in srgb,var(--warning, #b7791f) 18%,transparent); color:var(--text); }
.compressor-queue-dialog { width:min(780px, calc(100% - 24px)); }
.compressor-queue-dialog-body { gap:16px; }
.compressor-queue-summary { display:flex; align-items:center; justify-content:space-between; padding:10px 13px; border:1px solid var(--line); border-radius:14px; background:var(--surface); }
.compressor-queue-summary span { color:var(--muted); font-size:.86rem; }
.compressor-queue-summary strong { font-size:1.15rem; }
.compressor-queue-list { display:grid; gap:10px; }
.compressor-queue-item { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:12px; padding:14px; border:1px solid var(--line); border-radius:16px; background:linear-gradient(135deg,color-mix(in srgb,var(--warning, #b7791f) 7%,var(--surface)),var(--surface)); }
.compressor-queue-index { display:grid; place-items:center; width:32px; height:32px; border-radius:10px; background:color-mix(in srgb,var(--warning, #b7791f) 15%,transparent); font-weight:800; }
.compressor-queue-item-main { min-width:0; display:grid; gap:8px; }
.compressor-queue-item-head { display:flex; align-items:flex-start; justify-content:space-between; gap:10px; }
.compressor-queue-item-head h3 { margin:2px 0 0; font-size:1rem; }
.compressor-queue-reason { flex:0 0 auto; padding:5px 8px; border-radius:999px; background:color-mix(in srgb,var(--warning, #b7791f) 14%,transparent); font-size:.73rem; font-weight:700; }
.compressor-queue-command { margin:0; font-weight:700; overflow-wrap:anywhere; }
.compressor-queue-meta { display:flex; flex-wrap:wrap; gap:6px 12px; color:var(--muted); font-size:.78rem; }
.compressor-queue-meta strong { color:var(--text); font-weight:700; }
.compressor-queue-cancel { align-self:center; }
.compressor-queue-empty { display:grid; justify-items:center; gap:7px; padding:30px 18px; text-align:center; border:1px dashed var(--line); border-radius:16px; color:var(--muted); }
.compressor-queue-empty > span { display:grid; place-items:center; width:42px; height:42px; border-radius:999px; background:var(--surface); font-size:1.35rem; color:var(--text); }
.compressor-queue-empty strong { color:var(--text); font-size:1rem; }
.compressor-queue-empty p { margin:0; max-width:440px; }
.compressor-queue-actions { position:sticky; bottom:0; padding-top:10px; background:linear-gradient(transparent,var(--dialog-bg) 28%); }
@media (max-width:620px) { .compressor-queue-item { grid-template-columns:auto minmax(0,1fr); } .compressor-queue-cancel { grid-column:1 / -1; width:100%; } .compressor-queue-item-head { display:grid; } .compressor-queue-reason { justify-self:start; } }