v0.9.4-fix
This commit is contained in:
+13
-13
@@ -1,11 +1,11 @@
|
|||||||
b38e05f8a00969270280b0062ee129fccee39ae9c11cb614a6ad7b2b7ae2dfc7 ./.env.example
|
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||||
37ba4d5222b221d9a3d560246d968092fa4402288410eaba7e6bab87ebcc5aea ./Cargo.lock
|
37ba4d5222b221d9a3d560246d968092fa4402288410eaba7e6bab87ebcc5aea ./Cargo.lock
|
||||||
a0976abed3b84a5ba0c726410c87d31017d42098fb7e053af986869ea0c9db74 ./Cargo.toml
|
a0976abed3b84a5ba0c726410c87d31017d42098fb7e053af986869ea0c9db74 ./Cargo.toml
|
||||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||||
4011536d5181ce7d8132c3b2ad50699a3ffa057f33b361aaa96fe71b6ae3efe3 ./README.md
|
a6391cc7fc014d06205c3fac708e0316e7239eaca47e0a2cb05a18275c58f192 ./README.md
|
||||||
ec0f495ee6a618bdec10b260ee7838f7a872d8c5b0aa91c546226c1ad818f601 ./build.rs
|
ec0f495ee6a618bdec10b260ee7838f7a872d8c5b0aa91c546226c1ad818f601 ./build.rs
|
||||||
e183861962799504fe7dfc7f43ba5d75dc7f7f68c898689dd2c178f566af73e2 ./docs/API.md
|
e7f085a9b4bd75dd62609e3c9f62bdde532f5cb0675fda2cf5109e57fafef2d2 ./docs/API.md
|
||||||
a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./home-assistant/README.md
|
a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./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
|
||||||
@@ -25,8 +25,8 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./home-assista
|
|||||||
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
|
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
|
||||||
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
|
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
|
||||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||||
669da4cb511c9d7f72017edf3f16718eb3b32fc832d8431a597a330f4bc8f5c0 ./lang/en.json
|
fdae654fbb89505a65cc3e05c76907a3ded31dbcae019d4ccf1b1cc146990d71 ./lang/en.json
|
||||||
9bf8bccb7eaf99aba65f4b4b8776f2834f335fc0b007999d7e5c8d9d287067c8 ./lang/pl.json
|
093022ed2886317c092c19a556ab8cfed86d0322d88f620dfbae515ac64e5ac0 ./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,7 +48,7 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda
|
|||||||
417940b467c6f00bbf01cc47eb4479880dee9b2f284a80e3662a6d433dc11b92 ./src/api/events.rs
|
417940b467c6f00bbf01cc47eb4479880dee9b2f284a80e3662a6d433dc11b92 ./src/api/events.rs
|
||||||
d096000e94997cf9aaf9d3cc567214a78b8b2da7da57a76e2b3519247af6e592 ./src/api/groups.rs
|
d096000e94997cf9aaf9d3cc567214a78b8b2da7da57a76e2b3519247af6e592 ./src/api/groups.rs
|
||||||
a13d4e217fe3ddaa73873ba6e0d1bc93750cf61764d867ff7372274ed21d9599 ./src/api/history.rs
|
a13d4e217fe3ddaa73873ba6e0d1bc93750cf61764d867ff7372274ed21d9599 ./src/api/history.rs
|
||||||
b2a730970a767c67727d5bc8f7090edb26d8332a47bc72a28b5a1195a4dbafe7 ./src/api/house.rs
|
7b5de0bb8b6559a59a98ba56c853b712ffcc7211900f02a4a9a089027878dde1 ./src/api/house.rs
|
||||||
0d7d598f7ed24315ed0547a60a72a06f20ebcf926489e1c454986f42d22cc305 ./src/api/integrations.rs
|
0d7d598f7ed24315ed0547a60a72a06f20ebcf926489e1c454986f42d22cc305 ./src/api/integrations.rs
|
||||||
bb2a746ecdcc2da5fe54e18b455c7bd19453486dd5c008e71951a2c81d0e7d64 ./src/api/middleware.rs
|
bb2a746ecdcc2da5fe54e18b455c7bd19453486dd5c008e71951a2c81d0e7d64 ./src/api/middleware.rs
|
||||||
2b3feefbafc1f813c00dfe20db147aa1f5067ec5b8455f041ac3793802911be3 ./src/api/public_settings.rs
|
2b3feefbafc1f813c00dfe20db147aa1f5067ec5b8455f041ac3793802911be3 ./src/api/public_settings.rs
|
||||||
@@ -71,21 +71,21 @@ a384ea4042d1d11692b5f8693df1f54850dcc4e80726780b44b2b096bf667b78 ./src/db/zone_
|
|||||||
952ea5906d39ef917ad71edc345f76b29c07c63079644fd5699b12aa254cb35f ./src/engine.rs
|
952ea5906d39ef917ad71edc345f76b29c07c63079644fd5699b12aa254cb35f ./src/engine.rs
|
||||||
384f7027ca48bd4c4048320bc6d4cacd631bee40e8630ea22e0d6e5f22ddc3a6 ./src/engine/automations.rs
|
384f7027ca48bd4c4048320bc6d4cacd631bee40e8630ea22e0d6e5f22ddc3a6 ./src/engine/automations.rs
|
||||||
77931af524e9402fc826050b5fcec422d01b02b625c8ce7f2004f1f6520c9b4d ./src/engine/commands.rs
|
77931af524e9402fc826050b5fcec422d01b02b625c8ce7f2004f1f6520c9b4d ./src/engine/commands.rs
|
||||||
ced02b351ba86c43bd3683480a33ff7bd95c8a8109216c85979bc1cb14db5f41 ./src/engine/control_plan.rs
|
e16c1a65057f6fbf11bb6f27493ed975f5c89309b31dec0fa3248b9237494ed3 ./src/engine/control_plan.rs
|
||||||
38f42c86c31048780ca414f2c328f363ef803a0edbc1e1ed85503909a10b69ae ./src/engine/deadlines.rs
|
38f42c86c31048780ca414f2c328f363ef803a0edbc1e1ed85503909a10b69ae ./src/engine/deadlines.rs
|
||||||
511caa52f9d1a6c13c15731e6a394ca2790ba642845367da1f0b767c12b5be98 ./src/engine/groups.rs
|
511caa52f9d1a6c13c15731e6a394ca2790ba642845367da1f0b767c12b5be98 ./src/engine/groups.rs
|
||||||
18a7669318949e533f206d25cf7b63771d7f0f2c0b375a72ff78a2d921baaa61 ./src/engine/history.rs
|
18a7669318949e533f206d25cf7b63771d7f0f2c0b375a72ff78a2d921baaa61 ./src/engine/history.rs
|
||||||
f7f2570c5b4a0f655e7c68e44d3dcca478b546e31586e98227b0e8c21e73e8c4 ./src/engine/local_thermostat.rs
|
4972c199274647bff47122c367e51bae8dab24d7ec4d652288e227a5a64c2310 ./src/engine/local_thermostat.rs
|
||||||
ea61ee04f7cda0c4835a87b1e28f1ff8134a8b977741ef6e8ccb8f284d1c9470 ./src/engine/ownership.rs
|
ecab67072e756f5a371864282eb470a17de53016593e2e96540674066262c865 ./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
|
||||||
2e000ede494bafb5309af3bbcdf1f10024bb01012ab3f9555ff0c8f1898bc9d7 ./src/engine/targets.rs
|
bcfebf99a03ee676fd9c1c5d395adf271898fcb0df0d39bce3a3b7067456522a ./src/engine/targets.rs
|
||||||
a9188b588b2617ce1547a45d46141f7e5f1bb0027a48675dd86b274f807d0875 ./src/engine/temperature.rs
|
a9188b588b2617ce1547a45d46141f7e5f1bb0027a48675dd86b274f807d0875 ./src/engine/temperature.rs
|
||||||
c2fe371f9245ce299015f0b4e43f6625a207016abf73d2419acb6bd7437f045a ./src/engine/temporary_thermostat.rs
|
c2fe371f9245ce299015f0b4e43f6625a207016abf73d2419acb6bd7437f045a ./src/engine/temporary_thermostat.rs
|
||||||
9e86ea344e0bf6a2232905a98e5addce5da6f845cfed5b04a5d2d20787490b15 ./src/engine/tests.rs
|
5631fd8420c4137f4586d98939d24a83701400ac07a3f2fe9da8b8d486df3e64 ./src/engine/tests.rs
|
||||||
ebf8b9ec5c5a84850d8cad4c4faea090e52625b5a0be56625dc84060ecb35186 ./src/engine/zone_actions.rs
|
ebf8b9ec5c5a84850d8cad4c4faea090e52625b5a0be56625dc84060ecb35186 ./src/engine/zone_actions.rs
|
||||||
51e0d547709da755622c88bea5e69912b519040d5111ba5c2d74d071b73b52a9 ./src/engine/zone_control.rs
|
d2c2f3678bd13ec2d92404993173a691803da215dbdb1edafcd96e1963c7004e ./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
|
||||||
@@ -133,7 +133,7 @@ b3d5cb473bf9771de78e14c7de730cb86c73f8cdbb6344da67ba7d6abac879ea ./web/js/boots
|
|||||||
64a43e2142a69ebddf4160c0d0d3b573fa379c650431090fd15c9ba3b62bc4e7 ./web/js/charts.js
|
64a43e2142a69ebddf4160c0d0d3b573fa379c650431090fd15c9ba3b62bc4e7 ./web/js/charts.js
|
||||||
cf8d97770985b8cf920382725fa33d5f61497a639544bfc528af72b276de0b6a ./web/js/core.js
|
cf8d97770985b8cf920382725fa33d5f61497a639544bfc528af72b276de0b6a ./web/js/core.js
|
||||||
9767d5f2bd2844245245c0bd86dddd528975c1d1dd1d05b1449feda7a95214c4 ./web/js/dashboard.js
|
9767d5f2bd2844245245c0bd86dddd528975c1d1dd1d05b1449feda7a95214c4 ./web/js/dashboard.js
|
||||||
a41a5d768452872e9e88abeab8d97c0869a3285448165ec306f59341265b4b59 ./web/js/entities.js
|
c378f6234dbd4310a25ef6d65c12521726cd3fcce1c7069cd878b1d5af491011 ./web/js/entities.js
|
||||||
69a052771dfafbd4b778cbc9d1b7a7695427621ed66047282088a3a07e6e4c2e ./web/js/events.js
|
69a052771dfafbd4b778cbc9d1b7a7695427621ed66047282088a3a07e6e4c2e ./web/js/events.js
|
||||||
2fe6c804f86b0613a50a56c54f7a0cbee77273759edf3e6fec36ac77d35edbd7 ./web/js/forms.js
|
2fe6c804f86b0613a50a56c54f7a0cbee77273759edf3e6fec36ac77d35edbd7 ./web/js/forms.js
|
||||||
be07a341ba4b1f514a5e13553494194b22aa87108cb04de40635a0158921780b ./web/js/history.js
|
be07a341ba4b1f514a5e13553494194b22aa87108cb04de40635a0158921780b ./web/js/history.js
|
||||||
|
|||||||
@@ -177,7 +177,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, house rules, 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.
|
A **Zone** is the thermostat owner for a device. It decides demand from room temperature, setpoint, hysteresis, schedules, house rules, 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. After handing an OFF thermostat back to automation, an implicit Comfort target alone does not start it: the zone waits for an active schedule, manual preset/setpoint, Temporary Quick Thermostat, active group/local control or another explicit automatic power intent.
|
||||||
|
|
||||||
### Compressor protection
|
### Compressor protection
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ This distinction is important: direct device control intentionally behaves diffe
|
|||||||
|
|
||||||
The zone state exposes who currently owns control (`control_owner`, `control_source`, timestamps and reason). Physical remote/direct device actions can create a manual-device takeover so normal schedules do not immediately overwrite the user. Explicit zone thermostat control hands ownership back to the thermostat engine when appropriate.
|
The zone state exposes who currently owns control (`control_owner`, `control_source`, timestamps and reason). Physical remote/direct device actions can create a manual-device takeover so normal schedules do not immediately overwrite the user. Explicit zone thermostat control hands ownership back to the thermostat engine when appropriate.
|
||||||
|
|
||||||
Global **Turn all off** is an immediate bulk thermostat action: it powers every enabled unit off and leaves every thermostat locally OFF with no hand-back timer, but it does not create a persistent global gate. A later local thermostat or manual group action can therefore re-enable only its own scope. **Turn all on** releases those local OFF markers and sends ON to all enabled units; compressor protection may queue protected starts. Explicit local/group/manual control has priority over house rules.
|
Global **Turn all off** is an immediate bulk thermostat action: it powers every enabled unit off and leaves every thermostat locally OFF with no hand-back timer, but it does not create a persistent blocking gate. A later **Resume automation** releases only that zone to automatic arbitration; when no schedule/override/temporary or other explicit intent is active, it stays OFF. **Turn all on** is different: it is an explicit whole-house ON intent, releases local OFF markers and sends ON to enabled units; compressor protection may queue protected starts. Explicit local/group/manual control still has priority.
|
||||||
|
|
||||||
### Profiles and schedules
|
### Profiles and schedules
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -519,7 +519,7 @@ Semantics:
|
|||||||
- `enabled`: zone automation enable state.
|
- `enabled`: zone automation enable state.
|
||||||
- `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: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 automation. If there is no active schedule, manual preset/setpoint, Temporary Quick Thermostat or other explicit thermostat intent, the zone remains physically OFF instead of falling back to implicit Comfort.
|
||||||
- `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.
|
||||||
- `clear_override:true`: clear ordinary quick preset/setpoint override.
|
- `clear_override:true`: clear ordinary quick preset/setpoint override.
|
||||||
- `temporary_quick_thermostat`: start/replace a persisted temporary session.
|
- `temporary_quick_thermostat`: start/replace a persisted temporary session.
|
||||||
@@ -700,7 +700,7 @@ Returns public runtime settings.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This endpoint is a **bulk thermostat power action without a persistent global gate**. `false` first stores every thermostat as an indefinite local OFF and then sends immediate OFF to every technically enabled unit, so the next normal demand cycle cannot turn the house back on. A later explicit local thermostat or manual group action may re-enable only its own scope. `true` releases local OFF markers for all thermostats and sends ON to all technically enabled units. Group enablement, profiles, schedules and house mode are preserved. Pending compressor-protection tasks from before the action are cleared. When compressor protection is enabled, protected global starts are queued until their safe deadline; OFF is never delayed.
|
This endpoint is a **bulk thermostat power action without a persistent blocking global gate**. `false` first stores every thermostat as an indefinite local OFF and then sends immediate OFF to every technically enabled unit, so the next normal demand cycle cannot turn the house back on. A later per-zone **Resume automation** clears that local OFF but does not invent a Comfort demand: without an active schedule/override/temporary or another explicit intent the zone stays OFF. `true` is itself an explicit whole-house ON intent, releases local OFF markers for all thermostats and sends ON to all technically enabled units. Group enablement, profiles, schedules and house mode are preserved. Pending compressor-protection tasks from before the action are cleared. When compressor protection is enabled, protected global starts are queued until their safe deadline; OFF is never delayed.
|
||||||
|
|
||||||
Response includes:
|
Response includes:
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -250,7 +250,7 @@
|
|||||||
"mode.off": "Off",
|
"mode.off": "Off",
|
||||||
"house.seasonMode": "Home climate mode",
|
"house.seasonMode": "Home climate mode",
|
||||||
"house.smartThermostat": "Smart thermostat",
|
"house.smartThermostat": "Smart thermostat",
|
||||||
"house.setpointStrategy": "Cooling or heating controls zones automatically. “Do not control” leaves devices and manually activated zones untouched by the house thermostat.",
|
"house.setpointStrategy": "Heating or cooling selects the automation direction. After resume, a zone with no active schedule, manual target, temporary session or other explicit control stays OFF. “Do not control” leaves devices untouched.",
|
||||||
"house.outdoor": "Outdoor",
|
"house.outdoor": "Outdoor",
|
||||||
"house.modeUpdated": "Home climate mode updated",
|
"house.modeUpdated": "Home climate mode updated",
|
||||||
"zones.modePolicy": "Mode policy",
|
"zones.modePolicy": "Mode policy",
|
||||||
@@ -607,15 +607,15 @@
|
|||||||
"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 whether group-level control is enabled. 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. After automation is resumed it stays OFF unless a schedule, manual target, temporary session or another explicit control source is active.",
|
||||||
"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",
|
||||||
"zones.turnThermostatOn": "Turn thermostat on",
|
"zones.turnThermostatOn": "Turn thermostat on",
|
||||||
"zones.turnThermostatOff": "Turn thermostat off",
|
"zones.turnThermostatOff": "Turn thermostat off",
|
||||||
"zones.waiting": "Waiting",
|
"zones.waiting": "Waiting",
|
||||||
"zones.noMeasurement": "No measurement",
|
"zones.noMeasurement": "No measurement",
|
||||||
"zones.localThermostatOffDescriptionTimed": "This zone is locally off. Automation takes control again in {time}.",
|
"zones.localThermostatOffDescriptionTimed": "This zone is locally off. In {time}, automation will resume and start only if an explicit control source is active.",
|
||||||
"zones.localThermostatResuming": "Timer elapsed — automation is taking control…",
|
"zones.localThermostatResuming": "Timer elapsed — automation is checking for an active control source…",
|
||||||
"zones.resumeNow": "Resume now",
|
"zones.resumeNow": "Resume now",
|
||||||
"zones.controlOwner": "Control",
|
"zones.controlOwner": "Control",
|
||||||
"zones.ownerAutomation": "Automation",
|
"zones.ownerAutomation": "Automation",
|
||||||
|
|||||||
+4
-4
@@ -250,7 +250,7 @@
|
|||||||
"mode.off": "Wyłączone",
|
"mode.off": "Wyłączone",
|
||||||
"house.seasonMode": "Tryb klimatu domu",
|
"house.seasonMode": "Tryb klimatu domu",
|
||||||
"house.smartThermostat": "Inteligentny termostat",
|
"house.smartThermostat": "Inteligentny termostat",
|
||||||
"house.setpointStrategy": "Chłodzenie lub grzanie steruje strefami automatycznie. „Nie steruj” pozostawia urządzenia i ręcznie uruchomione strefy bez ingerencji termostatu domu.",
|
"house.setpointStrategy": "Grzanie lub chłodzenie określa kierunek pracy automatyki. Po wznowieniu strefa bez aktywnego harmonogramu, ręcznego celu, trybu czasowego lub innego jawnego sterowania pozostaje OFF. „Nie steruj” nie ingeruje w urządzenia.",
|
||||||
"house.outdoor": "Na zewnątrz",
|
"house.outdoor": "Na zewnątrz",
|
||||||
"house.modeUpdated": "Zmieniono tryb klimatu domu",
|
"house.modeUpdated": "Zmieniono tryb klimatu domu",
|
||||||
"zones.modePolicy": "Polityka trybu",
|
"zones.modePolicy": "Polityka trybu",
|
||||||
@@ -607,15 +607,15 @@
|
|||||||
"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 tego, czy sterowanie grupowe jest włączone. 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. Po wznowieniu automatyki pozostanie OFF, jeśli nie ma aktywnego harmonogramu, ręcznego celu, trybu czasowego ani innego jawnego sterowania.",
|
||||||
"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",
|
||||||
"zones.turnThermostatOn": "Włącz termostat",
|
"zones.turnThermostatOn": "Włącz termostat",
|
||||||
"zones.turnThermostatOff": "Wyłącz termostat",
|
"zones.turnThermostatOff": "Wyłącz termostat",
|
||||||
"zones.waiting": "Oczekiwanie",
|
"zones.waiting": "Oczekiwanie",
|
||||||
"zones.noMeasurement": "Brak pomiaru",
|
"zones.noMeasurement": "Brak pomiaru",
|
||||||
"zones.localThermostatOffDescriptionTimed": "Strefa jest lokalnie wyłączona. Automatyka przejmie sterowanie za {time}.",
|
"zones.localThermostatOffDescriptionTimed": "Strefa jest lokalnie wyłączona. Za {time} automatyka zostanie wznowiona i uruchomi strefę tylko przy aktywnym źródle sterowania.",
|
||||||
"zones.localThermostatResuming": "Czas minął — automatyka przejmuje sterowanie…",
|
"zones.localThermostatResuming": "Czas minął — automatyka sprawdza aktywne źródło sterowania…",
|
||||||
"zones.resumeNow": "Wznów teraz",
|
"zones.resumeNow": "Wznów teraz",
|
||||||
"zones.controlOwner": "Sterowanie",
|
"zones.controlOwner": "Sterowanie",
|
||||||
"zones.ownerAutomation": "Automatyka",
|
"zones.ownerAutomation": "Automatyka",
|
||||||
|
|||||||
+4
-3
@@ -115,8 +115,9 @@ async fn update_house_power(State(state): State<AppState>, Json(input): Json<Hou
|
|||||||
|
|
||||||
// Global power is a bulk thermostat action, not a persistent master gate. OFF stores every
|
// Global power is a bulk thermostat action, not a persistent master gate. OFF stores every
|
||||||
// thermostat as an indefinite local OFF so the next regulator cycle cannot immediately
|
// thermostat as an indefinite local OFF so the next regulator cycle cannot immediately
|
||||||
// resurrect demand. ON releases that OFF state. Later explicit local/group/manual actions
|
// resurrect demand. ON releases that OFF state and records an explicit automatic house-power
|
||||||
// remain independent and can re-enable only the selected scope.
|
// intent for otherwise-free zones. Later explicit local/group/manual actions remain
|
||||||
|
// independent and can take over only the selected scope.
|
||||||
{
|
{
|
||||||
let mut settings = state.settings.write().await;
|
let mut settings = state.settings.write().await;
|
||||||
if !settings.house_power_enabled {
|
if !settings.house_power_enabled {
|
||||||
@@ -138,7 +139,7 @@ async fn update_house_power(State(state): State<AppState>, Json(input): Json<Hou
|
|||||||
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 {
|
state.log("info", "house.power_all", if input.power {
|
||||||
"Whole-house ON sent; local OFF state released for all thermostats"
|
"Whole-house ON sent; local OFF state released and house thermostat intent armed"
|
||||||
} else {
|
} else {
|
||||||
"Whole-house OFF sent; all thermostats left locally OFF until explicitly re-enabled"
|
"Whole-house OFF sent; all thermostats left locally OFF until explicitly re-enabled"
|
||||||
}, json!({
|
}, json!({
|
||||||
|
|||||||
+34
-17
@@ -20,25 +20,36 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
|||||||
let configured_effective_mode_owned = effective_zone_mode(&zone, &settings.house_mode);
|
let configured_effective_mode_owned = effective_zone_mode(&zone, &settings.house_mode);
|
||||||
refresh_control_ownership(&mut zone, true);
|
refresh_control_ownership(&mut zone, true);
|
||||||
let configured_effective_mode = configured_effective_mode_owned.as_str();
|
let configured_effective_mode = configured_effective_mode_owned.as_str();
|
||||||
|
let target_mode = if configured_effective_mode == "off" { zone.mode.as_str() } else { configured_effective_mode };
|
||||||
|
let active_for_target = active_schedule_for_zone(&zone, &schedules, now);
|
||||||
|
let has_intent_source = zone_has_thermostat_intent_source(&zone, active_for_target, Utc::now());
|
||||||
|
let automation_idle = zone.enabled
|
||||||
|
&& !zone.device_manual_override
|
||||||
|
&& zone.local_thermostat_power != Some(false)
|
||||||
|
&& configured_effective_mode != "off"
|
||||||
|
&& !has_intent_source;
|
||||||
|
let automatic_active = zone.enabled
|
||||||
|
&& !zone.device_manual_override
|
||||||
|
&& zone.local_thermostat_power != Some(false)
|
||||||
|
&& configured_effective_mode != "off"
|
||||||
|
&& has_intent_source;
|
||||||
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) {
|
} else if automatic_active {
|
||||||
"off"
|
|
||||||
} else {
|
|
||||||
configured_effective_mode
|
configured_effective_mode
|
||||||
|
} else {
|
||||||
|
"off"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep the thermostat target readable even while the zone/group/house control is off.
|
|
||||||
// Home Assistant climate entities otherwise expose target_temperature as unknown.
|
|
||||||
let target_mode = if configured_effective_mode == "off" { zone.mode.as_str() } else { configured_effective_mode };
|
|
||||||
let active_for_target = active_schedule_for_zone(&zone, &schedules, now);
|
|
||||||
let (resolved_preset, resolved_target) = resolve_zone_target(&zone, active_for_target, target_mode);
|
let (resolved_preset, resolved_target) = resolve_zone_target(&zone, active_for_target, target_mode);
|
||||||
let active = if effective_mode == "off" { None } else { active_for_target };
|
let active = if automatic_active { active_for_target } else { None };
|
||||||
let next_events = if effective_mode == "off" {
|
// Future schedule events remain visible while the zone is currently idle between
|
||||||
|
// windows; idle must not be confused with a disabled schedule system.
|
||||||
|
let next_events = if configured_effective_mode == "off" {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
next_schedule_events(&zone, &schedules, effective_mode, now, 8)
|
next_schedule_events(&zone, &schedules, configured_effective_mode, now, 8)
|
||||||
};
|
};
|
||||||
for event in next_events.iter().take(2) {
|
for event in next_events.iter().take(2) {
|
||||||
let mut event = event.clone();
|
let mut event = event.clone();
|
||||||
@@ -57,23 +68,25 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
|||||||
mode: effective_mode.to_string(),
|
mode: effective_mode.to_string(),
|
||||||
configured_mode: zone.mode.clone(),
|
configured_mode: zone.mode.clone(),
|
||||||
inherit_house_mode: zone.inherit_house_mode,
|
inherit_house_mode: zone.inherit_house_mode,
|
||||||
preset: resolved_preset,
|
preset: if has_intent_source { resolved_preset } else { "auto".into() },
|
||||||
preset_override: zone.manual_preset.clone(),
|
preset_override: zone.manual_preset.clone(),
|
||||||
current_temperature: zone.current_temperature,
|
current_temperature: zone.current_temperature,
|
||||||
target_temperature: if zone.device_manual_override || !zone.enabled || effective_mode == "off" {
|
target_temperature: if !has_intent_source {
|
||||||
|
None
|
||||||
|
} else if zone.device_manual_override || !zone.enabled || effective_mode == "off" {
|
||||||
// A remote/manual takeover may leave a physical standby target persisted in the
|
// A remote/manual takeover may leave a physical standby target persisted in the
|
||||||
// zone runtime snapshot. Never publish that value as the thermostat target.
|
// zone runtime snapshot. Publish only the underlying explicit thermostat target.
|
||||||
Some(resolved_target)
|
Some(resolved_target)
|
||||||
} else {
|
} else {
|
||||||
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: zone.enabled && effective_mode != "off" && !zone.device_manual_override,
|
desired_power: automatic_active,
|
||||||
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() }),
|
||||||
actual_setpoint: device.filter(|item| item.power).map(|item| item.target_temperature),
|
actual_setpoint: device.filter(|item| item.power).map(|item| item.target_temperature),
|
||||||
demand: zone.enabled && effective_mode != "off" && !zone.device_manual_override && zone.demand,
|
demand: automatic_active && zone.demand,
|
||||||
control_source: zone.control_temperature_source.clone(),
|
control_source: zone.control_temperature_source.clone(),
|
||||||
manual_override_until: zone.manual_override_until,
|
manual_override_until: zone.manual_override_until,
|
||||||
local_thermostat_power: zone.local_thermostat_power,
|
local_thermostat_power: zone.local_thermostat_power,
|
||||||
@@ -84,8 +97,12 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
|||||||
control_command_source: zone.control_source.clone(),
|
control_command_source: zone.control_source.clone(),
|
||||||
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: if automation_idle {
|
||||||
blocked_reason: 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 },
|
"Automation idle: no active schedule or explicit thermostat request".into()
|
||||||
|
} else {
|
||||||
|
zone.control_reason.clone()
|
||||||
|
},
|
||||||
|
blocked_reason: 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 if automation_idle { Some("no_active_thermostat_intent".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()),
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
pub const LOCAL_THERMOSTAT_RESUME_DELAY_MINUTES: i64 = 15;
|
pub const LOCAL_THERMOSTAT_RESUME_DELAY_MINUTES: i64 = 15;
|
||||||
|
|
||||||
/// Apply the whole-house bulk power state to a thermostat without creating a persistent
|
/// Apply the whole-house bulk power state without creating a blocking global gate. OFF leaves
|
||||||
/// global gate. OFF leaves the zone explicitly/indefinitely off; ON releases that local OFF
|
/// the zone explicitly/indefinitely off. ON releases that local OFF and records a non-manual
|
||||||
/// so the zone can immediately return to its group/manual/automatic controller.
|
/// whole-house thermostat intent so an explicit Global ON remains effective without stealing
|
||||||
|
/// local/group/direct ownership.
|
||||||
pub fn set_house_bulk_thermostat_power(zone: &mut Zone, power: bool) -> bool {
|
pub fn set_house_bulk_thermostat_power(zone: &mut Zone, power: bool) -> bool {
|
||||||
if power {
|
if power {
|
||||||
if zone.local_thermostat_power != Some(false) { return false; }
|
let mut changed = false;
|
||||||
|
if zone.local_thermostat_power == Some(false) {
|
||||||
zone.local_thermostat_power = None;
|
zone.local_thermostat_power = None;
|
||||||
zone.local_thermostat_resume_at = None;
|
zone.local_thermostat_resume_at = None;
|
||||||
zone.local_thermostat_restore_zone_enabled = None;
|
zone.local_thermostat_restore_zone_enabled = None;
|
||||||
true
|
changed = true;
|
||||||
|
}
|
||||||
|
// Whole-house ON is itself an explicit thermostat intent, but it is not manual/local
|
||||||
|
// ownership. Mark only otherwise-free zones so group/local/direct owners keep priority.
|
||||||
|
if !zone.device_manual_override
|
||||||
|
&& zone.local_thermostat_power.is_none()
|
||||||
|
&& !zone.control_source.starts_with("group:")
|
||||||
|
&& zone.control_source != "house_power"
|
||||||
|
{
|
||||||
|
zone.control_source = "house_power".into();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
changed
|
||||||
} else {
|
} else {
|
||||||
let changed = zone.local_thermostat_power != Some(false)
|
let changed = zone.local_thermostat_power != Some(false)
|
||||||
|| zone.local_thermostat_resume_at.is_some()
|
|| zone.local_thermostat_resume_at.is_some()
|
||||||
|
|||||||
@@ -23,7 +23,13 @@ pub fn refresh_control_ownership(zone: &mut Zone, _house_power_enabled: bool) {
|
|||||||
};
|
};
|
||||||
("local_thermostat", source, resume_at, reason)
|
("local_thermostat", source, resume_at, reason)
|
||||||
} 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:")
|
||||||
|
|| matches!(zone.control_source.as_str(), "automation.device" | "house_power")
|
||||||
|
{
|
||||||
|
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())
|
||||||
};
|
};
|
||||||
if zone.control_owner != owner || zone.control_source != source {
|
if zone.control_owner != owner || zone.control_source != source {
|
||||||
|
|||||||
+50
-2
@@ -24,6 +24,8 @@ fn resolve_zone_target(zone: &Zone, schedule: Option<&Schedule>, mode: &str) ->
|
|||||||
(item.preset.clone(), profile_setpoint(zone, &item.preset, mode))
|
(item.preset.clone(), profile_setpoint(zone, &item.preset, mode))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Comfort is only a fallback target for an explicit controller (local/group/etc.).
|
||||||
|
// The absence of a schedule must not by itself create thermostat demand.
|
||||||
("comfort".into(), profile_setpoint(zone, "comfort", mode))
|
("comfort".into(), profile_setpoint(zone, "comfort", mode))
|
||||||
};
|
};
|
||||||
// Quick +/- temperature adjustments are independent from the selected preset.
|
// Quick +/- temperature adjustments are independent from the selected preset.
|
||||||
@@ -31,16 +33,62 @@ fn resolve_zone_target(zone: &Zone, schedule: Option<&Schedule>, mode: &str) ->
|
|||||||
(preset, zone.manual_setpoint.unwrap_or(base_target))
|
(preset, zone.manual_setpoint.unwrap_or(base_target))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return whether some thermostat source currently asks this zone to participate in climate
|
||||||
|
/// control. Heat/Cool configuration plus the implicit Comfort fallback is intentionally not a
|
||||||
|
/// source on its own.
|
||||||
|
fn zone_has_thermostat_intent_source(zone: &Zone, schedule: Option<&Schedule>, now: DateTime<Utc>) -> bool {
|
||||||
|
zone.local_thermostat_power == Some(true)
|
||||||
|
|| temporary_quick_thermostat_is_active(zone, now)
|
||||||
|
|| zone.manual_preset.is_some()
|
||||||
|
|| zone.manual_setpoint.is_some()
|
||||||
|
|| zone.control_source.starts_with("group:")
|
||||||
|
|| matches!(zone.control_source.as_str(), "automation.device" | "house_power")
|
||||||
|
|| schedule.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Automatic thermostat arbitration may run only when an explicit source exists and no higher
|
||||||
|
/// priority OFF/manual gate blocks it. This keeps a resumed/global/group-OFF zone physically OFF
|
||||||
|
/// while it waits for the next schedule window or another explicit thermostat request.
|
||||||
|
fn zone_has_active_thermostat_intent(zone: &Zone, schedule: Option<&Schedule>, now: DateTime<Utc>) -> bool {
|
||||||
|
zone.enabled
|
||||||
|
&& !zone.device_manual_override
|
||||||
|
&& zone.local_thermostat_power != Some(false)
|
||||||
|
&& zone_has_thermostat_intent_source(zone, schedule, now)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn refresh_zone_runtime_target(zone: &mut Zone, schedules: &[Schedule], house_mode: &str) {
|
pub fn refresh_zone_runtime_target(zone: &mut Zone, schedules: &[Schedule], house_mode: &str) {
|
||||||
let configured_mode = effective_zone_mode(zone, house_mode);
|
let configured_mode = effective_zone_mode(zone, house_mode);
|
||||||
zone.effective_mode = configured_mode.clone();
|
|
||||||
let target_mode = if configured_mode == "off" { zone.mode.as_str() } else { configured_mode.as_str() };
|
let target_mode = if configured_mode == "off" { zone.mode.as_str() } else { configured_mode.as_str() };
|
||||||
let schedule = active_schedule_for_zone(zone, schedules, Local::now());
|
let schedule = active_schedule_for_zone(zone, schedules, Local::now());
|
||||||
|
let has_intent_source = zone_has_thermostat_intent_source(zone, schedule, Utc::now());
|
||||||
|
|
||||||
|
// During a physical/manual takeover keep effective_mode owned by the real device state.
|
||||||
|
// We may still refresh an underlying explicit thermostat target for the Resume UI.
|
||||||
|
if zone.device_manual_override {
|
||||||
|
if has_intent_source {
|
||||||
let (preset, target) = resolve_zone_target(zone, schedule, target_mode);
|
let (preset, target) = resolve_zone_target(zone, schedule, target_mode);
|
||||||
zone.active_preset = preset;
|
zone.active_preset = preset;
|
||||||
if !zone.device_manual_override {
|
|
||||||
zone.effective_setpoint = Some(target);
|
zone.effective_setpoint = Some(target);
|
||||||
|
} else {
|
||||||
|
zone.effective_setpoint = None;
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !zone.enabled || zone.local_thermostat_power == Some(false) || !has_intent_source {
|
||||||
|
zone.effective_mode = "off".into();
|
||||||
|
zone.effective_setpoint = None;
|
||||||
|
zone.device_setpoint = None;
|
||||||
|
zone.demand = false;
|
||||||
|
zone.demand_since = None;
|
||||||
|
zone.target_alerted_at = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
zone.effective_mode = configured_mode;
|
||||||
|
let (preset, target) = resolve_zone_target(zone, schedule, target_mode);
|
||||||
|
zone.active_preset = preset;
|
||||||
|
zone.effective_setpoint = Some(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn effective_zone_mode(zone: &Zone, house_mode: &str) -> String {
|
fn effective_zone_mode(zone: &Zone, house_mode: &str) -> String {
|
||||||
|
|||||||
+84
-2
@@ -634,6 +634,81 @@ mod tests {
|
|||||||
assert_eq!(target, 22.5);
|
assert_eq!(target, 22.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn automatic_thermostat_without_active_intent_stays_idle() {
|
||||||
|
let zone = test_zone("device");
|
||||||
|
assert!(!zone_has_thermostat_intent_source(&zone, None, Utc::now()));
|
||||||
|
assert!(!zone_has_active_thermostat_intent(&zone, None, Utc::now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_thermostat_sources_activate_control_without_schedule() {
|
||||||
|
let mut zone = test_zone("device");
|
||||||
|
|
||||||
|
zone.manual_setpoint = Some(22.0);
|
||||||
|
assert!(zone_has_active_thermostat_intent(&zone, None, Utc::now()));
|
||||||
|
|
||||||
|
zone.manual_setpoint = None;
|
||||||
|
zone.manual_preset = Some("sleep".into());
|
||||||
|
assert!(zone_has_active_thermostat_intent(&zone, None, Utc::now()));
|
||||||
|
|
||||||
|
zone.manual_preset = None;
|
||||||
|
zone.local_thermostat_power = Some(true);
|
||||||
|
assert!(zone_has_active_thermostat_intent(&zone, None, Utc::now()));
|
||||||
|
|
||||||
|
zone.local_thermostat_power = None;
|
||||||
|
zone.control_source = "group:upstairs".into();
|
||||||
|
assert!(zone_has_active_thermostat_intent(&zone, None, Utc::now()));
|
||||||
|
|
||||||
|
zone.control_source = "automation.device".into();
|
||||||
|
refresh_control_ownership(&mut zone, true);
|
||||||
|
assert_eq!(zone.control_owner, "automation");
|
||||||
|
assert_eq!(zone.control_source, "automation.device");
|
||||||
|
assert!(zone_has_active_thermostat_intent(&zone, None, Utc::now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_off_blocks_even_an_active_schedule_until_resume() {
|
||||||
|
let mut zone = test_zone("device");
|
||||||
|
let schedule = test_schedule("active", vec![1,2,3,4,5,6,7], "00:00", "00:00");
|
||||||
|
zone.local_thermostat_power = Some(false);
|
||||||
|
assert!(zone_has_thermostat_intent_source(&zone, Some(&schedule), Utc::now()));
|
||||||
|
assert!(!zone_has_active_thermostat_intent(&zone, Some(&schedule), Utc::now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_target_refresh_has_no_implicit_comfort_activation() {
|
||||||
|
let mut zone = test_zone("device");
|
||||||
|
zone.inherit_house_mode = false;
|
||||||
|
zone.mode = "cool".into();
|
||||||
|
zone.effective_setpoint = Some(23.0);
|
||||||
|
|
||||||
|
refresh_zone_runtime_target(&mut zone, &[], "cool");
|
||||||
|
|
||||||
|
assert_eq!(zone.effective_mode, "off");
|
||||||
|
assert_eq!(zone.effective_setpoint, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resuming_local_off_without_schedule_stays_off() {
|
||||||
|
let mut zone = test_zone("device");
|
||||||
|
zone.inherit_house_mode = false;
|
||||||
|
zone.mode = "cool".into();
|
||||||
|
zone.demand = true;
|
||||||
|
set_local_thermostat_power(&mut zone, false, Utc::now());
|
||||||
|
|
||||||
|
assert!(reset_local_thermostat_override(&mut zone));
|
||||||
|
refresh_control_ownership(&mut zone, true);
|
||||||
|
refresh_zone_runtime_target(&mut zone, &[], "cool");
|
||||||
|
|
||||||
|
assert!(zone.local_thermostat_power.is_none());
|
||||||
|
assert_eq!(zone.control_source, "automation");
|
||||||
|
assert_eq!(zone.effective_mode, "off");
|
||||||
|
assert_eq!(zone.effective_setpoint, None);
|
||||||
|
assert!(!zone.demand);
|
||||||
|
assert!(!zone_has_active_thermostat_intent(&zone, None, Utc::now()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_target_refresh_applies_manual_profile_immediately() {
|
fn runtime_target_refresh_applies_manual_profile_immediately() {
|
||||||
let mut zone = test_zone("device");
|
let mut zone = test_zone("device");
|
||||||
@@ -800,14 +875,21 @@ mod tests {
|
|||||||
assert!(set_house_bulk_thermostat_power(&mut zone, true));
|
assert!(set_house_bulk_thermostat_power(&mut zone, true));
|
||||||
assert!(zone.local_thermostat_power.is_none());
|
assert!(zone.local_thermostat_power.is_none());
|
||||||
assert!(zone.local_thermostat_resume_at.is_none());
|
assert!(zone.local_thermostat_resume_at.is_none());
|
||||||
|
assert_eq!(zone.control_source, "house_power");
|
||||||
|
assert!(zone_has_active_thermostat_intent(&zone, None, Utc::now()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn whole_house_on_does_not_create_a_local_on_override() {
|
fn whole_house_on_creates_automatic_intent_without_local_ownership() {
|
||||||
let mut zone = test_zone("device");
|
let mut zone = test_zone("device");
|
||||||
zone.local_thermostat_power = None;
|
zone.local_thermostat_power = None;
|
||||||
assert!(!set_house_bulk_thermostat_power(&mut zone, true));
|
assert!(set_house_bulk_thermostat_power(&mut zone, true));
|
||||||
assert!(zone.local_thermostat_power.is_none());
|
assert!(zone.local_thermostat_power.is_none());
|
||||||
|
assert_eq!(zone.control_source, "house_power");
|
||||||
|
refresh_control_ownership(&mut zone, true);
|
||||||
|
assert_eq!(zone.control_owner, "automation");
|
||||||
|
assert_eq!(zone.control_source, "house_power");
|
||||||
|
assert!(zone_has_active_thermostat_intent(&zone, None, Utc::now()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -385,6 +385,60 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let active_schedule = active_schedule_for_zone(&zone, &schedules, Local::now());
|
let active_schedule = active_schedule_for_zone(&zone, &schedules, Local::now());
|
||||||
|
|
||||||
|
// Heat/Cool configuration alone is not an instruction to run forever on the implicit
|
||||||
|
// Comfort profile. After local/global/group OFF is handed back to automation, a zone
|
||||||
|
// with no active schedule, quick/manual target, temporary session or active scoped
|
||||||
|
// controller stays physically OFF. This also makes gaps between schedule windows true
|
||||||
|
// OFF periods instead of silently falling back to Comfort and recreating demand.
|
||||||
|
if !zone_has_active_thermostat_intent(&zone, active_schedule, Utc::now()) {
|
||||||
|
clear_compressor_pending(&mut zone, true);
|
||||||
|
zone.effective_mode = "off".into();
|
||||||
|
zone.effective_setpoint = None;
|
||||||
|
zone.device_setpoint = None;
|
||||||
|
zone.demand = false;
|
||||||
|
zone.demand_since = None;
|
||||||
|
zone.target_alerted_at = None;
|
||||||
|
if zone.control_owner == "automation" {
|
||||||
|
zone.control_reason = "Automation idle: no active schedule or explicit thermostat request".into();
|
||||||
|
zone.control_resume_at = 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() },
|
||||||
|
).await {
|
||||||
|
Ok(Some(updated_device)) => {
|
||||||
|
let transition_at = Utc::now();
|
||||||
|
if device.power != updated_device.power {
|
||||||
|
zone.last_power_change_at = Some(transition_at);
|
||||||
|
}
|
||||||
|
zone.last_action_at = Some(transition_at);
|
||||||
|
state.log("info", "zone.automation_idle_off", &format!("Zone {} remains OFF: no active thermostat intent", zone.name), json!({
|
||||||
|
"zone_id": zone.id,
|
||||||
|
"device_id": zone.device_id,
|
||||||
|
"active_schedule": false,
|
||||||
|
"manual_preset": zone.manual_preset,
|
||||||
|
"manual_setpoint": zone.manual_setpoint,
|
||||||
|
"control_source": zone.control_source,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(err) => state.log("error", "zone.automation_idle_off_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;
|
||||||
|
}
|
||||||
|
|
||||||
let (preset, target) = resolve_zone_target(&zone, active_schedule, effective_mode);
|
let (preset, target) = resolve_zone_target(&zone, active_schedule, effective_mode);
|
||||||
zone.active_preset = preset;
|
zone.active_preset = preset;
|
||||||
zone.effective_setpoint = Some(target);
|
zone.effective_setpoint = Some(target);
|
||||||
|
|||||||
+3
-2
@@ -237,11 +237,12 @@ function zoneCard(zone, detailed = true) {
|
|||||||
? `${tr('zones.greeTemp')}: ${fmtTemp(zone.device_temperature ?? device?.current_temperature)}`
|
? `${tr('zones.greeTemp')}: ${fmtTemp(zone.device_temperature ?? device?.current_temperature)}`
|
||||||
: `${tr('zones.greeTemp')}: ${fmtTemp(zone.device_temperature)} · ${tr('zones.externalTemp')}: ${fmtTemp(zone.external_temperature)} · ${tr('zones.usedSource')}: ${zoneControlSourceLabel(zone.control_temperature_source)}`;
|
: `${tr('zones.greeTemp')}: ${fmtTemp(zone.device_temperature)} · ${tr('zones.externalTemp')}: ${fmtTemp(zone.external_temperature)} · ${tr('zones.usedSource')}: ${zoneControlSourceLabel(zone.control_temperature_source)}`;
|
||||||
const roomTemperature = zone.current_temperature ?? zone.device_temperature ?? device?.current_temperature;
|
const roomTemperature = zone.current_temperature ?? zone.device_temperature ?? device?.current_temperature;
|
||||||
const target = Number(zone.effective_setpoint ?? zone.setpoint);
|
|
||||||
const manual = zone.manual_preset || 'auto';
|
const manual = zone.manual_preset || 'auto';
|
||||||
const displayPreset = zone.manual_preset || zone.active_preset || 'comfort';
|
|
||||||
const mode = zone.inherit_house_mode ? 'house' : zone.mode;
|
const mode = zone.inherit_house_mode ? 'house' : zone.mode;
|
||||||
const effectiveMode = zone.effective_mode || (zone.inherit_house_mode ? (app.settings?.house_mode || 'off') : zone.mode) || 'off';
|
const effectiveMode = zone.effective_mode || (zone.inherit_house_mode ? (app.settings?.house_mode || 'off') : zone.mode) || 'off';
|
||||||
|
const idleWithoutTarget = effectiveMode === 'off' && zone.effective_setpoint == null && zone.manual_setpoint == null && zone.manual_preset == null;
|
||||||
|
const target = idleWithoutTarget ? Number.NaN : Number(zone.effective_setpoint ?? zone.manual_setpoint ?? zone.setpoint);
|
||||||
|
const displayPreset = idleWithoutTarget ? 'auto' : (zone.manual_preset || zone.active_preset || 'comfort');
|
||||||
const globalModeAvailable = ['heat', 'cool'].includes(app.settings?.house_mode);
|
const globalModeAvailable = ['heat', 'cool'].includes(app.settings?.house_mode);
|
||||||
const globalModeDisabled = globalModeAvailable ? '' : ` disabled aria-disabled="true" title="${esc(tr('zones.globalModeUnavailable'))}"`;
|
const globalModeDisabled = globalModeAvailable ? '' : ` disabled aria-disabled="true" title="${esc(tr('zones.globalModeUnavailable'))}"`;
|
||||||
const hasManualOverride = zone.manual_preset != null || zone.manual_setpoint != null;
|
const hasManualOverride = zone.manual_preset != null || zone.manual_setpoint != null;
|
||||||
|
|||||||
Reference in New Issue
Block a user