v0.8.2
This commit is contained in:
Generated
+1
-1
@@ -633,7 +633,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gree-controller"
|
name = "gree-controller"
|
||||||
version = "0.8.22"
|
version = "0.8.24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gree-controller"
|
name = "gree-controller"
|
||||||
version = "0.8.22"
|
version = "0.8.24"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["GREE Controller contributors"]
|
authors = ["GREE Controller contributors"]
|
||||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||||
|
|||||||
+24
-24
@@ -1,12 +1,12 @@
|
|||||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||||
0571baaa38d6affcd32de5842628a53e0207795a1bb11abceded67ddaf4fbc89 ./Cargo.lock
|
05cd9d64999d2d6957aff2a38fd749ba2ec08bc82aba9aca740b7b0775153a56 ./Cargo.lock
|
||||||
a746338d1f28b0b6ca4118cdf0bedf5d6e68a5539d2e52b48387763899d96118 ./Cargo.toml
|
cb09462abf591a7126bc40c627b66149bdc662613e7d89e94ba532b251b6c8f2 ./Cargo.toml
|
||||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||||
f07790d52e9eb7cc9148093c44b3755341dcdfadfe2552457f0fb902ea226f5e ./README.md
|
bd64c474576e2e3e38e14c16c6c281098ad7c10ab2489fda10029de99fd0ccfb ./README.md
|
||||||
ec0f495ee6a618bdec10b260ee7838f7a872d8c5b0aa91c546226c1ad818f601 ./build.rs
|
ec0f495ee6a618bdec10b260ee7838f7a872d8c5b0aa91c546226c1ad818f601 ./build.rs
|
||||||
4f0cd0c69d168e0c17ec85c9326bf2b105cb82b7fc1dfd20c5f27de9f9e7b6a9 ./docs/API.md
|
dcbbaf9f3dce7bd6f5048662f97d68e6c7fe0040389ebbf382a11b690647a7e8 ./docs/API.md
|
||||||
95e3c8c99ba439a86a2fb05cfd8ef00c75cb959a024a349a082da935b726f500 ./home-assistant/README.md
|
a4571b7d31c96be8b3f540574444742f99a208fc0cdf27a14bebad70bc916ab3 ./home-assistant/README.md
|
||||||
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
|
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
|
||||||
6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py
|
6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py
|
||||||
71c8058f85fd9ce9f063b7a84dcf2956f23cdbbd02bd4bd99d2fbb9d177c72ad ./home-assistant/custom_components/gree_controller/climate.py
|
71c8058f85fd9ce9f063b7a84dcf2956f23cdbbd02bd4bd99d2fbb9d177c72ad ./home-assistant/custom_components/gree_controller/climate.py
|
||||||
@@ -16,17 +16,17 @@ ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assista
|
|||||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
|
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
|
||||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png
|
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png
|
||||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
|
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
|
||||||
d55689391ec02289dfffb622c18d8bc6423df192c267959395751d634ebe3358 ./home-assistant/custom_components/gree_controller/manifest.json
|
83a0ca6c4e90076544b2528790db9be3113b8ca7c5de6e943b70e6cce632d9cb ./home-assistant/custom_components/gree_controller/manifest.json
|
||||||
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
|
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
|
||||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
|
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
|
||||||
1cae12876eb81085910907cf1fcf340a18dcba4f8c967ef222c9496325057849 ./home-assistant/custom_components/gree_controller/sensor.py
|
1cae12876eb81085910907cf1fcf340a18dcba4f8c967ef222c9496325057849 ./home-assistant/custom_components/gree_controller/sensor.py
|
||||||
229ef9b3de8001bdedea42015a219c7f7c17d50f90b10705508494d434aeb549 ./home-assistant/custom_components/gree_controller/switch.py
|
1b8f5ddd5fb169e1db43b897f0af29eb197f113c36b3e8fae9af04a579a11d76 ./home-assistant/custom_components/gree_controller/switch.py
|
||||||
6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./home-assistant/custom_components/gree_controller/translations/en.json
|
6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./home-assistant/custom_components/gree_controller/translations/en.json
|
||||||
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
|
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
|
||||||
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
|
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
|
||||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||||
4f5c278b3deb14accdcae92b25ec412da4bc5e87b86860d051b0757476afd441 ./lang/en.json
|
1b6d4dbbc7176abb113996d250dfec4dd1eb66b05b1971a584638b4c1b1cf664 ./lang/en.json
|
||||||
1f5ee2f8673121f5a786c478ca4ace06917c66521a0ba79d5b52999a8d805204 ./lang/pl.json
|
ad75001644c6fbbca092bfac5bf5cedd77c4b2421b69679023c611c19ac65c38 ./lang/pl.json
|
||||||
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
||||||
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
|
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
|
||||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||||
@@ -48,15 +48,15 @@ ffa4d44d5f9f7a3e0e80e34c61ee510ab0f6b4b80a8d6c8137aae2ea74fbab02 ./src/api.rs
|
|||||||
417940b467c6f00bbf01cc47eb4479880dee9b2f284a80e3662a6d433dc11b92 ./src/api/events.rs
|
417940b467c6f00bbf01cc47eb4479880dee9b2f284a80e3662a6d433dc11b92 ./src/api/events.rs
|
||||||
aab086f56cbb42d5aa48c04111bdc87637e06e35f2aa1e475f4a6bf06f5dee67 ./src/api/groups.rs
|
aab086f56cbb42d5aa48c04111bdc87637e06e35f2aa1e475f4a6bf06f5dee67 ./src/api/groups.rs
|
||||||
a13d4e217fe3ddaa73873ba6e0d1bc93750cf61764d867ff7372274ed21d9599 ./src/api/history.rs
|
a13d4e217fe3ddaa73873ba6e0d1bc93750cf61764d867ff7372274ed21d9599 ./src/api/history.rs
|
||||||
8bdeed0880211cb782bdad48bf65335fe8b3da12a17513e50282358457caf76c ./src/api/house.rs
|
3a85cf4d9de30f67538091ab40c7f6ca729da78426a8dd251452366e884f4cb0 ./src/api/house.rs
|
||||||
0d7d598f7ed24315ed0547a60a72a06f20ebcf926489e1c454986f42d22cc305 ./src/api/integrations.rs
|
0d7d598f7ed24315ed0547a60a72a06f20ebcf926489e1c454986f42d22cc305 ./src/api/integrations.rs
|
||||||
bb2a746ecdcc2da5fe54e18b455c7bd19453486dd5c008e71951a2c81d0e7d64 ./src/api/middleware.rs
|
bb2a746ecdcc2da5fe54e18b455c7bd19453486dd5c008e71951a2c81d0e7d64 ./src/api/middleware.rs
|
||||||
2abb8d155b832bd7bde575b12ef37fbbf09c246255d9abd2adae0d56016ce170 ./src/api/public_settings.rs
|
2abb8d155b832bd7bde575b12ef37fbbf09c246255d9abd2adae0d56016ce170 ./src/api/public_settings.rs
|
||||||
8f7f0b8c157e9c97d7dc305b79923646ef47d76497abfcf09e9c67bb12af5dd1 ./src/api/schedules.rs
|
8f7f0b8c157e9c97d7dc305b79923646ef47d76497abfcf09e9c67bb12af5dd1 ./src/api/schedules.rs
|
||||||
9392cc0859bb94fe87742d25200e74c654eea6774ca32b4a3656abf2e1f05114 ./src/api/settings.rs
|
12d4b69478a9bc6dc4857b3c9c5144d96ba33529e68b36cccca67b1093d7cc71 ./src/api/settings.rs
|
||||||
74c156717b37e83d329f7c52ac622e02f690621544c4eb07c4502194db55e6e9 ./src/api/system.rs
|
74c156717b37e83d329f7c52ac622e02f690621544c4eb07c4502194db55e6e9 ./src/api/system.rs
|
||||||
681cccb8d09f4ad9c2e6467a4dbe2f9d991125775a5d2bfc9043daf940fecd29 ./src/api/websocket.rs
|
681cccb8d09f4ad9c2e6467a4dbe2f9d991125775a5d2bfc9043daf940fecd29 ./src/api/websocket.rs
|
||||||
0ed177691a5780a9db4af9101872151c28292059b76407a97b18fe9034c74c33 ./src/api/zones.rs
|
90f6cdaa688efb0fa4929f28186df47eb5be44b8600fde9a04ee9a28f47b17e4 ./src/api/zones.rs
|
||||||
9040e8cb6647c76a875148b7591abcccfe4e2d4708ef462a1e5d25dd4ccac911 ./src/config.rs
|
9040e8cb6647c76a875148b7591abcccfe4e2d4708ef462a1e5d25dd4ccac911 ./src/config.rs
|
||||||
369455b5ffb125f1a80b767f62fd8c84a953adb69548dbd8adb653a6fbaaa45b ./src/db.rs
|
369455b5ffb125f1a80b767f62fd8c84a953adb69548dbd8adb653a6fbaaa45b ./src/db.rs
|
||||||
24580b279cfcd5388ba584eaba2c9bb729b8aae81e77b781c23fb804c5b5fa5d ./src/db/climate.rs
|
24580b279cfcd5388ba584eaba2c9bb729b8aae81e77b781c23fb804c5b5fa5d ./src/db/climate.rs
|
||||||
@@ -69,14 +69,14 @@ f3bd91d0bdb7699f319b9341d928d3cc3b25f759653fcd2095a5c5f695d08fac ./src/db/event
|
|||||||
35cbb521a8ec456756496b8aee71f9dfebad528a2cf2653ba8b48f6035be0c01 ./src/db/tests.rs
|
35cbb521a8ec456756496b8aee71f9dfebad528a2cf2653ba8b48f6035be0c01 ./src/db/tests.rs
|
||||||
a384ea4042d1d11692b5f8693df1f54850dcc4e80726780b44b2b096bf667b78 ./src/db/zone_history.rs
|
a384ea4042d1d11692b5f8693df1f54850dcc4e80726780b44b2b096bf667b78 ./src/db/zone_history.rs
|
||||||
952ea5906d39ef917ad71edc345f76b29c07c63079644fd5699b12aa254cb35f ./src/engine.rs
|
952ea5906d39ef917ad71edc345f76b29c07c63079644fd5699b12aa254cb35f ./src/engine.rs
|
||||||
65e2df80013c57c46f1e0d30ee84016024487557a26e499e08070507319b536a ./src/engine/automations.rs
|
d418f27edde4a8a8ea89817899abfc48df632ec581deacf64db1dc409eef920f ./src/engine/automations.rs
|
||||||
910248f30ac9d697816458cd393c5f7d4bbdd3a12e48cb41cbb3ddb68a1ce395 ./src/engine/commands.rs
|
910248f30ac9d697816458cd393c5f7d4bbdd3a12e48cb41cbb3ddb68a1ce395 ./src/engine/commands.rs
|
||||||
3967cc60a108c71bd7e5d6a99c7caf32ed8f6cbbb63282a5a5d3357e7c07f933 ./src/engine/control_plan.rs
|
d91c50e68baa8f90a30fd94be1fda8cf548ed7386c1058f14f8d5b32625c0b25 ./src/engine/control_plan.rs
|
||||||
47acda24f31ec6af5ac94b00f52e232f0fdd443106abb9eeca7cc28d26b994bd ./src/engine/deadlines.rs
|
47acda24f31ec6af5ac94b00f52e232f0fdd443106abb9eeca7cc28d26b994bd ./src/engine/deadlines.rs
|
||||||
cb1fe22b11acdf6a593a196c4fa8f778bfe81765807f9e30416e3ca7fa9ef594 ./src/engine/groups.rs
|
a1afe1d6ef1869ecc7ab01eb43b005e54c58b314ee0f41a34ab4c2afbe03e892 ./src/engine/groups.rs
|
||||||
18a7669318949e533f206d25cf7b63771d7f0f2c0b375a72ff78a2d921baaa61 ./src/engine/history.rs
|
18a7669318949e533f206d25cf7b63771d7f0f2c0b375a72ff78a2d921baaa61 ./src/engine/history.rs
|
||||||
dad8930c3d811f02cfeced3aa4b9a404fe273ba1b99539bb174b9430575f3e22 ./src/engine/local_thermostat.rs
|
dad8930c3d811f02cfeced3aa4b9a404fe273ba1b99539bb174b9430575f3e22 ./src/engine/local_thermostat.rs
|
||||||
2ccaa0344609d9cacdf704293bc10a4e7e1cebc7f25a2e279f6b3b5c1798755a ./src/engine/ownership.rs
|
059dc7f6fe687a66efa9660538edabbf51e2b053796dc8f70c14e9737a584599 ./src/engine/ownership.rs
|
||||||
9c37adf514c96840fd6a5c715c579350fa86d6dcc0655a2aa97d1fd7c7e04299 ./src/engine/polling.rs
|
9c37adf514c96840fd6a5c715c579350fa86d6dcc0655a2aa97d1fd7c7e04299 ./src/engine/polling.rs
|
||||||
3d48608a065aa52a1fd8b9c59356a74d8216ecee3d47f8ff8ef7d3d47170c617 ./src/engine/runtime.rs
|
3d48608a065aa52a1fd8b9c59356a74d8216ecee3d47f8ff8ef7d3d47170c617 ./src/engine/runtime.rs
|
||||||
ffa72b93502eece187a96adb4ec94aec6048d4a6dc7af66e7c7af0931318787f ./src/engine/schedules.rs
|
ffa72b93502eece187a96adb4ec94aec6048d4a6dc7af66e7c7af0931318787f ./src/engine/schedules.rs
|
||||||
@@ -84,8 +84,8 @@ e0d5347f73066404f9df9d211e059fd10cf054466ae35bdad6533a862b5a816a ./src/engine/t
|
|||||||
a9188b588b2617ce1547a45d46141f7e5f1bb0027a48675dd86b274f807d0875 ./src/engine/temperature.rs
|
a9188b588b2617ce1547a45d46141f7e5f1bb0027a48675dd86b274f807d0875 ./src/engine/temperature.rs
|
||||||
fcd40789045b7cbf168880ff4a7fee12fb4afdc6fd376c8cca9c0cf63390e5a0 ./src/engine/temporary_thermostat.rs
|
fcd40789045b7cbf168880ff4a7fee12fb4afdc6fd376c8cca9c0cf63390e5a0 ./src/engine/temporary_thermostat.rs
|
||||||
05a5b0eaa9a556dd7a4d775636926647b7dda087c3a270ea3de299958f7e4c91 ./src/engine/tests.rs
|
05a5b0eaa9a556dd7a4d775636926647b7dda087c3a270ea3de299958f7e4c91 ./src/engine/tests.rs
|
||||||
970d5e7783827bb016bef6ec9727d2d74650fa0e157ccebc5348ff107ecc5446 ./src/engine/zone_actions.rs
|
c725a0471a5ff840f6624ca494cc970562f591f603d030640e2b27c34cda1f9e ./src/engine/zone_actions.rs
|
||||||
43343463167e7d3c3601c91765005619bf934dc8af7ca2afde4f8806b4ffbe5b ./src/engine/zone_control.rs
|
9629836c63a5fc34d86d37407805ea4c90cb6bde8c103dd6ddd50b60c53864d0 ./src/engine/zone_control.rs
|
||||||
4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs
|
4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs
|
||||||
c6ff66da9ad08506f839ec56a869ead3abc824b5311e0ea9244517d11f7f4207 ./src/home_assistant.rs
|
c6ff66da9ad08506f839ec56a869ead3abc824b5311e0ea9244517d11f7f4207 ./src/home_assistant.rs
|
||||||
6f9ef85cd53ba030b477bc3c186f0f7e762674124210eabcb0686f2806eaada1 ./src/influxdb.rs
|
6f9ef85cd53ba030b477bc3c186f0f7e762674124210eabcb0686f2806eaada1 ./src/influxdb.rs
|
||||||
@@ -95,14 +95,14 @@ feb50ecd60b9a9fe9de1f7d8e1e26caab5435d59347468f6266a318f431bc3d3 ./src/influxdb
|
|||||||
c3ff7232a2fa1d29428872a10cfd72ee60a3a1faaa662eff6c2d8c7dffc1ed3a ./src/main.rs
|
c3ff7232a2fa1d29428872a10cfd72ee60a3a1faaa662eff6c2d8c7dffc1ed3a ./src/main.rs
|
||||||
e935e1fcf0d379b627df5d5055dbdad3dd55786fde7191376918b05afdde1727 ./src/models.rs
|
e935e1fcf0d379b627df5d5055dbdad3dd55786fde7191376918b05afdde1727 ./src/models.rs
|
||||||
fca124ec30b5c9286f0f44cc18e8953bb2809f5009d4d521deaebfdeb00c1bd1 ./src/models/automation.rs
|
fca124ec30b5c9286f0f44cc18e8953bb2809f5009d4d521deaebfdeb00c1bd1 ./src/models/automation.rs
|
||||||
9a100f778d1f71697dca7ae4db9991a1b7b2da4f26e006ce0612f98c5406acec ./src/models/control_plan.rs
|
60ff96965cf9531b3964add783a6a45578aa1986844e99a239f182eb4f962d5b ./src/models/control_plan.rs
|
||||||
10b78ee6c225119fd86435928d5269fd9a9b482d3575f073157d5d20e0a17785 ./src/models/defaults.rs
|
10b78ee6c225119fd86435928d5269fd9a9b482d3575f073157d5d20e0a17785 ./src/models/defaults.rs
|
||||||
7cdc99a6f0920baf4ac88fbe7e6f1859e8598a00f00b8c3d84f20bc3a4de35f9 ./src/models/device.rs
|
7cdc99a6f0920baf4ac88fbe7e6f1859e8598a00f00b8c3d84f20bc3a4de35f9 ./src/models/device.rs
|
||||||
cfbaa853a5185c4be572588577b313a2d618837f7c07a3860d0e8d2cca04ee60 ./src/models/history.rs
|
cfbaa853a5185c4be572588577b313a2d618837f7c07a3860d0e8d2cca04ee60 ./src/models/history.rs
|
||||||
668c9cf157679f8c9fe72846f743629c79e263b14827110834b4addbdf9d13ec ./src/models/integrations.rs
|
668c9cf157679f8c9fe72846f743629c79e263b14827110834b4addbdf9d13ec ./src/models/integrations.rs
|
||||||
c19f243d709acb59db7bc4a91c02c538146fc5a7c014d90c53e8d8c62c4d6008 ./src/models/runtime.rs
|
c19f243d709acb59db7bc4a91c02c538146fc5a7c014d90c53e8d8c62c4d6008 ./src/models/runtime.rs
|
||||||
e77f8cdcb5fc0e51cdbf2527a69585a2f6f9089e159d1e45ec4a48735ab0e304 ./src/models/temporary_thermostat.rs
|
e77f8cdcb5fc0e51cdbf2527a69585a2f6f9089e159d1e45ec4a48735ab0e304 ./src/models/temporary_thermostat.rs
|
||||||
c78d7f10437c92d9fa9d6227e1de336dd22684c14fd20ee3b50dec28e23b56a7 ./src/models/zone.rs
|
98944fff61013ec10aa5fcb3840abe90d10ba7a901c57bab86fc71180a3eef1b ./src/models/zone.rs
|
||||||
9c735d7c475f21f2f388c21be784376f7184f0bf18af1886544ef51585a3b290 ./src/notifications.rs
|
9c735d7c475f21f2f388c21be784376f7184f0bf18af1886544ef51585a3b290 ./src/notifications.rs
|
||||||
7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs
|
7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs
|
||||||
f1f765331469f4219a551414833c19b6de413114c22fdd0468e1f417659553c9 ./src/protocol/gree.rs
|
f1f765331469f4219a551414833c19b6de413114c22fdd0468e1f417659553c9 ./src/protocol/gree.rs
|
||||||
@@ -127,14 +127,14 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
|||||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||||
de66cab9b650cb448814bfeda6d216e168dd3e6f07a1a438e4a9de9acbe6b798 ./web/404.html
|
de66cab9b650cb448814bfeda6d216e168dd3e6f07a1a438e4a9de9acbe6b798 ./web/404.html
|
||||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||||
68fbf1630c68f28e94008e609d33ba2d5b4a922a021c7386714e49e913f289d2 ./web/index.html
|
cf7723c6ab3bb7af3858171e3e100b2327649d7065541980cb2f24b57583f39d ./web/index.html
|
||||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
||||||
b3d5cb473bf9771de78e14c7de730cb86c73f8cdbb6344da67ba7d6abac879ea ./web/js/bootstrap.js
|
b3d5cb473bf9771de78e14c7de730cb86c73f8cdbb6344da67ba7d6abac879ea ./web/js/bootstrap.js
|
||||||
64a43e2142a69ebddf4160c0d0d3b573fa379c650431090fd15c9ba3b62bc4e7 ./web/js/charts.js
|
64a43e2142a69ebddf4160c0d0d3b573fa379c650431090fd15c9ba3b62bc4e7 ./web/js/charts.js
|
||||||
cf8d97770985b8cf920382725fa33d5f61497a639544bfc528af72b276de0b6a ./web/js/core.js
|
cf8d97770985b8cf920382725fa33d5f61497a639544bfc528af72b276de0b6a ./web/js/core.js
|
||||||
6e421388c7f6deb2aa42f1205dfdc52839f9f178fbd672129972528b8517a870 ./web/js/dashboard.js
|
6e421388c7f6deb2aa42f1205dfdc52839f9f178fbd672129972528b8517a870 ./web/js/dashboard.js
|
||||||
7ed64ca0ef62c4cd3d9c314adfcf1c3874a0c49139379dd6f175710786a2ced9 ./web/js/entities.js
|
4edcb49aa9feb11294aeaa7453bfde3f3d0921cf02446bdab3b47c7cf7a38988 ./web/js/entities.js
|
||||||
8c94a3b4aa5455638332362379b09dc151cdf47ab24774c9fd52100ecad689c5 ./web/js/events.js
|
018c26f2eb33a2b3f1ab55bf1f4f3dec60eb153f1aaa8996513b705b3cd2a050 ./web/js/events.js
|
||||||
2fe6c804f86b0613a50a56c54f7a0cbee77273759edf3e6fec36ac77d35edbd7 ./web/js/forms.js
|
2fe6c804f86b0613a50a56c54f7a0cbee77273759edf3e6fec36ac77d35edbd7 ./web/js/forms.js
|
||||||
be07a341ba4b1f514a5e13553494194b22aa87108cb04de40635a0158921780b ./web/js/history.js
|
be07a341ba4b1f514a5e13553494194b22aa87108cb04de40635a0158921780b ./web/js/history.js
|
||||||
6d4a9f17ff241eaf44e8d2260163d9226fca3c8d6e1a287d31a2c96b976532f9 ./web/js/main.js
|
6d4a9f17ff241eaf44e8d2260163d9226fca3c8d6e1a287d31a2c96b976532f9 ./web/js/main.js
|
||||||
@@ -144,6 +144,6 @@ d375868ded6ffcd6b18d4c661bb2051e7cc5b136f95d4d68355cdd6e0caaf278 ./web/js/realt
|
|||||||
28ff1324f4495b7bcd431eb94f51012c1b1e558f8acb931ac35b71b48374c200 ./web/js/settings-ui.js
|
28ff1324f4495b7bcd431eb94f51012c1b1e558f8acb931ac35b71b48374c200 ./web/js/settings-ui.js
|
||||||
9d44596929f67400f5f09d82330af4452914f2b4729eadcd4f695672d2cd849c ./web/js/settings.js
|
9d44596929f67400f5f09d82330af4452914f2b4729eadcd4f695672d2cd849c ./web/js/settings.js
|
||||||
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
|
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
|
||||||
32898e4562f8e7e70a20f95d2eecde235f4ecb1799b515f57e55579b0d4fd604 ./web/styles.css
|
e61c58b8acf64147d39c34b83f051693f6d15b3fc7eb9af8ab2dd46c0839793d ./web/styles.css
|
||||||
bc95830ec8273818ffde0a6b50eb3cc9ecbeee65a7a1d3aeff08cde86d05dc7b ./web/sw.js
|
bc95830ec8273818ffde0a6b50eb3cc9ecbeee65a7a1d3aeff08cde86d05dc7b ./web/sw.js
|
||||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||||
|
|||||||
@@ -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.
|
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.8.22**
|
**Current release: 0.8.24**
|
||||||
|
|
||||||
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
|
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ The interface is intentionally split by responsibility:
|
|||||||
| Dashboard | Whole-house summary and Quick Thermostat controls. |
|
| Dashboard | Whole-house summary and Quick Thermostat controls. |
|
||||||
| Devices | Discovery, binding and direct/technical GREE device control. |
|
| Devices | Discovery, binding and direct/technical GREE device control. |
|
||||||
| Zones | Thermostat configuration, profiles, sensors and control ownership. |
|
| Zones | Thermostat configuration, profiles, sensors and control ownership. |
|
||||||
| Groups | Climate group membership and group power/mode/preset/custom-temperature control. |
|
| Groups | Climate group membership plus optional group-level mode/preset/custom-temperature control. Group ON activates member thermostat control; Group OFF powers member units down and releases group ownership so individual thermostats are not left group-blocked. |
|
||||||
| Schedules | Weekly thermostat schedules and ready-made templates. |
|
| Schedules | Weekly thermostat schedules and ready-made templates. |
|
||||||
| Automations | Time/temperature triggered device or group actions. |
|
| Automations | Time/temperature triggered device or group actions. |
|
||||||
| History | Overview, zones, devices and HA sensors with precise chart tooltips and zoom. |
|
| History | Overview, zones, devices and HA sensors with precise chart tooltips and zoom. |
|
||||||
@@ -175,7 +175,7 @@ The hardened systemd unit allows `AF_NETLINK`, which is required for interface d
|
|||||||
|
|
||||||
A **Device** is the physical GREE unit. Device commands are direct/technical commands.
|
A **Device** is the physical GREE unit. Device commands are direct/technical commands.
|
||||||
|
|
||||||
A **Zone** is the thermostat owner for a device. It decides demand from room temperature, setpoint, hysteresis, schedules, group/house gates, manual ownership and safety lockouts.
|
A **Zone** is the thermostat owner for a device. It decides demand from room temperature, setpoint, hysteresis, schedules, house master state, manual ownership and safety lockouts. Group control can temporarily apply shared settings. Turning a group OFF powers its member units down and releases group ownership; each thermostat can then be switched back on independently without an OFF group acting as a membership block.
|
||||||
|
|
||||||
This distinction is important: direct device control intentionally behaves differently from thermostat control.
|
This distinction is important: direct device control intentionally behaves differently from thermostat control.
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -1,6 +1,6 @@
|
|||||||
# GREE Controller API reference
|
# GREE Controller API reference
|
||||||
|
|
||||||
HTTP and WebSocket API for GREE Controller **0.8.22**.
|
HTTP and WebSocket API for GREE Controller **0.8.24**.
|
||||||
|
|
||||||
[← Main documentation](../README.md)
|
[← Main documentation](../README.md)
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ Common statuses:
|
|||||||
| GET | `/api/groups/{id}` | Read a group. |
|
| GET | `/api/groups/{id}` | Read a group. |
|
||||||
| PUT | `/api/groups/{id}` | Replace group definition. |
|
| PUT | `/api/groups/{id}` | Replace group definition. |
|
||||||
| DELETE | `/api/groups/{id}` | Delete a group. |
|
| DELETE | `/api/groups/{id}` | Delete a group. |
|
||||||
| POST | `/api/groups/{id}/control` | Group power/mode/preset/custom-temperature control. |
|
| POST | `/api/groups/{id}/control` | Group control enable/mode/preset/custom-temperature control. |
|
||||||
| POST | `/api/house/control` | Set global thermostat mode. |
|
| POST | `/api/house/control` | Set global thermostat mode. |
|
||||||
| POST | `/api/house/power` | Set whole-house master power. |
|
| POST | `/api/house/power` | Set whole-house master power. |
|
||||||
| POST | `/api/house/preset` | Set/clear whole-house preset override. |
|
| POST | `/api/house/preset` | Set/clear whole-house preset override. |
|
||||||
@@ -212,7 +212,7 @@ Response:
|
|||||||
{
|
{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"name": "gree-controller",
|
"name": "gree-controller",
|
||||||
"version": "0.8.22",
|
"version": "0.8.24",
|
||||||
"uptime_seconds": 1234,
|
"uptime_seconds": 1234,
|
||||||
"control_ready": true,
|
"control_ready": true,
|
||||||
"time": "2026-08-30T06:54:00Z"
|
"time": "2026-08-30T06:54:00Z"
|
||||||
@@ -236,7 +236,7 @@ Returns the initial Web UI snapshot:
|
|||||||
"settings": {},
|
"settings": {},
|
||||||
"outdoor_temperature": null,
|
"outdoor_temperature": null,
|
||||||
"system": {
|
"system": {
|
||||||
"version": "0.8.22",
|
"version": "0.8.24",
|
||||||
"uptime_seconds": 1234,
|
"uptime_seconds": 1234,
|
||||||
"auth_required": false,
|
"auth_required": false,
|
||||||
"control_ready": true,
|
"control_ready": true,
|
||||||
@@ -517,7 +517,7 @@ Semantics:
|
|||||||
- `preset`: `auto`, `comfort`, `sleep`, `away`, `custom`; `auto` clears the profile override.
|
- `preset`: `auto`, `comfort`, `sleep`, `away`, `custom`; `auto` clears the profile override.
|
||||||
- `mode`: `house`, `cool`, `heat`; `house` restores global mode inheritance.
|
- `mode`: `house`, `cool`, `heat`; `house` restores global mode inheritance.
|
||||||
- `enabled`: zone automation enable state.
|
- `enabled`: zone automation enable state.
|
||||||
- `power:true`: local Quick Thermostat ownership — this zone can run through full thermostat logic even if its climate group is off.
|
- `power:true`: local Quick Thermostat ownership — this zone can run through full thermostat logic regardless of whether group-level control is enabled for its climate group.
|
||||||
- `power:false`: turns this zone off and creates a fresh backend-owned local hand-back timer (currently 15 minutes).
|
- `power:false`: turns this zone off and creates a fresh backend-owned local hand-back timer (currently 15 minutes).
|
||||||
- `clear_local_thermostat_override:true`: immediately return local Quick Thermostat ownership to normal group/schedule control.
|
- `clear_local_thermostat_override:true`: immediately return local Quick Thermostat ownership to normal group/schedule control.
|
||||||
- `clear_device_manual_override:true`: explicitly hand a physical/direct manual takeover back to the thermostat.
|
- `clear_device_manual_override:true`: explicitly hand a physical/direct manual takeover back to the thermostat.
|
||||||
@@ -662,14 +662,14 @@ Read, replace or delete a group. `DELETE` returns `204`.
|
|||||||
|
|
||||||
All fields optional:
|
All fields optional:
|
||||||
|
|
||||||
- `power`: group gate,
|
- `power`: enable/disable group-level control (legacy field name; this is not a device power gate),
|
||||||
- `mode`: `house`, `cool`, `heat`,
|
- `mode`: `house`, `cool`, `heat`,
|
||||||
- `preset`: `auto`, `comfort`, `sleep`, `away`, `custom`,
|
- `preset`: `auto`, `comfort`, `sleep`, `away`, `custom`,
|
||||||
- `setpoint`: custom group target in the `8–30°C` range; requires `preset: "custom"`.
|
- `setpoint`: custom group target in the `8–30°C` range; requires `preset: "custom"`.
|
||||||
|
|
||||||
A custom setpoint creates the same temporary manual override for every member zone and normally expires at that zone's next schedule boundary. Returns a group control/result object including updated members/state.
|
A custom setpoint creates the same temporary manual override for every member zone and normally expires at that zone's next schedule boundary. Returns a group control/result object including updated members/state.
|
||||||
|
|
||||||
Group power is a per-group gate. Whole-house `power=false` closes all group gates. If one group is later switched back on, only that group is reopened; other groups remain off and their own member zones stay disabled. A zone is never blocked merely because a different, unrelated group is on. If a zone belongs to multiple groups, any disabled membership remains authoritative.
|
Group `power` is a scoped bulk-power/control action. `power=false` immediately powers member units off and releases `group:*` ownership; members are stored as individually-off thermostats rather than being blocked by membership in an OFF group, so a user can turn a thermostat back on independently. `power=true` clears that scoped thermostat-OFF state and immediately re-runs group thermostat arbitration. Explicit Web/Home Assistant group power actions take over older member manual/temporary ownership; scheduled group automations still respect higher-priority manual/local ownership. Group mode/preset/setpoint changes are accepted only while the group is ON. Whole-house power remains independent and authoritative.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -687,7 +687,7 @@ House thermostat mode and master power are intentionally separate.
|
|||||||
|
|
||||||
Valid modes: `cool`, `heat`, `off`.
|
Valid modes: `cool`, `heat`, `off`.
|
||||||
|
|
||||||
- `cool`/`heat` are explicit whole-house activation requests: master power is enabled, group gates are enabled and the thermostat arbiter starts eligible managed zones.
|
- `cool`/`heat` are explicit whole-house activation requests: master power is enabled and the thermostat arbiter starts eligible managed zones. Existing per-group control enable/disable settings are preserved.
|
||||||
- `off` means **do not perform house-level thermostat control** for inherited zones. It does not itself change master power and does not forcibly power direct/manual devices down.
|
- `off` means **do not perform house-level thermostat control** for inherited zones. It does not itself change master power and does not forcibly power direct/manual devices down.
|
||||||
|
|
||||||
Returns public runtime settings.
|
Returns public runtime settings.
|
||||||
@@ -700,7 +700,7 @@ Returns public runtime settings.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`false` is authoritative: it disables group gates, clears local/manual ownership markers as required and powers every enabled physical device down. `true` resumes thermostat arbitration rather than blindly sending a bare ON frame.
|
`false` is authoritative: it clears local/manual ownership markers as required and powers every enabled physical device down. It does **not** modify per-group control enable/disable settings. `true` resumes thermostat arbitration rather than blindly sending a bare ON frame.
|
||||||
|
|
||||||
Response includes:
|
Response includes:
|
||||||
|
|
||||||
@@ -724,7 +724,7 @@ Response includes:
|
|||||||
|
|
||||||
Valid: `auto`, `comfort`, `sleep`, `away`.
|
Valid: `auto`, `comfort`, `sleep`, `away`.
|
||||||
|
|
||||||
A non-`auto` preset creates zone overrides that normally expire at each zone's next schedule boundary. `auto` clears them. Selecting a house preset also explicitly re-enables master power/group gates.
|
A non-`auto` preset creates zone overrides that normally expire at each zone's next schedule boundary. `auto` clears them. Selecting a house preset explicitly re-enables master power while preserving each group's control enable/disable setting.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ The recommended `combined` strategy keeps the GREE sensor as the primary input a
|
|||||||
|
|
||||||
Each controller zone is exposed as a full Home Assistant `climate` entity with current/target temperature and HVAC modes: Off, Auto (follow the controller house mode), Cool and Heat. For a **zone thermostat**, Auto does not mean the GREE unit's native automatic heat/cool algorithm: it means **inherit the whole-house Heating/Cooling selection from GREE Controller**. Direct physical-device climate entities still use the native GREE Auto mode. The target temperature remains published while a zone is Off, so Home Assistant can display the configured setpoint instead of `unknown`. The existing zone target `number` and enabled `switch` remain available for compatibility.
|
Each controller zone is exposed as a full Home Assistant `climate` entity with current/target temperature and HVAC modes: Off, Auto (follow the controller house mode), Cool and Heat. For a **zone thermostat**, Auto does not mean the GREE unit's native automatic heat/cool algorithm: it means **inherit the whole-house Heating/Cooling selection from GREE Controller**. Direct physical-device climate entities still use the native GREE Auto mode. The target temperature remains published while a zone is Off, so Home Assistant can display the configured setpoint instead of `unknown`. The existing zone target `number` and enabled `switch` remain available for compatibility.
|
||||||
|
|
||||||
From version 0.7.9, each zone climate also supports preset modes `auto`, `comfort`, `sleep` and `away`, and the same choices are exposed as a separate **Work profile** `select` on the zone device. `auto` removes the temporary per-zone profile override and returns the zone to its schedule. The zone enable switch now reports the configured zone state independently from group power gates; `effective_enabled` remains available in attributes/sensors to show when an enabled zone is currently blocked by a disabled group. From version 0.7.10, climate entity ON/OFF controls local thermostat power rather than technically enabling/disabling the zone: local ON may run the zone while its group is off, with full thermostat logic intact. From 0.7.12, every local OFF creates a fresh backend-owned 15-minute hand-back deadline exposed as `local_thermostat_resume_at`. A direct/pilot takeover suspends expiry of that local timer; if the unit is returned to the previous OFF state, the backend re-arms a fresh 15-minute countdown from that moment. The separate zone Enabled switch remains the technical availability switch.
|
From version 0.7.9, each zone climate also supports preset modes `auto`, `comfort`, `sleep` and `away`, and the same choices are exposed as a separate **Work profile** `select` on the zone device. `auto` removes the temporary per-zone profile override and returns the zone to its schedule. The zone enable switch reports the configured zone state independently from group control. Turning a group OFF powers its member units down and releases group ownership. Members are not blocked merely because the group remains OFF: an individual thermostat can be turned back on independently. Turning the group ON clears that scoped OFF state and immediately resumes group thermostat arbitration. From version 0.7.10, climate entity ON/OFF controls local thermostat power rather than technically enabling/disabling the zone. From 0.7.12, every local OFF creates a fresh backend-owned 15-minute hand-back deadline exposed as `local_thermostat_resume_at`. A direct/pilot takeover suspends expiry of that local timer; if the unit is returned to the previous OFF state, the backend re-arms a fresh 15-minute countdown from that moment. The separate zone Enabled switch remains the technical availability switch.
|
||||||
|
|
||||||
From version 0.6.4, zone climate entities use `climate.<zone_name>_thermostat`, for example `climate.igor_thermostat`. On integration reload, existing zone climate registry entries are migrated to this scheme using the zone's current name. If the target entity ID is already occupied, the old ID is retained and Home Assistant logs a warning.
|
From version 0.6.4, zone climate entities use `climate.<zone_name>_thermostat`, for example `climate.igor_thermostat`. On integration reload, existing zone climate registry entries are migrated to this scheme using the zone's current name. If the target entity ID is already occupied, the old ID is retained and Home Assistant logs a warning.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "gree_controller",
|
"domain": "gree_controller",
|
||||||
"name": "GREE Controller",
|
"name": "GREE Controller",
|
||||||
"version": "0.8.22",
|
"version": "0.8.24",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class GreeControllerHousePowerSwitch(CoordinatorEntity[GreeControllerCoordinator
|
|||||||
|
|
||||||
|
|
||||||
class GreeControllerGroupPowerSwitch(CoordinatorEntity[GreeControllerCoordinator], SwitchEntity):
|
class GreeControllerGroupPowerSwitch(CoordinatorEntity[GreeControllerCoordinator], SwitchEntity):
|
||||||
"""Enable or disable one controller climate group."""
|
"""Power a controller climate group on or off."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_name = "Power"
|
_attr_name = "Power"
|
||||||
|
|||||||
+13
-11
@@ -372,7 +372,7 @@
|
|||||||
"plan.ruleCount": "{count} active rules",
|
"plan.ruleCount": "{count} active rules",
|
||||||
"plan.event": "Change",
|
"plan.event": "Change",
|
||||||
"plan.groupSummary": "Group zones: {zones} · requesting action: {demand}",
|
"plan.groupSummary": "Group zones: {zones} · requesting action: {demand}",
|
||||||
"plan.groupOff": "Group is turned off",
|
"plan.groupOff": "Group off — group units are powered down and group ownership is released.",
|
||||||
"zones.enable": "Enable zone",
|
"zones.enable": "Enable zone",
|
||||||
"zones.disable": "Disable zone",
|
"zones.disable": "Disable zone",
|
||||||
"history.90d": "90 days",
|
"history.90d": "90 days",
|
||||||
@@ -549,7 +549,7 @@
|
|||||||
"house.noControl": "Do not control",
|
"house.noControl": "Do not control",
|
||||||
"house.powerAllEyebrow": "Global power",
|
"house.powerAllEyebrow": "Global power",
|
||||||
"house.powerAllTitle": "All air conditioners",
|
"house.powerAllTitle": "All air conditioners",
|
||||||
"house.powerAllHint": "Whole-house automation control. “Turn all off” performs a one-shot OFF for groups and units and stops automation; later remote/Devices control is still allowed. “Turn all on” resumes automation but never starts a unit without an effective Heating/Cooling mode.",
|
"house.powerAllHint": "Whole-house automation control. “Turn all off” powers units down and stops automation without changing each group control enable setting. “Turn all on” resumes automation using the current zone and group settings.",
|
||||||
"house.powerOnAll": "Turn all on",
|
"house.powerOnAll": "Turn all on",
|
||||||
"house.powerOffAll": "Turn all off",
|
"house.powerOffAll": "Turn all off",
|
||||||
"house.powerOnDone": "Whole-house automation resumed",
|
"house.powerOnDone": "Whole-house automation resumed",
|
||||||
@@ -559,25 +559,25 @@
|
|||||||
"nav.groups": "Groups",
|
"nav.groups": "Groups",
|
||||||
"groups.eyebrow": "Group control",
|
"groups.eyebrow": "Group control",
|
||||||
"groups.dashboardTitle": "Selected air conditioners",
|
"groups.dashboardTitle": "Selected air conditioners",
|
||||||
"groups.dashboardHint": "Control each group independently: choose a mode, profile, or set one custom temperature for the whole group.",
|
"groups.dashboardHint": "Control each group independently: ON activates control of its units, OFF powers the units down and releases group ownership. You can also choose mode, preset, or a custom temperature.",
|
||||||
"groups.manage": "Manage groups",
|
"groups.manage": "Manage groups",
|
||||||
"groups.new": "New group",
|
"groups.new": "New group",
|
||||||
"groups.description": "Combine thermostat zones, for example upstairs and downstairs, and control only that set. A group can use a profile or one custom temperature for all member zones.",
|
"groups.description": "Combine thermostat zones such as upstairs and downstairs and control only that set. Turning a group on activates its thermostats; turning it off powers its units down and releases group ownership.",
|
||||||
"groups.group": "Group",
|
"groups.group": "Group",
|
||||||
"groups.namePlaceholder": "Upstairs",
|
"groups.namePlaceholder": "Upstairs",
|
||||||
"groups.members": "Group members",
|
"groups.members": "Group members",
|
||||||
"groups.membersHint": "Groups are built from thermostat zones so their temperature profiles and schedules keep working.",
|
"groups.membersHint": "Groups are built from thermostat zones. ON takes control of group members; OFF powers their units down without leaving a group ownership lock.",
|
||||||
"groups.noMembers": "No members",
|
"groups.noMembers": "No members",
|
||||||
"groups.mixed": "Mixed",
|
"groups.mixed": "Mixed",
|
||||||
"groups.followHouse": "Global mode",
|
"groups.followHouse": "Global mode",
|
||||||
"groups.mode": "Mode",
|
"groups.mode": "Mode",
|
||||||
"groups.profile": "Profile",
|
"groups.profile": "Profile",
|
||||||
"groups.masterOff": "Whole-house automation is off. Pressing ON for this group resumes automation; units start only when an effective Heating/Cooling mode exists.",
|
"groups.masterOff": "Whole-house automation is off. You can prepare group control, but units remain off until whole-house control is enabled.",
|
||||||
"groups.memberCount": "{count} zones",
|
"groups.memberCount": "{count} zones",
|
||||||
"groups.emptyTitle": "No groups yet",
|
"groups.emptyTitle": "No groups yet",
|
||||||
"groups.emptyText": "Create a group to control selected air conditioners together.",
|
"groups.emptyText": "Create a group to control selected air conditioners together.",
|
||||||
"groups.partial": "Group updated, but {count} device commands failed.",
|
"groups.partial": "Group updated, but {count} control operations could not be completed.",
|
||||||
"groups.controlUpdated": "Group control updated.",
|
"groups.controlUpdated": "Group state updated.",
|
||||||
"groups.noZones": "Create thermostat zones before creating a group.",
|
"groups.noZones": "Create thermostat zones before creating a group.",
|
||||||
"automations.targetType": "Target type",
|
"automations.targetType": "Target type",
|
||||||
"automations.groupHint": "For a group, Auto means follow the whole-house heating/cooling mode. Group profiles are applied to all member zones.",
|
"automations.groupHint": "For a group, Auto means follow the whole-house heating/cooling mode. Group profiles are applied to all member zones.",
|
||||||
@@ -606,7 +606,7 @@
|
|||||||
"zones.localThermostatControl": "Local thermostat",
|
"zones.localThermostatControl": "Local thermostat",
|
||||||
"zones.localThermostatOn": "Local control · on",
|
"zones.localThermostatOn": "Local control · on",
|
||||||
"zones.localThermostatOff": "Local control · off",
|
"zones.localThermostatOff": "Local control · off",
|
||||||
"zones.localThermostatOnDescription": "This zone runs locally regardless of the group power gate. Sensor, hysteresis, profile and GREE regulation remain active.",
|
"zones.localThermostatOnDescription": "This zone runs locally regardless of whether group-level control is enabled. Sensor, hysteresis, profile and GREE regulation remain active.",
|
||||||
"zones.localThermostatOffDescription": "This zone is locally off. Without an active countdown it remains outside group control until automation is resumed manually.",
|
"zones.localThermostatOffDescription": "This zone is locally off. Without an active countdown it remains outside group control until automation is resumed manually.",
|
||||||
"zones.unitPowerLocalHint": "Turns this zone's local thermostat on or off; this is not remote-style or direct device control.",
|
"zones.unitPowerLocalHint": "Turns this zone's local thermostat on or off; this is not remote-style or direct device control.",
|
||||||
"zones.localPowerUpdated": "Local thermostat state updated",
|
"zones.localPowerUpdated": "Local thermostat state updated",
|
||||||
@@ -728,7 +728,7 @@
|
|||||||
"history.zoomOut": "Zoom out",
|
"history.zoomOut": "Zoom out",
|
||||||
"history.zoomReset": "Reset zoom",
|
"history.zoomReset": "Reset zoom",
|
||||||
"history.zoomIn": "Zoom in",
|
"history.zoomIn": "Zoom in",
|
||||||
"house.powerOffConfirm": "Turn off all air conditioners and disable all groups?",
|
"house.powerOffConfirm": "Turn off all air conditioners? Group ON/OFF settings will be preserved.",
|
||||||
"zones.temporaryStarting": "Starting…",
|
"zones.temporaryStarting": "Starting…",
|
||||||
"notifications.applicationTokenPlaceholder": "Application token",
|
"notifications.applicationTokenPlaceholder": "Application token",
|
||||||
"notifications.userKeyPlaceholder": "User/group key",
|
"notifications.userKeyPlaceholder": "User/group key",
|
||||||
@@ -778,6 +778,8 @@
|
|||||||
"zones.waitingDeviceDisabled": "Waiting — device is technically disabled",
|
"zones.waitingDeviceDisabled": "Waiting — device is technically disabled",
|
||||||
"zones.waitingMode": "Waiting for the unit mode to change",
|
"zones.waitingMode": "Waiting for the unit mode to change",
|
||||||
"groups.gateOff": "Group off",
|
"groups.gateOff": "Group off",
|
||||||
"zones.disabledByGroup": "Disabled by group: {groups}"
|
"zones.disabledByGroup": "Disabled by group: {groups}",
|
||||||
|
"groups.controlToggle": "Group power",
|
||||||
|
"groups.enableControlFirst": "Turn the group on first."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-11
@@ -372,7 +372,7 @@
|
|||||||
"plan.ruleCount": "Aktywne reguły: {count}",
|
"plan.ruleCount": "Aktywne reguły: {count}",
|
||||||
"plan.event": "Zmiana",
|
"plan.event": "Zmiana",
|
||||||
"plan.groupSummary": "Strefy w grupie: {zones} · żądające działania: {demand}",
|
"plan.groupSummary": "Strefy w grupie: {zones} · żądające działania: {demand}",
|
||||||
"plan.groupOff": "Grupa jest wyłączona",
|
"plan.groupOff": "Grupa wyłączona — jednostki grupy są wyłączone, a blokada grupowa jest zwolniona.",
|
||||||
"zones.enable": "Włącz strefę",
|
"zones.enable": "Włącz strefę",
|
||||||
"zones.disable": "Wyłącz strefę",
|
"zones.disable": "Wyłącz strefę",
|
||||||
"history.90d": "90 dni",
|
"history.90d": "90 dni",
|
||||||
@@ -549,7 +549,7 @@
|
|||||||
"house.noControl": "Nie steruj",
|
"house.noControl": "Nie steruj",
|
||||||
"house.powerAllEyebrow": "Zasilanie globalne",
|
"house.powerAllEyebrow": "Zasilanie globalne",
|
||||||
"house.powerAllTitle": "Wszystkie klimatyzatory",
|
"house.powerAllTitle": "Wszystkie klimatyzatory",
|
||||||
"house.powerAllHint": "Sterowanie automatyki domu. „Wyłącz wszystkie” jednorazowo wyłącza grupy i jednostki oraz zatrzymuje automatykę; późniejsze sterowanie pilotem/Devices jest nadal dozwolone. „Włącz wszystkie” wznawia automatykę, ale nie uruchamia jednostek bez efektywnego trybu Grzanie/Chłodzenie.",
|
"house.powerAllHint": "Sterowanie automatyki domu. „Wyłącz wszystkie” wyłącza jednostki i zatrzymuje automatykę, ale nie zmienia ustawienia włącz/wyłącz poszczególnych grup. „Włącz wszystkie” wznawia automatykę zgodnie z bieżącymi ustawieniami stref i grup.",
|
||||||
"house.powerOnAll": "Włącz wszystkie",
|
"house.powerOnAll": "Włącz wszystkie",
|
||||||
"house.powerOffAll": "Wyłącz wszystkie",
|
"house.powerOffAll": "Wyłącz wszystkie",
|
||||||
"house.powerOnDone": "Wznowiono automatykę domu",
|
"house.powerOnDone": "Wznowiono automatykę domu",
|
||||||
@@ -559,25 +559,25 @@
|
|||||||
"nav.groups": "Grupy",
|
"nav.groups": "Grupy",
|
||||||
"groups.eyebrow": "Sterowanie grupami",
|
"groups.eyebrow": "Sterowanie grupami",
|
||||||
"groups.dashboardTitle": "Wybrane klimatyzatory",
|
"groups.dashboardTitle": "Wybrane klimatyzatory",
|
||||||
"groups.dashboardHint": "Steruj każdą grupą niezależnie: wybierz tryb, profil albo ustaw własną temperaturę dla całej grupy.",
|
"groups.dashboardHint": "Steruj każdą grupą niezależnie: WŁ. uruchamia sterowanie jej jednostkami, WYŁ. wyłącza jednostki i zwalnia blokadę grupową. Możesz też wybrać tryb, profil albo własną temperaturę.",
|
||||||
"groups.manage": "Zarządzaj grupami",
|
"groups.manage": "Zarządzaj grupami",
|
||||||
"groups.new": "Nowa grupa",
|
"groups.new": "Nowa grupa",
|
||||||
"groups.description": "Połącz strefy termostatów, np. góra i dół, i steruj tylko wybranym zestawem. Grupa może używać profilu lub jednej własnej temperatury dla wszystkich stref.",
|
"groups.description": "Połącz strefy termostatów, np. góra i dół, i steruj tylko wybranym zestawem. Włączenie grupy uruchamia jej termostaty, a wyłączenie wyłącza jednostki i zwalnia blokadę grupową.",
|
||||||
"groups.group": "Grupa",
|
"groups.group": "Grupa",
|
||||||
"groups.namePlaceholder": "Góra",
|
"groups.namePlaceholder": "Góra",
|
||||||
"groups.members": "Członkowie grupy",
|
"groups.members": "Członkowie grupy",
|
||||||
"groups.membersHint": "Grupy bazują na strefach termostatów, więc nadal działają ich profile temperatur i harmonogramy.",
|
"groups.membersHint": "Grupy bazują na strefach termostatów. WŁ. przejmuje sterowanie członkami grupy, a WYŁ. wyłącza ich jednostki bez pozostawiania blokady grupowej.",
|
||||||
"groups.noMembers": "Brak członków",
|
"groups.noMembers": "Brak członków",
|
||||||
"groups.mixed": "Różne",
|
"groups.mixed": "Różne",
|
||||||
"groups.followHouse": "Tryb globalny",
|
"groups.followHouse": "Tryb globalny",
|
||||||
"groups.mode": "Tryb",
|
"groups.mode": "Tryb",
|
||||||
"groups.profile": "Profil",
|
"groups.profile": "Profil",
|
||||||
"groups.masterOff": "Automatyka domu jest wyłączona. Naciśnięcie WŁ. dla tej grupy wznowi automatykę; jednostki ruszą tylko przy prawidłowym trybie Grzanie/Chłodzenie.",
|
"groups.masterOff": "Automatyka domu jest wyłączona. Możesz przygotować sterowanie grupą, ale jednostki pozostaną wyłączone do czasu włączenia sterowania domem.",
|
||||||
"groups.memberCount": "Strefy: {count}",
|
"groups.memberCount": "Strefy: {count}",
|
||||||
"groups.emptyTitle": "Brak grup",
|
"groups.emptyTitle": "Brak grup",
|
||||||
"groups.emptyText": "Utwórz grupę, aby wspólnie sterować wybranymi klimatyzatorami.",
|
"groups.emptyText": "Utwórz grupę, aby wspólnie sterować wybranymi klimatyzatorami.",
|
||||||
"groups.partial": "Grupa zaktualizowana, ale nie udało się wykonać {count} poleceń urządzeń.",
|
"groups.partial": "Grupa zaktualizowana, ale nie udało się wykonać {count} operacji sterowania.",
|
||||||
"groups.controlUpdated": "Sterowanie grupą zaktualizowane.",
|
"groups.controlUpdated": "Stan grupy zaktualizowany.",
|
||||||
"groups.noZones": "Najpierw utwórz strefy termostatów.",
|
"groups.noZones": "Najpierw utwórz strefy termostatów.",
|
||||||
"automations.targetType": "Typ celu",
|
"automations.targetType": "Typ celu",
|
||||||
"automations.groupHint": "Dla grupy Auto oznacza dziedziczenie globalnego trybu grzania/chłodzenia. Profil grupy jest stosowany do wszystkich stref w grupie.",
|
"automations.groupHint": "Dla grupy Auto oznacza dziedziczenie globalnego trybu grzania/chłodzenia. Profil grupy jest stosowany do wszystkich stref w grupie.",
|
||||||
@@ -606,7 +606,7 @@
|
|||||||
"zones.localThermostatControl": "Termostat lokalny",
|
"zones.localThermostatControl": "Termostat lokalny",
|
||||||
"zones.localThermostatOn": "Sterowanie lokalne · włączone",
|
"zones.localThermostatOn": "Sterowanie lokalne · włączone",
|
||||||
"zones.localThermostatOff": "Sterowanie lokalne · wyłączone",
|
"zones.localThermostatOff": "Sterowanie lokalne · wyłączone",
|
||||||
"zones.localThermostatOnDescription": "Strefa działa lokalnie niezależnie od bramki grupy. Czujnik, histereza, profil i regulacja GREE pozostają aktywne.",
|
"zones.localThermostatOnDescription": "Strefa działa lokalnie niezależnie od tego, czy sterowanie grupowe jest włączone. Czujnik, histereza, profil i regulacja GREE pozostają aktywne.",
|
||||||
"zones.localThermostatOffDescription": "Strefa jest lokalnie wyłączona. Bez aktywnego licznika pozostaje poza sterowaniem grupy do ręcznego wznowienia automatyki.",
|
"zones.localThermostatOffDescription": "Strefa jest lokalnie wyłączona. Bez aktywnego licznika pozostaje poza sterowaniem grupy do ręcznego wznowienia automatyki.",
|
||||||
"zones.unitPowerLocalHint": "Włącza lub wyłącza lokalny termostat tej strefy; nie jest to sterowanie pilotem ani bezpośrednia komenda urządzenia.",
|
"zones.unitPowerLocalHint": "Włącza lub wyłącza lokalny termostat tej strefy; nie jest to sterowanie pilotem ani bezpośrednia komenda urządzenia.",
|
||||||
"zones.localPowerUpdated": "Zmieniono stan lokalnego termostatu",
|
"zones.localPowerUpdated": "Zmieniono stan lokalnego termostatu",
|
||||||
@@ -728,7 +728,7 @@
|
|||||||
"history.zoomOut": "Pomniejsz wykres",
|
"history.zoomOut": "Pomniejsz wykres",
|
||||||
"history.zoomReset": "Przywróć powiększenie",
|
"history.zoomReset": "Przywróć powiększenie",
|
||||||
"history.zoomIn": "Powiększ wykres",
|
"history.zoomIn": "Powiększ wykres",
|
||||||
"house.powerOffConfirm": "Wyłączyć wszystkie klimatyzatory i dezaktywować wszystkie grupy?",
|
"house.powerOffConfirm": "Wyłączyć wszystkie klimatyzatory? Ustawienia WŁ./WYŁ. grup pozostaną bez zmian.",
|
||||||
"zones.temporaryStarting": "Uruchamianie…",
|
"zones.temporaryStarting": "Uruchamianie…",
|
||||||
"notifications.applicationTokenPlaceholder": "Token aplikacji",
|
"notifications.applicationTokenPlaceholder": "Token aplikacji",
|
||||||
"notifications.userKeyPlaceholder": "Klucz użytkownika/grupy",
|
"notifications.userKeyPlaceholder": "Klucz użytkownika/grupy",
|
||||||
@@ -778,6 +778,8 @@
|
|||||||
"zones.waitingDeviceDisabled": "Oczekuje — urządzenie jest wyłączone technicznie",
|
"zones.waitingDeviceDisabled": "Oczekuje — urządzenie jest wyłączone technicznie",
|
||||||
"zones.waitingMode": "Oczekuje na zmianę trybu jednostki",
|
"zones.waitingMode": "Oczekuje na zmianę trybu jednostki",
|
||||||
"groups.gateOff": "Grupa wyłączona",
|
"groups.gateOff": "Grupa wyłączona",
|
||||||
"zones.disabledByGroup": "Wyłączona przez grupę: {groups}"
|
"zones.disabledByGroup": "Wyłączona przez grupę: {groups}",
|
||||||
|
"groups.controlToggle": "Zasilanie grupy",
|
||||||
|
"groups.enableControlFirst": "Najpierw włącz grupę."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-27
@@ -23,24 +23,6 @@ async fn clear_group_control_sources(state: &AppState, reason: &str) -> Result<(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_all_groups_power(state: &AppState, power: bool) -> Result<(), AppError> {
|
|
||||||
let mut group_ids: Vec<String> = state.db.list_groups()?.into_iter().map(|group| group.id).collect();
|
|
||||||
group_ids.sort();
|
|
||||||
group_ids.dedup();
|
|
||||||
let mut _group_guards = Vec::with_capacity(group_ids.len());
|
|
||||||
for group_id in &group_ids {
|
|
||||||
_group_guards.push(state.lock_group_operation(group_id).await);
|
|
||||||
}
|
|
||||||
for mut group in state.db.list_groups()? {
|
|
||||||
if group.power_enabled == power { continue; }
|
|
||||||
group.power_enabled = power;
|
|
||||||
group.updated_at = Utc::now();
|
|
||||||
state.db.save_group(&group)?;
|
|
||||||
state.broadcast("group.updated", serde_json::to_value(&group)?);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_all_local_thermostat_overrides(state: &AppState) -> Result<usize, AppError> {
|
fn clear_all_local_thermostat_overrides(state: &AppState) -> Result<usize, AppError> {
|
||||||
let mut cleared = 0;
|
let mut cleared = 0;
|
||||||
for mut zone in state.db.list_zones()? {
|
for mut zone in state.db.list_zones()? {
|
||||||
@@ -123,9 +105,6 @@ async fn update_house_control(State(state): State<AppState>, Json(input): Json<H
|
|||||||
};
|
};
|
||||||
state.broadcast("settings.updated", payload.clone());
|
state.broadcast("settings.updated", payload.clone());
|
||||||
clear_group_control_sources(&state, "Whole-house mode control took ownership").await?;
|
clear_group_control_sources(&state, "Whole-house mode control took ownership").await?;
|
||||||
if activate_all {
|
|
||||||
set_all_groups_power(&state, true).await?;
|
|
||||||
}
|
|
||||||
// run_zone_control_now takes the same cycle lock, so release the mutation window first.
|
// run_zone_control_now takes the same cycle lock, so release the mutation window first.
|
||||||
drop(cycle_guard);
|
drop(cycle_guard);
|
||||||
if activate_all {
|
if activate_all {
|
||||||
@@ -157,10 +136,10 @@ async fn update_house_power(State(state): State<AppState>, Json(input): Json<Hou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global power is a true cascade across group gates. OFF clears the current takeover
|
// Whole-house power is independent from group-control enablement. OFF clears current
|
||||||
// markers once, then each enabled device is re-cleared atomically with its OFF command.
|
// ownership markers and powers devices down, but preserves which groups the user has
|
||||||
// A later pilot action is therefore not erased by subsequent controller cycles.
|
// enabled for future group actions. A later house ON therefore does not silently turn
|
||||||
set_all_groups_power(&state, input.power).await?;
|
// disabled group control back on.
|
||||||
clear_group_control_sources(&state, if input.power { "Whole-house power control resumed automation" } else { "Whole-house power disabled" }).await?;
|
clear_group_control_sources(&state, if input.power { "Whole-house power control resumed automation" } else { "Whole-house power disabled" }).await?;
|
||||||
if !input.power {
|
if !input.power {
|
||||||
let zone_snapshot = state.db.list_zones()?;
|
let zone_snapshot = state.db.list_zones()?;
|
||||||
@@ -205,7 +184,7 @@ async fn update_house_power(State(state): State<AppState>, Json(input): Json<Hou
|
|||||||
let settings = state.settings.read().await;
|
let settings = state.settings.read().await;
|
||||||
let settings_payload = public_settings(&settings);
|
let settings_payload = public_settings(&settings);
|
||||||
drop(settings);
|
drop(settings);
|
||||||
state.log("info", "house.power_all", if input.power { "Whole-house automation enabled; thermostat arbiter resumed" } else { "Whole-house power disabled; all groups and enabled devices powered off" }, json!({
|
state.log("info", "house.power_all", if input.power { "Whole-house automation enabled; thermostat arbiter resumed" } else { "Whole-house power disabled; enabled devices powered off while group-control preferences were preserved" }, json!({
|
||||||
"power": input.power,
|
"power": input.power,
|
||||||
"failed": failed.len(),
|
"failed": failed.len(),
|
||||||
}));
|
}));
|
||||||
@@ -237,7 +216,6 @@ async fn update_house_preset(State(state): State<AppState>, Json(input): Json<Ho
|
|||||||
public_settings(&settings)
|
public_settings(&settings)
|
||||||
};
|
};
|
||||||
state.broadcast("settings.updated", settings_payload.clone());
|
state.broadcast("settings.updated", settings_payload.clone());
|
||||||
set_all_groups_power(&state, true).await?;
|
|
||||||
clear_group_control_sources(&state, "Whole-house preset control took ownership").await?;
|
clear_group_control_sources(&state, "Whole-house preset control took ownership").await?;
|
||||||
|
|
||||||
let schedules = state.db.list_schedules()?;
|
let schedules = state.db.list_schedules()?;
|
||||||
|
|||||||
+1
-6
@@ -380,24 +380,19 @@ async fn import_settings(State(state): State<AppState>, Json(mut export): Json<C
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Configuration import never restores ephemeral owners/timers or cached physical state.
|
// Configuration import never restores ephemeral owners/timers or cached physical state.
|
||||||
// Imported devices are reconciled from a fresh poll and current house/group/zone gates.
|
// Imported devices are reconciled from a fresh poll and current house/zone gates.
|
||||||
sanitize_configuration_runtime(&mut export);
|
sanitize_configuration_runtime(&mut export);
|
||||||
state.initial_device_sync_complete.store(false, Ordering::Release);
|
state.initial_device_sync_complete.store(false, Ordering::Release);
|
||||||
state.db.replace_configuration(&export)?;
|
state.db.replace_configuration(&export)?;
|
||||||
state.debug_gree_frames.store(export.settings.debug.gree_frames, Ordering::Relaxed);
|
state.debug_gree_frames.store(export.settings.debug.gree_frames, Ordering::Relaxed);
|
||||||
*state.settings.write().await = export.settings.clone();
|
*state.settings.write().await = export.settings.clone();
|
||||||
|
|
||||||
let disabled_group_zones: std::collections::HashSet<String> = export.groups.iter()
|
|
||||||
.filter(|group| !group.power_enabled)
|
|
||||||
.flat_map(|group| group.zone_ids.iter().cloned())
|
|
||||||
.collect();
|
|
||||||
let controllable_devices: std::collections::HashSet<String> = export.zones.iter()
|
let controllable_devices: std::collections::HashSet<String> = export.zones.iter()
|
||||||
.filter(|zone| {
|
.filter(|zone| {
|
||||||
let effective_mode = if zone.inherit_house_mode { export.settings.house_mode.as_str() } else { zone.mode.as_str() };
|
let effective_mode = if zone.inherit_house_mode { export.settings.house_mode.as_str() } else { zone.mode.as_str() };
|
||||||
export.settings.house_power_enabled
|
export.settings.house_power_enabled
|
||||||
&& zone.enabled
|
&& zone.enabled
|
||||||
&& effective_mode != "off"
|
&& effective_mode != "off"
|
||||||
&& !disabled_group_zones.contains(&zone.id)
|
|
||||||
})
|
})
|
||||||
.map(|zone| zone.device_id.clone())
|
.map(|zone| zone.device_id.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
+1
-3
@@ -618,9 +618,7 @@ async fn apply_zone_control_patch(state: &AppState, id: &str, patch: ZoneControl
|
|||||||
let device_override_cleared = if resume_device_automation { engine::reset_device_manual_override(&mut zone) } else { false };
|
let device_override_cleared = if resume_device_automation { engine::reset_device_manual_override(&mut zone) } else { false };
|
||||||
let runtime = state.settings.read().await.clone();
|
let runtime = state.settings.read().await.clone();
|
||||||
let house_mode = runtime.house_mode.clone();
|
let house_mode = runtime.house_mode.clone();
|
||||||
let blocked_by_group = zone.local_thermostat_power != Some(true)
|
engine::refresh_control_ownership(&mut zone, runtime.house_power_enabled);
|
||||||
&& state.db.list_groups()?.iter().any(|group| !group.power_enabled && group.zone_ids.iter().any(|zone_id| zone_id == &zone.id));
|
|
||||||
engine::refresh_control_ownership(&mut zone, runtime.house_power_enabled, blocked_by_group);
|
|
||||||
engine::refresh_zone_runtime_target(&mut zone, &schedules, &house_mode);
|
engine::refresh_zone_runtime_target(&mut zone, &schedules, &house_mode);
|
||||||
zone.revision = zone.revision.saturating_add(1);
|
zone.revision = zone.revision.saturating_add(1);
|
||||||
zone.updated_at = Utc::now();
|
zone.updated_at = Utc::now();
|
||||||
|
|||||||
@@ -66,15 +66,6 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
|||||||
}));
|
}));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if item.action_group_id.is_none() && device_blocked_by_disabled_group(&item.action_device_id, &zones, &groups) {
|
|
||||||
// Group power-off is authoritative for normal controller-owned zones. A manual
|
|
||||||
// takeover is filtered above and therefore remains higher priority than the group.
|
|
||||||
state.log("info", "automation.blocked_by_group", &format!("Automation {} suppressed by disabled group", item.name), json!({
|
|
||||||
"automation_id": item.id, "device_id": item.action_device_id
|
|
||||||
}));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.action_group_id.is_none()
|
if item.action_group_id.is_none()
|
||||||
&& device_has_enabled_thermostat_zone(&item.action_device_id, &zones)
|
&& device_has_enabled_thermostat_zone(&item.action_device_id, &zones)
|
||||||
&& automation_action_conflicts_with_thermostat(&item.action)
|
&& automation_action_conflicts_with_thermostat(&item.action)
|
||||||
@@ -166,15 +157,6 @@ fn device_blocked_by_local_thermostat(device_id: &str, zones: &[Zone]) -> bool {
|
|||||||
zones.iter().any(|zone| zone.device_id == device_id && zone.local_thermostat_power.is_some())
|
zones.iter().any(|zone| zone.device_id == device_id && zone.local_thermostat_power.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn device_blocked_by_disabled_group(device_id: &str, zones: &[Zone], groups: &[crate::models::ClimateGroup]) -> bool {
|
|
||||||
let zone_ids: std::collections::HashSet<&str> = zones.iter()
|
|
||||||
.filter(|zone| zone.device_id == device_id)
|
|
||||||
.map(|zone| zone.id.as_str())
|
|
||||||
.collect();
|
|
||||||
if zone_ids.is_empty() { return false; }
|
|
||||||
groups.iter().any(|group| !group.power_enabled && group.zone_ids.iter().any(|zone_id| zone_ids.contains(zone_id.as_str())))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_temperature(devices: &[Device], device_id: Option<&str>) -> Option<f64> {
|
fn find_temperature(devices: &[Device], device_id: Option<&str>) -> Option<f64> {
|
||||||
let id = device_id?;
|
let id = device_id?;
|
||||||
// Never fire a temperature automation from stale cached data of an offline/disabled unit.
|
// Never fire a temperature automation from stale cached data of an offline/disabled unit.
|
||||||
|
|||||||
@@ -18,14 +18,12 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
|||||||
for mut zone in zones {
|
for mut zone in zones {
|
||||||
let device = devices.iter().find(|item| item.id == zone.device_id);
|
let device = devices.iter().find(|item| item.id == zone.device_id);
|
||||||
let configured_effective_mode_owned = effective_zone_mode(&zone, &settings.house_mode);
|
let configured_effective_mode_owned = effective_zone_mode(&zone, &settings.house_mode);
|
||||||
let blocked_by_group = zone.local_thermostat_power != Some(true)
|
refresh_control_ownership(&mut zone, settings.house_power_enabled);
|
||||||
&& groups.iter().any(|group| !group.power_enabled && group.zone_ids.iter().any(|zone_id| zone_id == &zone.id));
|
|
||||||
refresh_control_ownership(&mut zone, settings.house_power_enabled, blocked_by_group);
|
|
||||||
let configured_effective_mode = configured_effective_mode_owned.as_str();
|
let configured_effective_mode = configured_effective_mode_owned.as_str();
|
||||||
let manual_device_mode = device.map(|item| if item.power { item.mode.as_str() } else { "off" });
|
let manual_device_mode = device.map(|item| if item.power { item.mode.as_str() } else { "off" });
|
||||||
let effective_mode = if zone.device_manual_override {
|
let effective_mode = if zone.device_manual_override {
|
||||||
manual_device_mode.unwrap_or(configured_effective_mode)
|
manual_device_mode.unwrap_or(configured_effective_mode)
|
||||||
} else if zone.local_thermostat_power == Some(false) || blocked_by_group {
|
} else if zone.local_thermostat_power == Some(false) {
|
||||||
"off"
|
"off"
|
||||||
} else {
|
} else {
|
||||||
configured_effective_mode
|
configured_effective_mode
|
||||||
@@ -48,8 +46,7 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
|||||||
house_events.push(event);
|
house_events.push(event);
|
||||||
}
|
}
|
||||||
let effective_enabled = zone.enabled
|
let effective_enabled = zone.enabled
|
||||||
&& zone.local_thermostat_power != Some(false)
|
&& zone.local_thermostat_power != Some(false);
|
||||||
&& (!blocked_by_group || zone.device_manual_override);
|
|
||||||
zones_out.push(ZoneControlPlan {
|
zones_out.push(ZoneControlPlan {
|
||||||
zone_id: zone.id.clone(),
|
zone_id: zone.id.clone(),
|
||||||
zone_name: zone.name.clone(),
|
zone_name: zone.name.clone(),
|
||||||
@@ -71,7 +68,7 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
|||||||
zone.effective_setpoint.or(Some(resolved_target))
|
zone.effective_setpoint.or(Some(resolved_target))
|
||||||
},
|
},
|
||||||
device_setpoint: device.filter(|item| item.power).map(|item| item.target_temperature),
|
device_setpoint: device.filter(|item| item.power).map(|item| item.target_temperature),
|
||||||
desired_power: settings.house_power_enabled && zone.enabled && effective_mode != "off" && !zone.device_manual_override && !blocked_by_group,
|
desired_power: settings.house_power_enabled && zone.enabled && effective_mode != "off" && !zone.device_manual_override,
|
||||||
desired_mode: effective_mode.to_string(),
|
desired_mode: effective_mode.to_string(),
|
||||||
actual_power: device.map(|item| item.power),
|
actual_power: device.map(|item| item.power),
|
||||||
actual_mode: device.map(|item| if item.power { item.mode.clone() } else { "off".into() }),
|
actual_mode: device.map(|item| if item.power { item.mode.clone() } else { "off".into() }),
|
||||||
@@ -88,7 +85,7 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
|||||||
control_since: zone.control_since,
|
control_since: zone.control_since,
|
||||||
resume_at: zone.control_resume_at,
|
resume_at: zone.control_resume_at,
|
||||||
control_reason: zone.control_reason.clone(),
|
control_reason: zone.control_reason.clone(),
|
||||||
blocked_reason: if !settings.house_power_enabled { Some("global_off".into()) } else if zone.device_manual_override { Some("manual_override".into()) } else if blocked_by_group { Some("group_off".into()) } else if zone.lockout_until.map(|until| until > Utc::now()).unwrap_or(false) { Some(zone.lockout_reason.clone().unwrap_or_else(|| "lockout".into())) } else if !zone.enabled { Some("zone_disabled".into()) } else if device.map(|d| !d.online || d.communication_failures > 0).unwrap_or(true) { Some("offline".into()) } else { None },
|
blocked_reason: if !settings.house_power_enabled { Some("global_off".into()) } else if zone.device_manual_override { Some("manual_override".into()) } else if zone.lockout_until.map(|until| until > Utc::now()).unwrap_or(false) { Some(zone.lockout_reason.clone().unwrap_or_else(|| "lockout".into())) } else if !zone.enabled { Some("zone_disabled".into()) } else if device.map(|d| !d.online || d.communication_failures > 0).unwrap_or(true) { Some("offline".into()) } else { None },
|
||||||
lockout_until: zone.lockout_until,
|
lockout_until: zone.lockout_until,
|
||||||
current_schedule_id: active.map(|item| item.id.clone()),
|
current_schedule_id: active.map(|item| item.id.clone()),
|
||||||
current_schedule_name: active.map(|item| item.name.clone()),
|
current_schedule_name: active.map(|item| item.name.clone()),
|
||||||
|
|||||||
+121
-47
@@ -26,7 +26,7 @@ pub async fn control_group(state: &AppState, group_id: &str, patch: GroupControl
|
|||||||
// is being applied. Group and zone locks then make the member update atomic.
|
// is being applied. Group and zone locks then make the member update atomic.
|
||||||
let _house_guard = state.lock_house_operation().await;
|
let _house_guard = state.lock_house_operation().await;
|
||||||
// Group state participates in thermostat arbitration. Exclude an already-running cycle
|
// Group state participates in thermostat arbitration. Exclude an already-running cycle
|
||||||
// while changing the group gate/profile so no cycle can act on a stale group snapshot.
|
// while changing group control/profile state so no cycle can act on a stale group snapshot.
|
||||||
// Lock order stays house -> cycle -> group -> zone -> device.
|
// Lock order stays house -> cycle -> group -> zone -> device.
|
||||||
let _cycle_guard = state.lock_zone_control_cycle().await;
|
let _cycle_guard = state.lock_zone_control_cycle().await;
|
||||||
let _group_guard = state.lock_group_operation(group_id).await;
|
let _group_guard = state.lock_group_operation(group_id).await;
|
||||||
@@ -55,6 +55,23 @@ pub async fn control_group(state: &AppState, group_id: &str, patch: GroupControl
|
|||||||
let schedules = state.db.list_schedules()?;
|
let schedules = state.db.list_schedules()?;
|
||||||
let custom_setpoint = patch.setpoint.map(|value| (value * 10.0).round() / 10.0);
|
let custom_setpoint = patch.setpoint.map(|value| (value * 10.0).round() / 10.0);
|
||||||
let climate_change = patch.mode.is_some() || patch.preset.is_some() || custom_setpoint.is_some();
|
let climate_change = patch.mode.is_some() || patch.preset.is_some() || custom_setpoint.is_some();
|
||||||
|
let resulting_control_enabled = patch.power.unwrap_or(group.power_enabled);
|
||||||
|
if climate_change && !resulting_control_enabled {
|
||||||
|
if source == "automation.group" {
|
||||||
|
state.log("info", "automation.group_control_disabled", &format!("Group automation suppressed because group control is disabled for {}", group.name), json!({
|
||||||
|
"group_id": group.id, "source": source
|
||||||
|
}));
|
||||||
|
return Ok(json!({
|
||||||
|
"group": group,
|
||||||
|
"zones": [],
|
||||||
|
"devices": state.db.list_devices()?,
|
||||||
|
"failed": [],
|
||||||
|
"master_power_enabled": state.settings.read().await.house_power_enabled,
|
||||||
|
"suppressed": true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return Err(AppError::BadRequest("enable group control before changing group mode, preset or setpoint".into()));
|
||||||
|
}
|
||||||
if let Some(power) = patch.power {
|
if let Some(power) = patch.power {
|
||||||
group.power_enabled = power;
|
group.power_enabled = power;
|
||||||
}
|
}
|
||||||
@@ -62,27 +79,39 @@ pub async fn control_group(state: &AppState, group_id: &str, patch: GroupControl
|
|||||||
state.db.save_group(&group)?;
|
state.db.save_group(&group)?;
|
||||||
state.broadcast("group.updated", serde_json::to_value(&group)?);
|
state.broadcast("group.updated", serde_json::to_value(&group)?);
|
||||||
|
|
||||||
// Explicit group ON is a conscious request to run this group. Resume the global
|
// Group power is a scoped bulk power action. OFF powers member thermostats down but
|
||||||
// master without changing the gates of any other groups. This makes group ON work
|
// releases group ownership: members are represented as individually OFF, not as
|
||||||
// even after a previous whole-house OFF while preserving multi-group OFF priority.
|
// "blocked by a disabled group". ON clears that scoped OFF and lets group arbitration
|
||||||
if patch.power == Some(true) && source != "automation.group" {
|
// take ownership again. The whole-house master remains independent.
|
||||||
let mut settings = state.settings.write().await;
|
let explicit_group_power = patch.power.is_some() && source != "automation.group";
|
||||||
if !settings.house_power_enabled {
|
|
||||||
settings.house_power_enabled = true;
|
|
||||||
state.db.save_runtime_settings(&settings)?;
|
|
||||||
state.broadcast("house.power_changed", json!({"house_power_enabled": true}));
|
|
||||||
state.log("info", "house.power_resumed_by_group", &format!("Whole-house master resumed by group {}", group.name), json!({
|
|
||||||
"group_id": group.id, "source": source
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut zones = Vec::new();
|
let mut zones = Vec::new();
|
||||||
|
let mut failed = Vec::new();
|
||||||
|
let mut forced_off_devices = std::collections::HashSet::new();
|
||||||
for zone_id in &group.zone_ids {
|
for zone_id in &group.zone_ids {
|
||||||
let Some(zone_snapshot) = state.db.get_zone(zone_id)? else { continue; };
|
let Some(zone_snapshot) = state.db.get_zone(zone_id)? else { continue; };
|
||||||
let _device_guard = state.lock_device_operation(&zone_snapshot.device_id).await;
|
let _device_guard = state.lock_device_operation(&zone_snapshot.device_id).await;
|
||||||
let Some(mut zone) = state.db.get_zone(zone_id)? else { continue; };
|
let Some(mut zone) = state.db.get_zone(zone_id)? else { continue; };
|
||||||
let temporary_owns_zone = temporary_quick_thermostat_is_active(&zone, Utc::now());
|
let mut temporary_owns_zone = temporary_quick_thermostat_is_active(&zone, Utc::now());
|
||||||
|
|
||||||
|
// A user/HA group power click is an explicit scoped takeover. End any older direct
|
||||||
|
// or temporary ownership so all members react consistently to the bulk command.
|
||||||
|
// Scheduled automations deliberately do not do this: manual/local ownership keeps
|
||||||
|
// its higher priority and the automation can only affect currently free members.
|
||||||
|
if explicit_group_power {
|
||||||
|
if zone.temporary_quick_thermostat.is_some() {
|
||||||
|
if temporary_owns_zone {
|
||||||
|
finish_temporary_quick_thermostat(&mut zone, &schedules, &state.settings.read().await.house_mode);
|
||||||
|
} else {
|
||||||
|
zone.temporary_quick_thermostat = None;
|
||||||
|
}
|
||||||
|
temporary_owns_zone = false;
|
||||||
|
}
|
||||||
|
if zone.device_manual_override {
|
||||||
|
reset_device_manual_override(&mut zone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if temporary_owns_zone {
|
if temporary_owns_zone {
|
||||||
if let Some(session) = zone.temporary_quick_thermostat.as_mut() {
|
if let Some(session) = zone.temporary_quick_thermostat.as_mut() {
|
||||||
if let Some(mode) = patch.mode.as_deref() { session.deferred_mode = Some(mode.to_string()); }
|
if let Some(mode) = patch.mode.as_deref() { session.deferred_mode = Some(mode.to_string()); }
|
||||||
@@ -127,48 +156,93 @@ pub async fn control_group(state: &AppState, group_id: &str, patch: GroupControl
|
|||||||
zone.manual_override_until = next_schedule_boundary_utc(&zone.id, &schedules, Local::now());
|
zone.manual_override_until = next_schedule_boundary_utc(&zone.id, &schedules, Local::now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !temporary_owns_zone && !zone.device_manual_override && zone.local_thermostat_power.is_none() {
|
// Power OFF is persisted as an ordinary per-zone thermostat OFF, with no automatic
|
||||||
zone.control_owner = "automation".into();
|
// 15-minute hand-back. This keeps the physical unit off while removing group ownership
|
||||||
zone.control_source = format!("group:{}", group.id);
|
// and, importantly, still lets the user turn that thermostat back on independently.
|
||||||
zone.control_since = Some(Utc::now());
|
// Power ON clears this scoped OFF and returns the member to group arbitration.
|
||||||
zone.control_reason = format!("Controlled by group {}", group.name);
|
let mut force_power_off_after_save = false;
|
||||||
|
let mut applied_group_power: Option<bool> = None;
|
||||||
|
if let Some(power) = patch.power {
|
||||||
|
let automatic_power_blocked = source == "automation.group"
|
||||||
|
&& (zone.device_manual_override || zone.local_thermostat_power.is_some() || temporary_owns_zone);
|
||||||
|
if automatic_power_blocked {
|
||||||
|
failed.push(json!({
|
||||||
|
"scope": "ownership",
|
||||||
|
"zone_id": zone.id,
|
||||||
|
"device_id": zone.device_id,
|
||||||
|
"error": "manual/local thermostat ownership has priority over group automation",
|
||||||
|
}));
|
||||||
|
} else if power {
|
||||||
|
zone.local_thermostat_power = None;
|
||||||
|
zone.local_thermostat_resume_at = None;
|
||||||
|
zone.local_thermostat_restore_zone_enabled = None;
|
||||||
|
applied_group_power = Some(true);
|
||||||
|
} else {
|
||||||
|
zone.local_thermostat_power = Some(false);
|
||||||
|
zone.local_thermostat_resume_at = None;
|
||||||
|
zone.local_thermostat_restore_zone_enabled = None;
|
||||||
|
zone.demand = false;
|
||||||
|
zone.demand_since = None;
|
||||||
|
zone.effective_mode = "off".into();
|
||||||
|
zone.device_setpoint = None;
|
||||||
|
force_power_off_after_save = forced_off_devices.insert(zone.device_id.clone());
|
||||||
|
applied_group_power = Some(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !temporary_owns_zone && !zone.device_manual_override {
|
||||||
|
if applied_group_power == Some(false) {
|
||||||
|
zone.control_owner = "local_thermostat".into();
|
||||||
|
zone.control_source = "local_thermostat".into();
|
||||||
|
zone.control_since = Some(Utc::now());
|
||||||
|
zone.control_resume_at = None;
|
||||||
|
zone.control_reason = format!("Group {} powered the thermostat off; group ownership released", group.name);
|
||||||
|
} else if group.power_enabled && zone.local_thermostat_power.is_none() {
|
||||||
|
zone.control_owner = "automation".into();
|
||||||
|
zone.control_source = format!("group:{}", group.id);
|
||||||
|
zone.control_since = Some(Utc::now());
|
||||||
|
zone.control_resume_at = zone.manual_override_until;
|
||||||
|
zone.control_reason = format!("Controlled by group {}", group.name);
|
||||||
|
} else if !group.power_enabled && zone.control_source == format!("group:{}", group.id) {
|
||||||
|
zone.control_owner = "automation".into();
|
||||||
|
zone.control_source = "automation".into();
|
||||||
|
zone.control_since = Some(Utc::now());
|
||||||
|
zone.control_resume_at = zone.manual_override_until;
|
||||||
|
zone.control_reason = format!("Group {} ownership released; zone automation resumed", group.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
zone.revision = zone.revision.saturating_add(1);
|
zone.revision = zone.revision.saturating_add(1);
|
||||||
zone.updated_at = Utc::now();
|
zone.updated_at = Utc::now();
|
||||||
state.db.save_zone(&zone)?;
|
state.db.save_zone(&zone)?;
|
||||||
state.broadcast("zone.updated", serde_json::to_value(&zone)?);
|
state.broadcast("zone.updated", serde_json::to_value(&zone)?);
|
||||||
|
if force_power_off_after_save {
|
||||||
|
if let Err(err) = force_power_off_device_locked(state, &zone.device_id).await {
|
||||||
|
state.log("error", "group.power_off_error", &err.to_string(), json!({
|
||||||
|
"group_id": group.id,
|
||||||
|
"zone_id": zone.id,
|
||||||
|
"device_id": zone.device_id,
|
||||||
|
"source": source,
|
||||||
|
}));
|
||||||
|
failed.push(json!({
|
||||||
|
"scope": "device",
|
||||||
|
"zone_id": zone.id,
|
||||||
|
"device_id": zone.device_id,
|
||||||
|
"error": err.to_string(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
zones.push(zone);
|
zones.push(zone);
|
||||||
}
|
}
|
||||||
|
|
||||||
let runtime = state.settings.read().await.clone();
|
let runtime = state.settings.read().await.clone();
|
||||||
let master_power_enabled = runtime.house_power_enabled;
|
let master_power_enabled = runtime.house_power_enabled;
|
||||||
let should_command_power = patch.power.is_some();
|
let control_toggled_on = patch.power == Some(true);
|
||||||
let desired_power = group.power_enabled;
|
let control_enabled = group.power_enabled;
|
||||||
// A zone may intentionally belong to more than one group. Power-off is authoritative:
|
|
||||||
// turning one group on must never briefly wake a member that is still blocked by another group.
|
|
||||||
let mut failed = Vec::new();
|
|
||||||
if should_command_power && !desired_power {
|
|
||||||
let mut seen = std::collections::HashSet::new();
|
|
||||||
for zone in &zones {
|
|
||||||
if !seen.insert(zone.device_id.clone()) { continue; }
|
|
||||||
// Group OFF is an immediate safety transition. Group ON never emits a bare
|
|
||||||
// power=true frame; the thermostat arbiter starts the unit with mode/target.
|
|
||||||
let Some(device) = state.db.get_device(&zone.device_id)? else { continue; };
|
|
||||||
if !device.enabled { continue; }
|
|
||||||
match send_group_power_if_current(state, &group.id, &zone.id, &device.id, false).await {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) => {
|
|
||||||
state.log("error", "group.power_error", &err.to_string(), json!({
|
|
||||||
"group_id": group.id, "device_id": device.id, "device_name": device.name,
|
|
||||||
"power": false, "source": source,
|
|
||||||
}));
|
|
||||||
failed.push(json!({"device_id": device.id, "device_name": device.name, "error": err.to_string()}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let run_immediately = desired_power && (should_command_power || climate_change);
|
// OFF is already a forced, per-member physical transition performed under zone -> device
|
||||||
|
// locks above. ON/profile/setpoint changes use the thermostat arbiter so target, hysteresis,
|
||||||
|
// compressor protection and mode-change safety are applied consistently before returning.
|
||||||
|
let run_immediately = control_toggled_on || (control_enabled && climate_change);
|
||||||
let zones = if run_immediately {
|
let zones = if run_immediately {
|
||||||
// Do not just wake the background loop: a group ON/profile/setpoint action is expected
|
// Do not just wake the background loop: a group ON/profile/setpoint action is expected
|
||||||
// to arbitrate every member before the HTTP request completes. Release member/domain
|
// to arbitrate every member before the HTTP request completes. Release member/domain
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
pub fn refresh_control_ownership(zone: &mut Zone, house_power_enabled: bool, blocked_by_group: bool) {
|
pub fn refresh_control_ownership(zone: &mut Zone, house_power_enabled: bool) {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let (owner, source, resume_at, reason) = if !house_power_enabled {
|
let (owner, source, resume_at, reason) = if !house_power_enabled {
|
||||||
("global_off", "global".to_string(), None, "Whole-house power is disabled".to_string())
|
("global_off", "global".to_string(), None, "Whole-house power is disabled".to_string())
|
||||||
@@ -24,9 +24,6 @@ pub fn refresh_control_ownership(zone: &mut Zone, house_power_enabled: bool, blo
|
|||||||
"Local thermostat owns the zone".into()
|
"Local thermostat owns the zone".into()
|
||||||
};
|
};
|
||||||
("local_thermostat", source, resume_at, reason)
|
("local_thermostat", source, resume_at, reason)
|
||||||
} else if blocked_by_group {
|
|
||||||
let source = if zone.control_source.starts_with("group:") { zone.control_source.clone() } else { "group".into() };
|
|
||||||
("automation", source, None, "Zone is blocked by a disabled group".to_string())
|
|
||||||
} else {
|
} else {
|
||||||
let source = if zone.control_source.starts_with("group:") { zone.control_source.clone() } else { "automation".into() };
|
let source = if zone.control_source.starts_with("group:") { zone.control_source.clone() } else { "automation".into() };
|
||||||
("automation", source, zone.manual_override_until, "Automatic thermostat/schedule control".to_string())
|
("automation", source, zone.manual_override_until, "Automatic thermostat/schedule control".to_string())
|
||||||
|
|||||||
@@ -22,20 +22,7 @@ async fn thermostat_ownership_is_current(state: &AppState, zone_id: &str, device
|
|||||||
if !state.settings.read().await.house_power_enabled { return Ok(false); }
|
if !state.settings.read().await.house_power_enabled { return Ok(false); }
|
||||||
let Some(zone) = state.db.get_zone(zone_id)? else { return Ok(false); };
|
let Some(zone) = state.db.get_zone(zone_id)? else { return Ok(false); };
|
||||||
if zone.device_id != device_id || !zone.enabled || zone.device_manual_override || zone.local_thermostat_power == Some(false) { return Ok(false); }
|
if zone.device_id != device_id || !zone.enabled || zone.device_manual_override || zone.local_thermostat_power == Some(false) { return Ok(false); }
|
||||||
if zone.local_thermostat_power == Some(true) { return Ok(true); }
|
Ok(true)
|
||||||
let blocked = state.db.list_groups()?.iter().any(|group| {
|
|
||||||
!group.power_enabled && group.zone_ids.iter().any(|member| member == zone_id)
|
|
||||||
});
|
|
||||||
Ok(!blocked)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn group_off_ownership_is_current(state: &AppState, zone_id: &str, device_id: &str) -> Result<bool, AppError> {
|
|
||||||
if !state.settings.read().await.house_power_enabled { return Ok(false); }
|
|
||||||
let Some(zone) = state.db.get_zone(zone_id)? else { return Ok(false); };
|
|
||||||
if zone.device_id != device_id || !zone.enabled || zone.device_manual_override || zone.local_thermostat_power.is_some() { return Ok(false); }
|
|
||||||
Ok(state.db.list_groups()?.iter().any(|group| {
|
|
||||||
!group.power_enabled && group.zone_ids.iter().any(|member| member == zone_id)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_zone_command_if_owned(
|
async fn send_zone_command_if_owned(
|
||||||
@@ -43,57 +30,16 @@ async fn send_zone_command_if_owned(
|
|||||||
zone_id: &str,
|
zone_id: &str,
|
||||||
device_id: &str,
|
device_id: &str,
|
||||||
command: DeviceCommand,
|
command: DeviceCommand,
|
||||||
require_group_block: bool,
|
|
||||||
) -> Result<Option<Device>, AppError> {
|
) -> Result<Option<Device>, AppError> {
|
||||||
// Ownership must be checked after acquiring the same per-device lock used by polling.
|
// Ownership must be checked after acquiring the same per-device lock used by polling.
|
||||||
// Otherwise polling could detect a remote takeover while this task is waiting for the lock,
|
// Otherwise polling could detect a remote takeover while this task is waiting for the lock,
|
||||||
// and a stale thermostat decision would still be sent immediately afterwards.
|
// and a stale thermostat decision would still be sent immediately afterwards.
|
||||||
let _device_guard = state.lock_device_operation(device_id).await;
|
let _device_guard = state.lock_device_operation(device_id).await;
|
||||||
let owned = if require_group_block {
|
let owned = thermostat_ownership_is_current(state, zone_id, device_id).await?;
|
||||||
group_off_ownership_is_current(state, zone_id, device_id).await?
|
|
||||||
} else {
|
|
||||||
thermostat_ownership_is_current(state, zone_id, device_id).await?
|
|
||||||
};
|
|
||||||
if !owned { return Ok(None); }
|
if !owned { return Ok(None); }
|
||||||
send_command_locked(state, device_id, command).await.map(Some)
|
send_command_locked(state, device_id, command).await.map(Some)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_group_power_if_current(
|
|
||||||
state: &AppState,
|
|
||||||
group_id: &str,
|
|
||||||
zone_id: &str,
|
|
||||||
device_id: &str,
|
|
||||||
desired_power: bool,
|
|
||||||
) -> Result<Option<Device>, AppError> {
|
|
||||||
let _device_guard = state.lock_device_operation(device_id).await;
|
|
||||||
let Some(zone) = state.db.get_zone(zone_id)? else { return Ok(None); };
|
|
||||||
if zone.device_id != device_id || !zone.enabled || zone.device_manual_override || zone.local_thermostat_power.is_some() { return Ok(None); }
|
|
||||||
let groups = state.db.list_groups()?;
|
|
||||||
let Some(group) = groups.iter().find(|group| group.id == group_id) else { return Ok(None); };
|
|
||||||
if group.power_enabled != desired_power || !group.zone_ids.iter().any(|member| member == zone_id) { return Ok(None); }
|
|
||||||
if desired_power {
|
|
||||||
let settings = state.settings.read().await;
|
|
||||||
if !settings.house_power_enabled { return Ok(None); }
|
|
||||||
let effective_mode = if zone.inherit_house_mode { settings.house_mode.as_str() } else { zone.mode.as_str() };
|
|
||||||
if effective_mode == "off" { return Ok(None); }
|
|
||||||
if groups.iter().any(|other| {
|
|
||||||
other.id != group_id && !other.power_enabled && other.zone_ids.iter().any(|member| member == zone_id)
|
|
||||||
}) {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let Some(device) = state.db.get_device(device_id)? else { return Ok(None); };
|
|
||||||
if !device.enabled { return Ok(None); }
|
|
||||||
let command = DeviceCommand { power: Some(desired_power), ..Default::default() };
|
|
||||||
if desired_power {
|
|
||||||
send_command_locked(state, device_id, command).await.map(Some)
|
|
||||||
} else {
|
|
||||||
// A deliberate group OFF is a one-shot safety transition. Send it even if the
|
|
||||||
// cached state already says OFF; the regulator itself will not keep repeating it.
|
|
||||||
send_command_locked_forced(state, device_id, command).await.map(Some)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_automatic_device_command_if_owned(
|
async fn send_automatic_device_command_if_owned(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
device_id: &str,
|
device_id: &str,
|
||||||
@@ -105,7 +51,6 @@ async fn send_automatic_device_command_if_owned(
|
|||||||
if device_blocked_by_disabled_zone(device_id, &zones)
|
if device_blocked_by_disabled_zone(device_id, &zones)
|
||||||
|| device_blocked_by_manual_override(device_id, &zones)
|
|| device_blocked_by_manual_override(device_id, &zones)
|
||||||
|| device_blocked_by_local_thermostat(device_id, &zones)
|
|| device_blocked_by_local_thermostat(device_id, &zones)
|
||||||
|| device_blocked_by_disabled_group(device_id, &zones, &state.db.list_groups()?)
|
|
||||||
{
|
{
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -132,10 +77,6 @@ async fn apply_automatic_device_action(
|
|||||||
if !settings.house_power_enabled || zone.device_manual_override || zone.local_thermostat_power.is_some() {
|
if !settings.house_power_enabled || zone.device_manual_override || zone.local_thermostat_power.is_some() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let groups = state.db.list_groups()?;
|
|
||||||
if groups.iter().any(|group| !group.power_enabled && group.zone_ids.iter().any(|member| member == &zone.id)) {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
// A power-on automation is an explicit domain transition and may re-enable a zone that
|
// A power-on automation is an explicit domain transition and may re-enable a zone that
|
||||||
// a previous power automation disabled. Other actions still respect a disabled zone gate.
|
// a previous power automation disabled. Other actions still respect a disabled zone gate.
|
||||||
if !zone.enabled && command.power != Some(true) { return Ok(None); }
|
if !zone.enabled && command.power != Some(true) { return Ok(None); }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
async fn control_zones(state: &AppState) -> Result<()> {
|
async fn control_zones(state: &AppState) -> Result<()> {
|
||||||
let _cycle_guard = state.lock_zone_control_cycle().await;
|
let _cycle_guard = state.lock_zone_control_cycle().await;
|
||||||
let schedules = state.db.list_schedules()?;
|
let schedules = state.db.list_schedules()?;
|
||||||
let groups = state.db.list_groups()?;
|
|
||||||
let settings = state.settings.read().await.clone();
|
let settings = state.settings.read().await.clone();
|
||||||
let mut zone_snapshot = state.db.list_zones()?;
|
let mut zone_snapshot = state.db.list_zones()?;
|
||||||
// Local quick-thermostat OFF is intentionally temporary. Expire the ownership marker
|
// Local quick-thermostat OFF is intentionally temporary. Expire the ownership marker
|
||||||
@@ -119,9 +118,7 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
|||||||
// remains authoritative and is checked before this loop.
|
// remains authoritative and is checked before this loop.
|
||||||
let effective_mode_owned = effective_zone_mode(&zone, &settings.house_mode);
|
let effective_mode_owned = effective_zone_mode(&zone, &settings.house_mode);
|
||||||
zone.effective_mode = effective_mode_owned.clone();
|
zone.effective_mode = effective_mode_owned.clone();
|
||||||
let ownership_blocked_by_group = zone.local_thermostat_power != Some(true)
|
refresh_control_ownership(&mut zone, settings.house_power_enabled);
|
||||||
&& groups.iter().any(|group| !group.power_enabled && group.zone_ids.iter().any(|zone_id| zone_id == &zone.id));
|
|
||||||
refresh_control_ownership(&mut zone, settings.house_power_enabled, ownership_blocked_by_group);
|
|
||||||
let effective_mode = effective_mode_owned.as_str();
|
let effective_mode = effective_mode_owned.as_str();
|
||||||
|
|
||||||
let previous_source = zone.control_temperature_source.clone();
|
let previous_source = zone.control_temperature_source.clone();
|
||||||
@@ -289,30 +286,6 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let blocked_by_group = ownership_blocked_by_group;
|
|
||||||
if blocked_by_group {
|
|
||||||
zone.effective_mode = "off".into();
|
|
||||||
zone.demand = false;
|
|
||||||
zone.demand_since = None;
|
|
||||||
zone.device_setpoint = None;
|
|
||||||
if device.online && device.communication_failures == 0 && device.power {
|
|
||||||
match send_zone_command_if_owned(
|
|
||||||
state,
|
|
||||||
&zone.id,
|
|
||||||
&zone.device_id,
|
|
||||||
DeviceCommand { power: Some(false), ..Default::default() },
|
|
||||||
true,
|
|
||||||
).await {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) => state.log("error", "group.power_error", &err.to_string(), json!({"zone_id": zone.id, "device_id": zone.device_id})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
record_zone_history(state, &zone, outdoor_temperature, settings.poll_interval_seconds);
|
|
||||||
let persisted_zone = persist_zone_cycle(state, &zone, cycle_started_at)?;
|
|
||||||
state.broadcast("zone.updated", serde_json::to_value(&persisted_zone)?);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if discrepancy && previous_source != "device_discrepancy_fallback" {
|
if discrepancy && previous_source != "device_discrepancy_fallback" {
|
||||||
state.log("warn", "zone.sensor_discrepancy", &format!("Zone {} sensors differ by more than {:.1} C; using GREE sensor", zone.name, zone.max_sensor_difference), json!({
|
state.log("warn", "zone.sensor_discrepancy", &format!("Zone {} sensors differ by more than {:.1} C; using GREE sensor", zone.name, zone.max_sensor_difference), json!({
|
||||||
"zone_id": zone.id,
|
"zone_id": zone.id,
|
||||||
@@ -464,7 +437,7 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
|||||||
state.broadcast("zone.updated", serde_json::to_value(&persisted_zone)?);
|
state.broadcast("zone.updated", serde_json::to_value(&persisted_zone)?);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match send_zone_command_if_owned(state, &zone.id, &zone.device_id, DeviceCommand { power: Some(false), ..Default::default() }, false).await {
|
match send_zone_command_if_owned(state, &zone.id, &zone.device_id, DeviceCommand { power: Some(false), ..Default::default() }).await {
|
||||||
Ok(Some(_)) => {
|
Ok(Some(_)) => {
|
||||||
zone.last_power_change_at = Some(now);
|
zone.last_power_change_at = Some(now);
|
||||||
zone.lockout_until = Some(now + chrono::Duration::seconds(zone.min_off_seconds as i64));
|
zone.lockout_until = Some(now + chrono::Duration::seconds(zone.min_off_seconds as i64));
|
||||||
@@ -516,7 +489,7 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
|||||||
sleep: desired_sleep,
|
sleep: desired_sleep,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
match send_zone_command_if_owned(state, &zone.id, &zone.device_id, command, false).await {
|
match send_zone_command_if_owned(state, &zone.id, &zone.device_id, command).await {
|
||||||
Ok(Some(updated_device)) => {
|
Ok(Some(updated_device)) => {
|
||||||
let transition_at = Utc::now();
|
let transition_at = Utc::now();
|
||||||
if device.power != updated_device.power { zone.last_power_change_at = Some(transition_at); }
|
if device.power != updated_device.power { zone.last_power_change_at = Some(transition_at); }
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ pub struct ZoneControlPlan {
|
|||||||
pub device_name: String,
|
pub device_name: String,
|
||||||
/// Configured per-zone enable switch. Group/master gates are reported separately.
|
/// Configured per-zone enable switch. Group/master gates are reported separately.
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
/// True when the configured zone is not currently blocked by a disabled climate group.
|
/// True when the configured zone is eligible for thermostat control. Disabled group control does not block the zone.
|
||||||
pub effective_enabled: bool,
|
pub effective_enabled: bool,
|
||||||
/// Effective mode currently used by the controller.
|
/// Effective mode currently used by the controller.
|
||||||
pub mode: String,
|
pub mode: String,
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ pub struct ClimateGroup {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub zone_ids: Vec<String>,
|
pub zone_ids: Vec<String>,
|
||||||
|
/// Group bulk-power/control state. False powers member thermostats off while releasing
|
||||||
|
/// group ownership; each member can still be turned back on independently.
|
||||||
#[serde(default = "default_group_power_enabled")]
|
#[serde(default = "default_group_power_enabled")]
|
||||||
pub power_enabled: bool,
|
pub power_enabled: bool,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
|||||||
+4
-4
@@ -62,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel dashboard-global-panel">
|
<div class="panel dashboard-global-panel">
|
||||||
<div class="dashboard-global-power">
|
<div class="dashboard-global-power">
|
||||||
<div><span class="eyebrow" data-i18n="house.powerAllEyebrow">Global power</span><h3 data-i18n="house.powerAllTitle">All air conditioners</h3><p data-i18n="house.powerAllHint">Master power for thermostat zones. Turn all off also disables all groups; turn all on starts enabled zones only.</p></div>
|
<div><span class="eyebrow" data-i18n="house.powerAllEyebrow">Global power</span><h3 data-i18n="house.powerAllTitle">All air conditioners</h3><p data-i18n="house.powerAllHint">Master power for thermostat zones. Turn all off powers units down without changing group-control settings; turn all on resumes eligible zones.</p></div>
|
||||||
<div class="house-power-actions"><button id="housePowerOn" type="button" class="secondary" data-action="house-power" data-value="true" data-i18n="house.powerOnAll">Turn all on</button><button id="housePowerOff" type="button" class="secondary" data-action="house-power" data-value="false" data-i18n="house.powerOffAll">Turn all off</button></div>
|
<div class="house-power-actions"><button id="housePowerOn" type="button" class="secondary" data-action="house-power" data-value="true" data-i18n="house.powerOnAll">Turn all on</button><button id="housePowerOff" type="button" class="secondary" data-action="house-power" data-value="false" data-i18n="house.powerOffAll">Turn all off</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-global-divider"></div>
|
<div class="dashboard-global-divider"></div>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
<div class="dashboard-tab-panel" data-dashboard-panel="thermostats" hidden>
|
<div class="dashboard-tab-panel" data-dashboard-panel="thermostats" hidden>
|
||||||
<section class="dashboard-primary-section">
|
<section class="dashboard-primary-section">
|
||||||
<div class="dashboard-section-head">
|
<div class="dashboard-section-head">
|
||||||
<div><span class="eyebrow" data-i18n="groups.eyebrow">Group control</span><h2 data-i18n="groups.dashboardTitle">Control selected air conditioners</h2><p data-i18n="groups.dashboardHint">Each group can use its own cooling/heating mode and Auto/Comfort/Sleep/Away profile.</p></div>
|
<div><span class="eyebrow" data-i18n="groups.eyebrow">Group control</span><h2 data-i18n="groups.dashboardTitle">Control selected air conditioners</h2><p data-i18n="groups.dashboardHint">Group ON activates its thermostats; Group OFF powers its units down and releases group ownership. You can also choose a mode, preset, or custom temperature.</p></div>
|
||||||
<div class="dashboard-section-actions"><span class="badge" id="dashboardGroupCount">0</span><button type="button" class="secondary" data-go="groups" data-i18n="groups.manage">Manage groups</button></div>
|
<div class="dashboard-section-actions"><span class="badge" id="dashboardGroupCount">0</span><button type="button" class="secondary" data-go="groups" data-i18n="groups.manage">Manage groups</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="group-quick-grid dashboard-group-strip" id="dashboardGroups"></div>
|
<div class="group-quick-grid dashboard-group-strip" id="dashboardGroups"></div>
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
|
|
||||||
<section class="view" data-view="groups">
|
<section class="view" data-view="groups">
|
||||||
<div class="section-heading"><div><span class="eyebrow" data-i18n="groups.eyebrow">Group control</span><h1 data-i18n="nav.groups">Groups</h1></div><button class="secondary" data-open="groupDialog" data-i18n="groups.new">New group</button></div>
|
<div class="section-heading"><div><span class="eyebrow" data-i18n="groups.eyebrow">Group control</span><h1 data-i18n="nav.groups">Groups</h1></div><button class="secondary" data-open="groupDialog" data-i18n="groups.new">New group</button></div>
|
||||||
<p class="lead" data-i18n="groups.description">Combine zones such as upstairs or downstairs and control only that set. A group can run Sleep + Cooling while another uses Comfort + Heating.</p>
|
<p class="lead" data-i18n="groups.description">Combine thermostat zones and control only that set. Turning a group on activates its thermostats; turning it off powers its units down and releases group ownership.</p>
|
||||||
<div class="list-grid" id="groupList"></div>
|
<div class="list-grid" id="groupList"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -515,7 +515,7 @@
|
|||||||
<div class="dialog-head"><h2 data-i18n="groups.group">Group</h2><button type="button" data-close>×</button></div>
|
<div class="dialog-head"><h2 data-i18n="groups.group">Group</h2><button type="button" data-close>×</button></div>
|
||||||
<label><span data-i18n="common.name">Name</span><input name="name" required data-i18n-placeholder="groups.namePlaceholder" placeholder="Upstairs"></label>
|
<label><span data-i18n="common.name">Name</span><input name="name" required data-i18n-placeholder="groups.namePlaceholder" placeholder="Upstairs"></label>
|
||||||
<fieldset><legend data-i18n="groups.members">Group members</legend><div class="group-zone-choices" id="groupZoneChoices"></div></fieldset>
|
<fieldset><legend data-i18n="groups.members">Group members</legend><div class="group-zone-choices" id="groupZoneChoices"></div></fieldset>
|
||||||
<p class="field-note" data-i18n="groups.membersHint">Groups are built from thermostat zones so Comfort/Sleep profiles and schedules keep working.</p>
|
<p class="field-note" data-i18n="groups.membersHint">Groups are built from thermostat zones. ON takes control of members; OFF powers their units down without leaving a group ownership lock.</p>
|
||||||
<div class="form-actions"><button type="button" class="secondary" data-close data-i18n="actions.cancel">Cancel</button><button class="primary" type="submit" data-i18n="actions.save">Save</button></div>
|
<div class="form-actions"><button type="button" class="secondary" data-close data-i18n="actions.cancel">Cancel</button><button class="primary" type="submit" data-i18n="actions.save">Save</button></div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
+11
-22
@@ -97,13 +97,6 @@ function groupControlForZone(zone) {
|
|||||||
return app.groups.find(group => group.id===groupId && group.power_enabled !== false && (group.zone_ids || []).includes(zone.id)) || null;
|
return app.groups.find(group => group.id===groupId && group.power_enabled !== false && (group.zone_ids || []).includes(zone.id)) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockingGroupsForZone(zone) {
|
|
||||||
if (app.settings?.house_power_enabled === false || zone?.local_thermostat_power === true) return [];
|
|
||||||
const zoneId=zone?.id || zone?.zone_id;
|
|
||||||
if(!zoneId) return [];
|
|
||||||
return app.groups.filter(group => group.power_enabled === false && (group.zone_ids || []).includes(zoneId));
|
|
||||||
}
|
|
||||||
|
|
||||||
function zoneLockoutActive(zone) {
|
function zoneLockoutActive(zone) {
|
||||||
const until=zone?.lockout_until ? new Date(zone.lockout_until).getTime() : NaN;
|
const until=zone?.lockout_until ? new Date(zone.lockout_until).getTime() : NaN;
|
||||||
return Number.isFinite(until) && until>Date.now();
|
return Number.isFinite(until) && until>Date.now();
|
||||||
@@ -137,8 +130,6 @@ function zoneRuntimeStatusLabel(zone, effectiveMode, device = null) {
|
|||||||
if (zone.device_manual_override === true) return tr('zones.manualDeviceControl');
|
if (zone.device_manual_override === true) return tr('zones.manualDeviceControl');
|
||||||
if (!zone.enabled) return tr('common.disabled');
|
if (!zone.enabled) return tr('common.disabled');
|
||||||
if (zone.local_thermostat_power === false) return tr('zones.localThermostatOff');
|
if (zone.local_thermostat_power === false) return tr('zones.localThermostatOff');
|
||||||
const blockers=blockingGroupsForZone(zone);
|
|
||||||
if (blockers.length) return tr('zones.disabledByGroup', {groups:blockers.map(group=>group.name).join(', ')});
|
|
||||||
if ((effectiveMode || 'off') === 'off') return tr('zones.waiting');
|
if ((effectiveMode || 'off') === 'off') return tr('zones.waiting');
|
||||||
if (device?.enabled === false) return tr('zones.waitingDeviceDisabled');
|
if (device?.enabled === false) return tr('zones.waitingDeviceDisabled');
|
||||||
if (device && (!device.online || Number(device.communication_failures || 0) > 0)) return tr('zones.waitingOffline');
|
if (device && (!device.online || Number(device.communication_failures || 0) > 0)) return tr('zones.waitingOffline');
|
||||||
@@ -166,15 +157,12 @@ function zoneCard(zone, detailed = true) {
|
|||||||
const hasManualOverride = zone.manual_preset != null || zone.manual_setpoint != null;
|
const hasManualOverride = zone.manual_preset != null || zone.manual_setpoint != null;
|
||||||
const deviceManualOverride = zone.device_manual_override === true;
|
const deviceManualOverride = zone.device_manual_override === true;
|
||||||
const controlGroup = groupControlForZone(zone);
|
const controlGroup = groupControlForZone(zone);
|
||||||
const blockingGroups = blockingGroupsForZone(zone);
|
|
||||||
const deviceUnavailable = !!device && (device.enabled === false || !device.online || Number(device.communication_failures || 0) > 0);
|
const deviceUnavailable = !!device && (device.enabled === false || !device.online || Number(device.communication_failures || 0) > 0);
|
||||||
const lockoutWaiting = zone.demand && zoneLockoutActive(zone);
|
const lockoutWaiting = zone.demand && zoneLockoutActive(zone);
|
||||||
const startWaiting = zone.demand && !!device && !deviceUnavailable && !device.power;
|
const startWaiting = zone.demand && !!device && !deviceUnavailable && !device.power;
|
||||||
const modeWaiting = zone.demand && !!device?.power && !deviceUnavailable && ['heat','cool'].includes(effectiveMode) && device.mode !== effectiveMode;
|
const modeWaiting = zone.demand && !!device?.power && !deviceUnavailable && ['heat','cool'].includes(effectiveMode) && device.mode !== effectiveMode;
|
||||||
const groupBlockedWaiting = blockingGroups.length > 0;
|
const visualGroup = controlGroup;
|
||||||
const singleBlockingGroup = blockingGroups.length === 1 ? blockingGroups[0] : null;
|
const waitingDemand = lockoutWaiting || startWaiting || modeWaiting || (zone.demand && deviceUnavailable);
|
||||||
const visualGroup = controlGroup || singleBlockingGroup;
|
|
||||||
const waitingDemand = lockoutWaiting || startWaiting || modeWaiting || groupBlockedWaiting || (zone.demand && deviceUnavailable);
|
|
||||||
const workingDemand = zone.demand && !!device?.power && !deviceUnavailable && !waitingDemand;
|
const workingDemand = zone.demand && !!device?.power && !deviceUnavailable && !waitingDemand;
|
||||||
const localThermostatPower = zone.local_thermostat_power;
|
const localThermostatPower = zone.local_thermostat_power;
|
||||||
const localThermostatResumeAt = zone.local_thermostat_resume_at || null;
|
const localThermostatResumeAt = zone.local_thermostat_resume_at || null;
|
||||||
@@ -210,7 +198,7 @@ function zoneCard(zone, detailed = true) {
|
|||||||
const groupNames = (app.groups || []).filter(group => (group.zone_ids || []).includes(zone.id)).map(group => group.name);
|
const groupNames = (app.groups || []).filter(group => (group.zone_ids || []).includes(zone.id)).map(group => group.name);
|
||||||
const groupText = groupNames.length ? groupNames.join(' · ') : tr('zones.noGroup');
|
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');
|
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' : ''} ${singleBlockingGroup ? 'group-blocked' : ''}" data-zone-config="${esc(zone.id)}"${groupControlStyle(visualGroup)}>
|
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>${zone.enabled ? '✓' : '○'}</span>${esc(state)}</button></div>
|
||||||
<div class="zone-config-status">
|
<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-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>
|
||||||
@@ -229,14 +217,14 @@ function zoneCard(zone, detailed = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const runtimeStatus = zoneRuntimeStatusLabel(zone, effectiveMode, device);
|
const runtimeStatus = zoneRuntimeStatusLabel(zone, effectiveMode, device);
|
||||||
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' : ''} ${singleBlockingGroup ? 'group-blocked' : ''}" data-zone-card="${esc(zone.id)}"${groupControlStyle(visualGroup)}>
|
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>
|
<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>
|
||||||
<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">−</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="preset-row quick-control-row quick-control-row-4">
|
<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('')}
|
${['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>
|
</div>
|
||||||
<div class="mode-row zone-mode-row quick-control-row quick-control-row-3"><button class="${mode==='house'&&globalModeAvailable?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="house"${globalModeDisabled}>${esc(tr('zones.followHouseShort'))}</button><button class="${mode==='heat'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="heat">${esc(modeLabel('heat'))}</button><button class="${mode==='cool'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="cool">${esc(modeLabel('cool'))}</button></div>
|
<div class="mode-row zone-mode-row quick-control-row quick-control-row-3"><button class="${mode==='house'&&globalModeAvailable?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="house"${globalModeDisabled}>${esc(tr('zones.followHouseShort'))}</button><button class="${mode==='heat'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="heat">${esc(modeLabel('heat'))}</button><button class="${mode==='cool'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="cool">${esc(modeLabel('cool'))}</button></div>
|
||||||
<div class="zone-state-line"><span class="zone-runtime-status ${groupBlockedWaiting ? 'group-blocked-status' : ''}" title="${esc(runtimeStatus)}">${esc(runtimeStatus)}${controlGroup ? ` <b class="group-control-tag">${esc(controlGroup.name)}</b>` : ''}</span><span>${esc(override)}</span></div>
|
<div class="zone-state-line"><span class="zone-runtime-status" title="${esc(runtimeStatus)}">${esc(runtimeStatus)}${controlGroup ? ` <b class="group-control-tag">${esc(controlGroup.name)}</b>` : ''}</span><span>${esc(override)}</span></div>
|
||||||
<div class="zone-state-line control-owner-line"><span><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}</span><span>${esc(zoneControlOwnerMeta(zone))}</span></div>
|
<div class="zone-state-line control-owner-line"><span><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}</span><span>${esc(zoneControlOwnerMeta(zone))}</span></div>
|
||||||
${manualTakeover}
|
${manualTakeover}
|
||||||
</article>`;
|
</article>`;
|
||||||
@@ -288,18 +276,19 @@ function groupCard(group, detailed = false) {
|
|||||||
const customDraft = app.groupCustomDrafts?.[group.id];
|
const customDraft = app.groupCustomDrafts?.[group.id];
|
||||||
const draftTarget = Number(customDraft?.value);
|
const draftTarget = Number(customDraft?.value);
|
||||||
const customTarget = Number.isFinite(draftTarget) ? draftTarget : currentCustomTarget;
|
const customTarget = Number.isFinite(draftTarget) ? draftTarget : currentCustomTarget;
|
||||||
const customEditorVisible = state.preset === 'custom' || customDraft?.open === true;
|
const customEditorVisible = powerEnabled && (state.preset === 'custom' || customDraft?.open === true);
|
||||||
const customDraftPending = customDraft?.open === true && state.preset !== 'custom';
|
const customDraftPending = powerEnabled && customDraft?.open === true && state.preset !== 'custom';
|
||||||
const groupIsControlSource = powerEnabled && state.zones.some(zone => String(zone.control_source || '') === `group:${group.id}` && !zone.device_manual_override && zone.local_thermostat_power == null);
|
const groupIsControlSource = powerEnabled && state.zones.some(zone => String(zone.control_source || '') === `group:${group.id}` && !zone.device_manual_override && zone.local_thermostat_power == null);
|
||||||
|
const climateControlsDisabled = powerEnabled ? '' : ` disabled aria-disabled="true" title="${esc(tr('groups.enableControlFirst'))}"`;
|
||||||
const groupStatusBadge = groupIsControlSource
|
const groupStatusBadge = groupIsControlSource
|
||||||
? `<span class="group-control-badge">${esc(tr('groups.activeControl'))}</span>`
|
? `<span class="group-control-badge">${esc(tr('groups.activeControl'))}</span>`
|
||||||
: (!powerEnabled ? `<span class="group-control-badge group-off-badge">${esc(tr('groups.gateOff'))}</span>` : '');
|
: (!powerEnabled ? `<span class="group-control-badge group-off-badge">${esc(tr('groups.gateOff'))}</span>` : '');
|
||||||
return `<article class="list-card group-card group-linked ${powerEnabled ? '' : 'group-off'} ${groupIsControlSource ? 'group-controlled' : ''}"${groupControlStyle(group)}>
|
return `<article class="list-card group-card group-linked ${powerEnabled ? '' : 'group-off'} ${groupIsControlSource ? 'group-controlled' : ''}"${groupControlStyle(group)}>
|
||||||
<div class="list-card-head"><div><span class="eyebrow">${esc(tr('groups.group'))}</span><h3>${esc(group.name)}</h3><p>${esc(memberText)}</p></div>${groupStatusBadge}</div>
|
<div class="list-card-head"><div><span class="eyebrow">${esc(tr('groups.group'))}</span><h3>${esc(group.name)}</h3><p>${esc(memberText)}</p></div>${groupStatusBadge}</div>
|
||||||
${detailed ? `<div class="group-state-summary"><div><small>${esc(tr('groups.mode'))}</small><strong>${esc(modeLabelForGroup(state.mode))}</strong></div><div><small>${esc(tr('groups.profile'))}</small><strong>${esc(presetLabelForGroup(state.preset))}</strong></div><div><small>${esc(tr('groups.members'))}</small><strong>${state.zones.length}</strong></div></div>` : ''}
|
${detailed ? `<div class="group-state-summary"><div><small>${esc(tr('groups.mode'))}</small><strong>${esc(modeLabelForGroup(state.mode))}</strong></div><div><small>${esc(tr('groups.profile'))}</small><strong>${esc(presetLabelForGroup(state.preset))}</strong></div><div><small>${esc(tr('groups.members'))}</small><strong>${state.zones.length}</strong></div></div>` : ''}
|
||||||
<div class="group-control-block"><small>${esc(tr('common.power'))}</small><div class="group-button-row"><button class="${powerEnabled ? 'active' : ''}" data-action="group-power" data-id="${esc(group.id)}" data-value="true">${esc(tr('common.on'))}</button><button class="${!powerEnabled ? 'active' : ''}" data-action="group-power" data-id="${esc(group.id)}" data-value="false">${esc(tr('common.off'))}</button></div></div>
|
<div class="group-control-block"><small>${esc(tr('groups.controlToggle'))}</small><div class="group-button-row"><button class="${powerEnabled ? 'active' : ''}" data-action="group-power" data-id="${esc(group.id)}" data-value="true">${esc(tr('common.on'))}</button><button class="${!powerEnabled ? 'active' : ''}" data-action="group-power" data-id="${esc(group.id)}" data-value="false">${esc(tr('common.off'))}</button></div></div>
|
||||||
<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}">${esc(mode === 'house' ? tr('groups.followHouse') : modeLabel(mode))}</button>`).join('')}</div></div>
|
<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}">${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'))}">${esc(tr('preset.custom'))}</button></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>
|
<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>
|
||||||
${masterOff ? `<p class="field-note warning-note">${esc(tr('groups.masterOff'))}</p>` : ''}
|
${masterOff ? `<p class="field-note warning-note">${esc(tr('groups.masterOff'))}</p>` : ''}
|
||||||
${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><button class="danger" data-action="delete-group" data-id="${esc(group.id)}">${esc(tr('actions.delete'))}</button></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><button class="danger" data-action="delete-group" data-id="${esc(group.id)}">${esc(tr('actions.delete'))}</button></div></div>` : ''}
|
||||||
|
|||||||
+1
-1
@@ -122,7 +122,7 @@ document.addEventListener('click', async event => {
|
|||||||
catch(error){ toast(error.message,true); }
|
catch(error){ toast(error.message,true); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === 'group-power') return sendGroupControl(button.dataset.id,{power:button.dataset.value==='true'});
|
if (action === 'group-power') { const power=button.dataset.value==='true'; if(!power) delete app.groupCustomDrafts[button.dataset.id]; return sendGroupControl(button.dataset.id,{power}); }
|
||||||
if (action === 'group-mode') return sendGroupControl(button.dataset.id,{mode:button.dataset.value});
|
if (action === 'group-mode') return sendGroupControl(button.dataset.id,{mode:button.dataset.value});
|
||||||
if (action === 'group-preset') { delete app.groupCustomDrafts[button.dataset.id]; return sendGroupControl(button.dataset.id,{preset:button.dataset.value}); }
|
if (action === 'group-preset') { delete app.groupCustomDrafts[button.dataset.id]; return sendGroupControl(button.dataset.id,{preset:button.dataset.value}); }
|
||||||
if (action === 'group-custom-open') {
|
if (action === 'group-custom-open') {
|
||||||
|
|||||||
+1
-13
@@ -1386,7 +1386,7 @@ textarea[aria-invalid="true"] {
|
|||||||
.group-custom-temperature-row button { grid-column:auto; }
|
.group-custom-temperature-row button { grid-column:auto; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* v0.8.22: make group gates visually unambiguous. */
|
/* v0.8.24: group OFF powers members down but releases group ownership; OFF groups do not membership-block thermostats. */
|
||||||
.list-card.group-card.group-linked {
|
.list-card.group-card.group-linked {
|
||||||
border-color:color-mix(in srgb,var(--group-control-color) 42%,var(--line));
|
border-color:color-mix(in srgb,var(--group-control-color) 42%,var(--line));
|
||||||
}
|
}
|
||||||
@@ -1399,18 +1399,6 @@ textarea[aria-invalid="true"] {
|
|||||||
border:1px dashed color-mix(in srgb,var(--group-control-color) 55%,var(--line));
|
border:1px dashed color-mix(in srgb,var(--group-control-color) 55%,var(--line));
|
||||||
background:color-mix(in srgb,var(--group-control-color) 7%,var(--surface));
|
background:color-mix(in srgb,var(--group-control-color) 7%,var(--surface));
|
||||||
}
|
}
|
||||||
.zone-thermostat.group-blocked,.zone-config-card.group-blocked {
|
|
||||||
border-color:var(--group-control-color);
|
|
||||||
border-style:dashed;
|
|
||||||
box-shadow:0 0 0 1px color-mix(in srgb,var(--group-control-color) 25%,transparent);
|
|
||||||
}
|
|
||||||
.quick-thermostat-control .zone-state-line .group-blocked-status {
|
|
||||||
white-space:normal;
|
|
||||||
overflow:visible;
|
|
||||||
text-overflow:clip;
|
|
||||||
color:color-mix(in srgb,var(--group-control-color) 78%,var(--text));
|
|
||||||
font-weight:700;
|
|
||||||
}
|
|
||||||
.list-card.group-card.group-linked.group-controlled {
|
.list-card.group-card.group-linked.group-controlled {
|
||||||
border-style:solid;
|
border-style:solid;
|
||||||
border-color:var(--group-control-color);
|
border-color:var(--group-control-color);
|
||||||
|
|||||||
Reference in New Issue
Block a user