This commit is contained in:
Mateusz Gruszczyński
2026-08-27 16:44:41 +02:00
parent ba61cb77b1
commit 65eaf50fb6
12 changed files with 64 additions and 49 deletions
+22 -3
View File
@@ -1,8 +1,8 @@
# GREE Controller v0.7.10 - build and validation report
# GREE Controller v0.7.11 - build and validation report
## Scope
Version 0.7.10 includes the previous climate-group, whole-house and zone-profile fixes plus a strict separation between local quick-thermostat ownership and true physical/manual-device takeover. Global controls are consolidated, group control stays immediately below them, and lower-priority dashboard sections are collapsed on demand.
Version 0.7.11 includes the previous climate-group, whole-house and zone-profile fixes plus a strict separation between local quick-thermostat ownership and true physical/manual-device takeover. Global controls are consolidated, group control stays immediately below them, and lower-priority dashboard sections are collapsed on demand.
## Implemented changes
@@ -398,7 +398,7 @@ Validation for this package is recorded after the final archive/manifest checks.
## 2026-08-27 v0.7.10 local quick thermostat and false takeover fix
- Added persisted `local_thermostat_power` ownership with three distinct states: normal group/schedule ownership (`null`), locally forced thermostat ON (`true`) and locally forced thermostat OFF (`false`).
- Dashboard quick ON/OFF and the legacy zone manual-power route now use local thermostat semantics; they never call the direct/manual-device command path.
- Dashboard quick ON/OFF uses local thermostat semantics and never calls the direct/manual-device command path.
- A locally active thermostat bypasses disabled climate-group power gates while retaining the normal sensor, hysteresis, profile/target, Smart Fan and GREE setpoint-modulation logic.
- **Resume automation** for a local thermostat clears local ownership and returns the zone to group/schedule control. The existing device takeover Resume action remains separate.
- Direct device commands and genuine remote/pilot changes still create `device_manual_override`; local thermostat ownership does not.
@@ -417,3 +417,22 @@ Validation for this package is recorded after the final archive/manifest checks.
- Rust source delimiter/static invariant checks passed, including the new regulator wake-up and control-plan fields.
- `FILE_MANIFEST.sha256` is regenerated from the final tree and verified before packaging; the resulting ZIP is tested with `unzip -t`.
- This environment does not provide `cargo`, `rustc` or `rustfmt`, so the normal target/CI update path must still run `cargo test --all-targets` and `cargo build --release` before replacing the running binary.
## 2026-08-27 v0.7.11 cleanup and compile fix
- Removed the obsolete `/api/zones/{id}/manual-power` compatibility route, request structure and handler. The only zone quick-power API is now `POST /api/zones/{id}/control` with `power:true/false`.
- Renamed Web UI local-thermostat power internals so they no longer use misleading `manual-power` terminology.
- Fixed Rust error E0505 in `local_thermostat_ownership_blocks_direct_automation`: the test now passes a borrowed one-element slice with `std::slice::from_ref(&zone)` instead of moving `zone` while borrowing `zone.device_id`.
- Advanced package/Home Assistant/PWA metadata to `0.7.11`.
## Final validation for v0.7.11
- `node --check` passed for Web UI JavaScript files.
- Home Assistant modules, helper scripts and `make_zip.py` passed `python3 -m py_compile`.
- JSON files parsed successfully; Polish and English language packs have identical 602-key sets.
- All shell scripts passed `bash -n`; `web/index.html` parsed successfully and CSS braces are balanced.
- Runtime source trees (`src`, `web`, `home-assistant`, `scripts`) contain no legacy `manual-power` route, handler, symbol or CSS naming.
- Cargo package and Home Assistant manifest report version `0.7.11`; PWA cache metadata was rotated for this release.
- The reported Rust E0505 test case was corrected with borrow-only `std::slice::from_ref(&zone)` calls.
- This packaging environment does not provide `cargo`, `rustc` or `rustfmt`; target/CI must run `cargo test --all-targets` and `cargo build --release` before deployment.
Generated
+1 -1
View File
@@ -633,7 +633,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.7.10"
version = "0.7.11"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
version = "0.7.10"
version = "0.7.11"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+11 -11
View File
@@ -1,12 +1,12 @@
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
ef3c2cdf13fdeac192c2f94fb321af3dc60ad93d97266c00cf2b8021ed0f211c ./BUILD_REPORT.md
723ce7201821a3ff3404c93430de5dc8451f0e091f39fdbe532167d9a647250a ./Cargo.lock
1c52ed460ad66fa818d0f405ca92ef9df1beff0c220f34c548e54d93e7e99721 ./Cargo.toml
d660312751ad5ddbc730d25ddd2181b1c4c8be4a8da18e2b23eaa39ca00c1d63 ./BUILD_REPORT.md
83f9d4ee92b722c3f81bb739a55d4b2d66bbd09276c31876cbf4f610a812c280 ./Cargo.lock
d42b2cdbf06555dd327405c9f84c14b7f222faa545c4e6f5d9fde8b6e9ad55ac ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
536dec61cf1e37c98b33989db53f356895fb1c241d72eccf317e800a830fa984 ./README.md
f846576df3ace004a7c4f3cd104242cbc38814b383938d6d914ad3ebc085c71e ./README.md
f2c8eb28fedb26bb8f5636e1805e7a782cc472b87d882f6752d520f16216a248 ./build.rs
5254141d54e0fe55bb4942d9328221eeddae6d8f63d71d33c2e642647bd3cac6 ./docs/API.md
38bbc16b8350ca5f70646b40d1b7d0a9231425208c99f4f2d17979431c0c289a ./docs/API.md
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md
10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md
@@ -23,7 +23,7 @@ b065b61ef1adafe96aae3e47bff3e8b0bba57796fdfac1503669046beb1c0157 ./home-assista
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
dcfa12c94cfb939078c13658162c2f1e4bf7ad2f29d790524e72a1d3c4a4f3d7 ./home-assistant/custom_components/gree_controller/manifest.json
7a0171c08b8edd44369416b067c0a267b88c8fb28fbf34125385d0d6c9e308cb ./home-assistant/custom_components/gree_controller/manifest.json
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
1cae12876eb81085910907cf1fcf340a18dcba4f8c967ef222c9496325057849 ./home-assistant/custom_components/gree_controller/sensor.py
@@ -46,10 +46,10 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
7c4bf023ad1df88abdbc98eb0ff60f984396359f6d8320b8817da5fe832e76a6 ./src/api.rs
9a9c8267a8b4dab192b1d97d2ef3a255338e2a3831f7bf33ce4b18fb85f74f19 ./src/api.rs
32b1d0a7ac2060fd18fe7f1ca1d110ee45beaf67e81514d24d6bde3d36301387 ./src/config.rs
5dfda2f4dc540c502885b0cd7017dc77768684588acf528f01d1fd88f1af4aec ./src/db.rs
5da9a1369af514300911dc59c8f53a5d254a6e3622d6a689786b3ca5bfb1fa40 ./src/engine.rs
2c7f21d18d04d63691f0856b895fd29cd625861220773dce9e74a52d5d241619 ./src/engine.rs
ae3b496749a3fd723b243d9bea92e5d76249f52814c80359ad9bac53abacb074 ./src/error.rs
081f6909e95839c7da8c40b866af9de237e0540f3e65e1ce739ce70a6fa94846 ./src/home_assistant.rs
190b0a33431539676e5dd7796698077f16c179d42eae4501ca96a91bf797cbf8 ./src/influxdb.rs
@@ -62,10 +62,10 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol
6a1c0cab3eab80ecd254c5e486b1fba068523edb8d699c64054c17f67f4a31d0 ./src/queries.rs
6081635986e1ce39a0e195c3976ae30a033454fe90ec80bcbdf05fc26bf7087f ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
f7c44789b68f6f49e8409b3867f410f2ed39cb59d0bbffceec7fd9dad2bd5f71 ./web/app.js
23dcfdfc9eb2093c7289dad522dcaec76c802c7b45dbd58e074b3bdf8be2f323 ./web/app.js
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
5e39be3a379e3036c03773d5f4cc22baed79e9688b574b438fe87b6f5828089a ./web/index.html
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
d4369e34b5392be142c038f5fc68343bd3514a8b34aa8503f1ed2ed700737299 ./web/styles.css
38cec1ebc782749c4838e1bd73cd371fb57a99310df793c52a69adad9f36b5c7 ./web/sw.js
59df58dc528fbbbb5b6097df7110e78d35951506cc056816c7b2be51f4d51965 ./web/styles.css
f7d2bae7f7abe896ad56996b5423547e4edd056c9bcd7fedbbf89ea91f5bacee ./web/sw.js
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
+9 -2
View File
@@ -4,7 +4,7 @@
Standalone local GREE air-conditioner controller written in Rust. It runs on a regular Linux host or an LXC container and provides a mobile-first web interface without depending on the vendor cloud.
Current version: **0.7.10**.
Current version: **0.7.11**.
## Highlights
@@ -47,7 +47,7 @@ See [`BUILD_REPORT.md`](BUILD_REPORT.md) for package validation details and [`do
On Debian, Ubuntu or an LXC container:
```bash
unzip gree-controller-v0.7.10.zip
unzip gree-controller-v0.7.11.zip
cd gree-controller
chmod +x scripts/*.sh
./scripts/dev.sh
@@ -477,6 +477,13 @@ Zones now use setpoint modulation, global seasonal house mode, cooling/heating C
- Rotated the PWA cache key so browsers fetch the corrected stylesheet after upgrade.
## 0.7.11 Cleanup and Rust test fix
- Removed the obsolete `/api/zones/{id}/manual-power` compatibility endpoint and its handler; local thermostat power has one canonical API path: `/api/zones/{id}/control`.
- Renamed dashboard internals from `manual-power` terminology to `local-thermostat-power` to match the actual control model.
- Fixed the Rust ownership error in `local_thermostat_ownership_blocks_direct_automation` by borrowing the test zone via `std::slice::from_ref`, so the test no longer moves `zone` while borrowing `zone.device_id`.
## 0.7.10 Local quick-thermostat ownership
- Quick thermostat ON/OFF now controls a dedicated local thermostat state instead of using remote/manual-device semantics.
+1 -1
View File
@@ -137,7 +137,7 @@ curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: appl
`mode` on the quick zone endpoint accepts `house`, `cool`, or `heat`. `house` means **Follow global mode** in the UI and should be left unchanged when the zone is meant to inherit the global GREE Controller mode. A fixed `cool`/`heat` is an explicit per-zone override; therefore house mode `off` stops inherited zones but does not disable a zone deliberately fixed to Cooling or Heating. Whole-house master power remains authoritative over all zones.
`power:true/false` on `/api/zones/{id}/control` is a local quick-thermostat power override, not a physical/manual command. `true` lets that zone run through the full thermostat even when its climate group is off; `false` keeps only that zone locally off. `clear_local_thermostat_override:true` returns power ownership to the normal group/schedule path. The legacy `/api/zones/{id}/manual-power` route is retained for compatibility but now has the same local-thermostat semantics. Direct/pilot-style control remains `/api/devices/{id}/command`.
`power:true/false` on `/api/zones/{id}/control` is a local quick-thermostat power override, not a physical/manual command. `true` lets that zone run through the full thermostat even when its climate group is off; `false` keeps only that zone locally off. `clear_local_thermostat_override:true` returns power ownership to the normal group/schedule path. Direct/pilot-style control remains `/api/devices/{id}/command`.
### Physical/manual device takeover
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.7.10",
"version": "0.7.11",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
-20
View File
@@ -50,7 +50,6 @@ pub fn router(state: AppState) -> Router {
.route("/api/zones", get(list_zones).post(create_zone))
.route("/api/zones/:id", get(get_zone).put(update_zone).delete(delete_zone))
.route("/api/zones/:id/control", post(update_zone_control))
.route("/api/zones/:id/manual-power", post(update_zone_manual_power))
.route("/api/zones/:id/schedule-template", post(apply_schedule_template))
.route("/api/groups", get(list_groups).post(create_group))
.route("/api/groups/:id", get(get_group).put(update_group).delete(delete_group))
@@ -720,25 +719,6 @@ async fn update_zone_control(State(state): State<AppState>, Path(id): Path<Strin
}
#[derive(Debug, Deserialize)]
struct ZoneManualPowerPatch { power: bool }
async fn update_zone_manual_power(
State(state): State<AppState>,
Path(id): Path<String>,
Json(input): Json<ZoneManualPowerPatch>,
) -> Result<Json<Value>, AppError> {
// Backward-compatible route: despite the historical name this now controls the local
// thermostat, not the physical unit like a pilot. Direct/pilot semantics stay on /devices.
let zone = apply_zone_control_patch(
&state,
&id,
ZoneControlPatch { power: Some(input.power), ..Default::default() },
).await?;
let device = state.db.get_device(&zone.device_id)?;
Ok(Json(json!({"zone": zone, "device": device})))
}
async fn ensure_device_stopped_for_detach(state: &AppState, device_id: &str, source: &str) -> Result<(), AppError> {
let Some(device) = state.db.get_device(device_id)? else { return Ok(()); };
+12 -3
View File
@@ -2301,11 +2301,20 @@ mod tests {
#[test]
fn local_thermostat_ownership_blocks_direct_automation() {
let mut zone = test_zone("device");
assert!(!device_blocked_by_local_thermostat(&zone.device_id, &[zone.clone()]));
assert!(!device_blocked_by_local_thermostat(
&zone.device_id,
std::slice::from_ref(&zone),
));
zone.local_thermostat_power = Some(true);
assert!(device_blocked_by_local_thermostat(&zone.device_id, &[zone.clone()]));
assert!(device_blocked_by_local_thermostat(
&zone.device_id,
std::slice::from_ref(&zone),
));
zone.local_thermostat_power = Some(false);
assert!(device_blocked_by_local_thermostat(&zone.device_id, &[zone]));
assert!(device_blocked_by_local_thermostat(
&zone.device_id,
std::slice::from_ref(&zone),
));
}
#[test]
+2 -2
View File
@@ -730,7 +730,7 @@ function zoneCard(zone, detailed = true) {
: '';
const localPowerDisabled = !device || device.enabled === false ? ' disabled' : '';
const localRequestedOn = localThermostatPower === true ? true : (localThermostatPower === false ? false : !!device?.power);
const manualUnitPower = `<div class="zone-manual-power"><button type="button" class="zone-unit-power-toggle ${localRequestedOn ? 'active' : ''}" title="${esc(tr('zones.unitPowerLocalHint'))}" data-action="zone-device-power" data-id="${esc(zone.id)}" data-value="${localRequestedOn ? 'false' : 'true'}" aria-pressed="${localRequestedOn ? 'true' : 'false'}"${localPowerDisabled}>${esc(tr(localRequestedOn ? 'zones.turnThermostatOff' : 'zones.turnThermostatOn'))}</button></div>`;
const localThermostatPowerControl = `<div class="zone-local-thermostat-power"><button type="button" class="zone-unit-power-toggle ${localRequestedOn ? 'active' : ''}" title="${esc(tr('zones.unitPowerLocalHint'))}" data-action="zone-device-power" data-id="${esc(zone.id)}" data-value="${localRequestedOn ? 'false' : 'true'}" aria-pressed="${localRequestedOn ? 'true' : 'false'}"${localPowerDisabled}>${esc(tr(localRequestedOn ? 'zones.turnThermostatOff' : 'zones.turnThermostatOn'))}</button></div>`;
if (detailed) {
const groupNames = (app.groups || []).filter(group => (group.zone_ids || []).includes(zone.id)).map(group => group.name);
@@ -756,7 +756,7 @@ function zoneCard(zone, detailed = true) {
return `<article class="list-card zone-thermostat quick-control-card quick-thermostat-control ${zone.demand ? 'demanding' : ''} ${zone.enabled ? '' : 'zone-disabled'} ${deviceManualOverride ? 'manual-takeover' : ''}" data-zone-card="${esc(zone.id)}">
<div class="list-card-head"><div><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(displayPreset))}</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'))}"><span>${zone.enabled ? '' : ''}</span>${esc(state)}</button></div>
${manualUnitPower}
${localThermostatPowerControl}
<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">${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">
${['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('')}
+3 -3
View File
@@ -780,9 +780,9 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
.quick-device-control .temperature-control { gap:12px; padding:12px 0 10px; }
.quick-device-control .current-line { margin:-3px 0 10px; font-size:11px; }
.quick-device-control .device-toggles.quick-control-row { margin-top:1px; }
.quick-thermostat-control .zone-manual-power { display:flex; margin-top:6px; }
.quick-thermostat-control .zone-manual-power .zone-unit-power-toggle { width:100%; min-height:30px; font-size:10px; }
.quick-thermostat-control .zone-manual-power .zone-unit-power-toggle.active { color:var(--accent-text); background:var(--accent); font-weight:800; }
.quick-thermostat-control .zone-local-thermostat-power { display:flex; margin-top:6px; }
.quick-thermostat-control .zone-local-thermostat-power .zone-unit-power-toggle { width:100%; min-height:30px; font-size:10px; }
.quick-thermostat-control .zone-local-thermostat-power .zone-unit-power-toggle.active { color:var(--accent-text); background:var(--accent); font-weight:800; }
.quick-thermostat-control .thermostat-main { margin:8px 0 7px; }
.quick-thermostat-control .zone-enable-toggle { min-height:28px; padding:4px 8px; font-size:10px; }
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'gree-controller-v0710-local-thermostat-ownership';
const CACHE = 'gree-controller-v0711-clean-local-thermostat';
const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/';
const ASSETS = [path('/'), path('/styles.css'), path('/app.js'), path('/theme-init.js'), path('/favicon.svg'), path('/manifest.webmanifest'), path('/lang/index.json'), path('/lang/en.json')];