diff --git a/Cargo.lock b/Cargo.lock index a4bc718..6255392 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.9.0" +version = "0.9.1" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index afe3504..5ffc8bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index 8a0ddc0..8c54a85 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -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 diff --git a/README.md b/README.md index eceb140..26e13cf 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/API.md b/docs/API.md index a23f05b..dd59a55 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # GREE Controller API reference -HTTP and WebSocket API for GREE Controller **0.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, diff --git a/home-assistant/custom_components/gree_controller/manifest.json b/home-assistant/custom_components/gree_controller/manifest.json index c32ed53..51adf07 100644 --- a/home-assistant/custom_components/gree_controller/manifest.json +++ b/home-assistant/custom_components/gree_controller/manifest.json @@ -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", diff --git a/lang/en.json b/lang/en.json index 1981f08..f122dfa 100644 --- a/lang/en.json +++ b/lang/en.json @@ -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?" } } diff --git a/lang/pl.json b/lang/pl.json index 1055d2f..3d4dafe 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -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?" } } diff --git a/web/index.html b/web/index.html index 060b078..eaff810 100644 --- a/web/index.html +++ b/web/index.html @@ -87,7 +87,7 @@
Zones

Thermostats

-
0
+
0
@@ -113,7 +113,7 @@
-
Automation

Zones

+
Automation

Zones

Zones define thermostat logic and sensor configuration. Use Quick thermostats on the Dashboard for current temperature changes and presets.

@@ -457,6 +457,16 @@ + +
+
Compressor protection

Command queue

+

Commands waiting for the compressor protection window to end. You can cancel one task or clear the whole queue.

+
Waiting0
+
+
+
+
+
diff --git a/web/js/entities.js b/web/js/entities.js index 646f650..cd51c68 100644 --- a/web/js/entities.js +++ b/web/js/entities.js @@ -126,6 +126,68 @@ function compressorQueuePanel(zone) { return `
${esc(tr('zones.compressorQueueTitle'))}${esc(compressorPendingDescription(zone))}${esc(tr('zones.compressorQueueUntil', {time:when}))}
`; } +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 = `
${esc(tr('zones.queueEmptyTitle'))}

${esc(tr('zones.queueEmptyText'))}

`; + 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 `
+
${index + 1}
+
+
${esc(zone.name)}

${esc(device?.name || tr('common.noDevice'))}

${esc(tr('zones.queueReasonProtection'))}
+

${esc(compressorPendingDescription(zone))}

+
+ ${esc(tr('zones.queueSource', {source}))} + ${esc(tr('zones.compressorQueueUntil', {time:exact}))} + ${esc(compressorQueueRemainingText(until))} +
+
+ +
`; + }).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) { diff --git a/web/js/events.js b/web/js/events.js index 3b0a845..05ffe9a 100644 --- a/web/js/events.js +++ b/web/js/events.js @@ -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; } diff --git a/web/js/main.js b/web/js/main.js index 5778b83..9b71be8 100644 --- a/web/js/main.js +++ b/web/js/main.js @@ -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)); diff --git a/web/styles.css b/web/styles.css index 6e07de2..8c9e84a 100644 --- a/web/styles.css +++ b/web/styles.css @@ -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; } }