This commit is contained in:
Mateusz Gruszczyński
2026-08-25 09:43:28 +02:00
parent fd26d6877f
commit 440c7c3a8d
15 changed files with 118 additions and 55 deletions
+11 -2
View File
@@ -1,8 +1,8 @@
# GREE Controller v0.6.5 - build and validation report
# GREE Controller v0.6.6 - build and validation report
## Scope
Version 0.6.5 includes the 0.6.4 changes described below plus the whole-house thermostat semantics, master power control, Home Assistant house controls and LAN response-time reporting documented in this report.
Version 0.6.6 includes the 0.6.5 changes described below plus synchronized whole-house activation: selecting a global operating mode or work profile now re-enables master power and powers on all enabled units. The global power panel is also placed above the house climate controls.
## Implemented changes
@@ -130,3 +130,12 @@ Validation performed in the build environment:
- Home Assistant now exposes controller-level **Thermostat mode** and **Work profile** select entities plus an **All air conditioners** master-power switch, backed by dedicated restricted HA endpoints.
- Device state stores the latest successful GREE communication round-trip time. **LAN network -> Devices** displays it in milliseconds next to the IP address and protocol.
- PWA cache key advanced to `gree-controller-v065`.
## 0.6.6 synchronized whole-house activation
- The global power panel is displayed above **House climate mode**.
- Selecting whole-house Cooling or Heating clears a previous global power-off and powers on all enabled units.
- Selecting Auto, Comfort, Sleep or Away as a whole-house profile does the same.
- House mode `off` / **Do not control** remains non-invasive and does not alter master power.
- The behavior is implemented in the shared backend endpoints, so Home Assistant house-mode/profile controls follow the same rules.
Generated
+1 -1
View File
@@ -633,7 +633,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.6.5"
version = "0.6.6"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
version = "0.6.5"
version = "0.6.6"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+14 -14
View File
@@ -1,19 +1,19 @@
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
c6ea33a11a287f159c6e4c5f19967d8599146692cd576cfe6b8ccacddadd558a ./BUILD_REPORT.md
c9673a32218f054f3d3d3b7f89cba3f0aa75f7078558e43ed3adff064e65d209 ./Cargo.lock
735f566fe24949e8a4fae1bebd42d12e91a91cb99a36b114803f0cf0b93212a6 ./Cargo.toml
c5c9d9dad288bd9ec4695db7e4c1003ddd39a6cdf70073362dd84c7a87da57f8 ./BUILD_REPORT.md
dae36dbeeb9f26dce98eefc232c47b2fcbd5b75c9b896943990fa65b642c3638 ./Cargo.lock
2a20c8523878f420c0f3d4df542b60220d7b645da8ed2acdc71d7328d721013e ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
90fe7b3c18f2ce0d244b8134b73841d3b416d33f7c13d3da107517a124c58d7e ./README.md
0e702e55efa6445d959c03c54dff71b1e56317b6fa9de28c52ef34a38c65e9fc ./README.md
f2c8eb28fedb26bb8f5636e1805e7a782cc472b87d882f6752d520f16216a248 ./build.rs
6e0e5eb103c4acdaeb04c224fdd49e7ec051c667732d131439a165ac1739b713 ./docs/API.md
1aeca9e23d3a4fcbe3214acafdc2a33cdffa0ad46efb5ead4477a2ebc3a4286c ./docs/API.md
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md
10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md
19e6532f21c59b93c77863e8a8ac3a7d9abaf869f18e30401b7d0975b404b06e ./docs/PROJECT_SPEC.md
7748b1230029439a405139750b2e0607b8e463bfd56ad86982394105069697c5 ./docs/REVERSE_PROXY.md
28f1403d84b01b37bebb72548aa39a75bb14bdbb611a43bfa761319957cd6f39 ./docs/SECURITY.md
248570ceff0e020c9d81e8b9e1622ce33f3350f51a04371a9f4fc3f8fa13f62c ./home-assistant/README.md
8942e83eabe4edc9728594236b61e3c2be39b8bb46b66c19c4eff7ec7fe80716 ./home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
001215412d77abd393f34c414ff6d0ebbaf419f140d76037b7f114a7955df6c3 ./home-assistant/custom_components/gree_controller/api.py
abcca32b090e5a5d02574a3a340e97321f9879ae6c94ef12eb1f42d967eae2d5 ./home-assistant/custom_components/gree_controller/climate.py
@@ -23,7 +23,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./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
8666eebd03276d6649104dcf538b79073f4f50a51b1440bcbad0a23c09b0f78b ./home-assistant/custom_components/gree_controller/manifest.json
52c67fb50b8e9fec5c60af20df6db3385cc40afe08e57b9ea1776ab5fefdaf70 ./home-assistant/custom_components/gree_controller/manifest.json
d0206766d76f70ea1c75a618e41bf1e8e8c0429f42ee97f40daac1ea12f16658 ./home-assistant/custom_components/gree_controller/number.py
459233ccefa7827fc1c6887146d8a69e01149a497635fd5e1802c220f418eb28 ./home-assistant/custom_components/gree_controller/select.py
ec35191e27a018a72cb341914e2abd7fecd25f6b641ea25bebb9e809dd8ecaff ./home-assistant/custom_components/gree_controller/sensor.py
@@ -32,8 +32,8 @@ ec35191e27a018a72cb341914e2abd7fecd25f6b641ea25bebb9e809dd8ecaff ./home-assista
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
997c7a91017b5d51b3e44016d21252b37d302f95ba0fb5802f3d8fd116687d6f ./lang/en.json
33f6935ad1fe72ea0e6893e230ff95565388410fb0fbfe974b34610d789c5d2b ./lang/pl.json
3b147813b7ad9d44c99ec4b2c062de77f914ae04538abc6cfaa9e5451fc6a83c ./lang/en.json
c301de561c5999031b7a09101a67ab5c7ee49118fe181ad152741c6c2ea01272 ./lang/pl.json
028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
@@ -46,7 +46,7 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
f8d1349236819fe0f0253df5269353245f0cacf5b29614ac79c2317c16943636 ./src/api.rs
f3d38b84a241cf0dd14ae8f8db7f1ef972734c1f2acaeb8d0bd7997c6e8f05bd ./src/api.rs
32b1d0a7ac2060fd18fe7f1ca1d110ee45beaf67e81514d24d6bde3d36301387 ./src/config.rs
a89bc49e359715109149130a10ac60abd26bfd2bc18c5ee5142e01696d9f9cdd ./src/db.rs
8ec14c01036c73f28cdb330d696f0d3573198e7ea4d03ba6bf9cc1d34ec6c7a8 ./src/engine.rs
@@ -62,10 +62,10 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol
d1cba5e55c77d44c75a9c9897ae76c327fd96bdf9c01ad27e34322b689348487 ./src/queries.rs
f74ac398c6f2e64ebb965900330324f3ec7403e70339638a408229f29fbd07ae ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
53b94be090889a8f709b16b0f88e9e9c732427151de3cb740c1f16144dec37b4 ./web/app.js
8a5471859605115780e1a1cc3d07345698c846c642a9a604977ae2ca73cec0ec ./web/app.js
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
59e85721942f533243d12491e201dbfedef859dc752b7f0c7c37405705832d2d ./web/index.html
3d47ede16e8652f2b36223ca26271ca302d8a1c62030f18a47b461da9c1bf47f ./web/index.html
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
a9643913bf5ceba3b0940fff583350f5bb853e7d0421296a9661a4e3e256bde1 ./web/styles.css
e804540ecc928c3f29a8e71fdae68a6e0a36eb0c0698b1ca22cf8f42a888d61c ./web/sw.js
3796167c55146cf549a6df8b7057072ba4731ed667f9691a35a21832139169cd ./web/styles.css
aba62a4b8535e59fd78650142e96c7234ef744e02f28178923b40b5188cb0265 ./web/sw.js
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
+2 -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.6.5**.
Current version: **0.6.6**.
## Highlights
@@ -46,7 +46,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.6.5.zip
unzip gree-controller-v0.6.6.zip
cd gree-controller
chmod +x scripts/*.sh
./scripts/dev.sh
+1 -1
View File
@@ -79,7 +79,7 @@ curl -X POST "$BASE/api/house/preset" -H "$AUTH" -H 'Content-Type: application/j
-d '{"preset":"sleep"}'
```
House modes: `cool`, `heat`, `off` (`off` = no house-level thermostat control). House presets: `auto`, `comfort`, `sleep`, `away`. A non-`auto` house preset creates temporary per-zone overrides that expire at each zone's next schedule boundary. `POST /api/house/power` controls a separate whole-house master power state. With master power off, zones and controller automations cannot restart units; the selected house thermostat mode is preserved. This is independent from house mode `off`, which only means no house-level thermostat control and allows direct/manual device operation.
House modes: `cool`, `heat`, `off` (`off` = no house-level thermostat control). House presets: `auto`, `comfort`, `sleep`, `away`. A non-`auto` house preset creates temporary per-zone overrides that expire at each zone's next schedule boundary. `POST /api/house/power` controls a separate whole-house master power state. With master power off, zones and controller automations cannot restart units; the selected house thermostat mode is preserved. This is independent from house mode `off`, which only means no house-level thermostat control and allows direct/manual device operation. Choosing whole-house `cool`/`heat` or any whole-house preset is treated as an explicit whole-house activation: master power is set back to on and all enabled units are powered on. Choosing house mode `off` does not change master power.
A zone stores separate profile temperatures for both seasons:
+1
View File
@@ -132,6 +132,7 @@ The integration also exposes three controller-level entities on the **GREE Contr
- **Thermostat mode** (`select`) — Cooling, Heating or **Do not control**. Do not control pauses house-level thermostat commands but leaves direct device control and explicit per-zone Heat/Cool overrides untouched.
- **Work profile** (`select`) — Auto schedule, Comfort, Sleep or Away. If zones have mixed manual profiles, the select has no single current option until a whole-house profile is chosen again.
- Choosing **Cooling/Heating** in the thermostat-mode select or any whole-house **Work profile** is treated as explicit whole-house activation: it switches **All air conditioners** back on. **Do not control** does not change master power.
- **All air conditioners** (`switch`) — separate master power. Turning it off powers every enabled unit down and prevents zones/controller automations from restarting them. Turning it on powers all enabled units on again without changing the selected thermostat mode or profile.
These controls use the Home Assistant integration token and the dedicated `/api/integrations/home-assistant/house/*` endpoints.
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.6.5",
"version": "0.6.6",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
+1 -1
View File
@@ -534,7 +534,7 @@
"house.noControl": "Do not control",
"house.powerAllEyebrow": "Global power",
"house.powerAllTitle": "All air conditioners",
"house.powerAllHint": "Separate master power for all enabled units. “Turn all off” prevents zones and automations from restarting them without changing the selected thermostat mode.",
"house.powerAllHint": "Master power for all enabled units. “Turn all off” prevents them from restarting. Choosing whole-house Cooling/Heating or an Auto/Comfort/Sleep/Away profile automatically switches master power back on.",
"house.powerOnAll": "Turn all on",
"house.powerOffAll": "Turn all off",
"house.powerOnDone": "All air conditioners turned on",
+1 -1
View File
@@ -534,7 +534,7 @@
"house.noControl": "Nie steruj",
"house.powerAllEyebrow": "Zasilanie globalne",
"house.powerAllTitle": "Wszystkie klimatyzatory",
"house.powerAllHint": "Osobne nadrzędne zasilanie wszystkich aktywnych jednostek. „Wyłącz wszystkie” blokuje ponowne uruchamianie przez strefy i automatyzacje, ale nie zmienia wybranego trybu termostatu.",
"house.powerAllHint": "Nadrzędne zasilanie wszystkich aktywnych jednostek. „Wyłącz wszystkie” blokuje ich ponowne uruchamianie. Globalny wybór Chłodzenia/Grzania albo profilu Auto/Komfort/Sen/Poza domem automatycznie ustawia „Włącz wszystkie”.",
"house.powerOnAll": "Włącz wszystkie",
"house.powerOffAll": "Wyłącz wszystkie",
"house.powerOnDone": "Włączono wszystkie klimatyzatory",
+71 -24
View File
@@ -594,16 +594,55 @@ async fn update_zone_control(State(state): State<AppState>, Path(id): Path<Strin
#[derive(Debug, Deserialize)]
struct HouseControlPatch { mode: String }
async fn command_all_enabled_devices_power(state: &AppState, power: bool, source: &str) -> Result<Vec<Value>, AppError> {
let mut failed = Vec::new();
for device in state.db.list_devices()? {
if !device.enabled || device.power == power { continue; }
let command = DeviceCommand { power: Some(power), ..Default::default() };
if let Err(err) = engine::send_command(state, &device.id, command).await {
state.log("error", "house.power_all_error", &err.to_string(), json!({
"device_id": device.id,
"device_name": device.name,
"power": power,
"source": source,
}));
failed.push(json!({
"device_id": device.id,
"device_name": device.name,
"error": err.to_string(),
}));
}
}
Ok(failed)
}
async fn update_house_control(State(state): State<AppState>, Json(input): Json<HouseControlPatch>) -> Result<Json<Value>, AppError> {
if !matches!(input.mode.as_str(), "cool" | "heat" | "off") {
return Err(AppError::BadRequest("house mode must be cool, heat or off".into()));
}
let mut settings = state.settings.write().await;
settings.house_mode = input.mode;
state.db.save_runtime_settings(&settings)?;
let payload = public_settings(&settings);
let mode = input.mode;
let activate_all = mode != "off";
let payload = {
let mut settings = state.settings.write().await;
settings.house_mode = mode.clone();
// Choosing a real whole-house operating mode is an explicit request to run the
// house climate. It therefore clears a previous global power-off. "off" keeps
// its separate meaning: do not control, without changing master power.
if activate_all { settings.house_power_enabled = true; }
state.db.save_runtime_settings(&settings)?;
public_settings(&settings)
};
state.broadcast("settings.updated", payload.clone());
state.log("info", "house.mode", &format!("House mode set to {}", settings.house_mode), json!({"mode": settings.house_mode}));
if activate_all {
let failed = command_all_enabled_devices_power(&state, true, "house_mode").await?;
if !failed.is_empty() {
state.log("warn", "house.mode_power_partial", "House mode enabled master power, but some devices could not be powered on", json!({
"mode": mode,
"failed": failed.len(),
}));
}
}
state.log("info", "house.mode", &format!("House mode set to {}", mode), json!({"mode": mode, "master_power_enabled": activate_all}));
Ok(Json(payload))
}
@@ -623,23 +662,7 @@ async fn update_house_power(State(state): State<AppState>, Json(input): Json<Hou
}
}
let mut failed = Vec::new();
for device in state.db.list_devices()? {
if !device.enabled || device.power == input.power { continue; }
let command = DeviceCommand { power: Some(input.power), ..Default::default() };
if let Err(err) = engine::send_command(&state, &device.id, command).await {
state.log("error", "house.power_all_error", &err.to_string(), json!({
"device_id": device.id,
"device_name": device.name,
"power": input.power,
}));
failed.push(json!({
"device_id": device.id,
"device_name": device.name,
"error": err.to_string(),
}));
}
}
let failed = command_all_enabled_devices_power(&state, input.power, "house_power").await?;
let devices = state.db.list_devices()?;
let settings = state.settings.read().await;
@@ -664,6 +687,17 @@ async fn update_house_preset(State(state): State<AppState>, Json(input): Json<Ho
if !matches!(input.preset.as_str(), "auto" | "comfort" | "sleep" | "away") {
return Err(AppError::BadRequest("house preset must be auto, comfort, sleep or away".into()));
}
// A whole-house profile is also an explicit whole-house activation. This mirrors
// selecting cooling/heating and makes the separate master-power control intuitive.
let settings_payload = {
let mut settings = state.settings.write().await;
settings.house_power_enabled = true;
state.db.save_runtime_settings(&settings)?;
public_settings(&settings)
};
state.broadcast("settings.updated", settings_payload.clone());
let schedules = state.db.list_schedules()?;
let mut zones = state.db.list_zones()?;
for zone in &mut zones {
@@ -680,8 +714,21 @@ async fn update_house_preset(State(state): State<AppState>, Json(input): Json<Ho
state.db.save_zone(zone)?;
state.broadcast("zone.updated", serde_json::to_value(&*zone)?);
}
state.log("info", "house.preset", &format!("House preset set to {}", input.preset), json!({"preset": input.preset}));
Ok(Json(json!({"preset": input.preset, "zones": zones})))
let failed = command_all_enabled_devices_power(&state, true, "house_preset").await?;
let devices = state.db.list_devices()?;
state.log("info", "house.preset", &format!("House preset set to {}", input.preset), json!({
"preset": input.preset,
"master_power_enabled": true,
"failed": failed.len(),
}));
Ok(Json(json!({
"preset": input.preset,
"zones": zones,
"devices": devices,
"settings": settings_payload,
"failed": failed,
})))
}
#[derive(Debug, Deserialize)]
+7 -1
View File
@@ -1439,7 +1439,13 @@ document.addEventListener('click', async event => {
return;
}
if (action === 'house-preset') {
try { const result=await api('/api/house/preset',{method:'POST',body:{preset:button.dataset.value}}); app.zones=result.zones||app.zones; renderHouseClimate(); renderZones(); scheduleControlPlanLoad(); toast(tr('house.presetUpdated')); }
try {
const result=await api('/api/house/preset',{method:'POST',body:{preset:button.dataset.value}});
app.settings=result.settings||app.settings; app.zones=result.zones||app.zones; app.devices=result.devices||app.devices;
renderAll(); scheduleControlPlanLoad();
const failed=Array.isArray(result.failed)?result.failed.length:0;
if(failed) toast(tr('house.powerPartial',{count:failed}),true); else toast(tr('house.presetUpdated'));
}
catch(error){ toast(error.message,true); } return;
}
if (action === 'zone-temperature') { const zone=app.zones.find(v=>v.id===button.dataset.id); if(zone) { const base=Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint); queueZoneTemperature(zone, base+Number(button.dataset.delta)); } return; }
+2 -2
View File
@@ -37,11 +37,11 @@
<div class="hero-temp" id="heroTemperature">--<small>°C</small></div>
</div>
<div class="metrics" id="metrics"></div>
<div class="panel house-climate" id="houseClimate"></div>
<div class="panel house-power-panel">
<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">Direct power control for all enabled units, independent from thermostat presets.</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 all enabled units. Whole-house mode or profile selection automatically switches it back on.</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>
<div class="panel house-climate" id="houseClimate"></div>
<div class="section-heading"><div><span class="eyebrow" data-i18n="plan.eyebrow">Control plan</span><h2 data-i18n="plan.title">What happens next</h2></div></div>
<div class="automation-plan-grid" id="controlPlan"></div>
<div class="section-heading"><div><span class="eyebrow" data-i18n="nav.zones">Zones</span><h2 data-i18n="dashboard.quickThermostats">Quick thermostats</h2></div></div>
+3 -3
View File
@@ -298,7 +298,7 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; }
.mode-row, .fan-row, .preset-row { scrollbar-width: none; }
.mode-row::-webkit-scrollbar, .fan-row::-webkit-scrollbar, .preset-row::-webkit-scrollbar { display: none; }
.house-climate { display: grid; gap: 16px; margin: -15px 0 12px; }
.house-climate { display: grid; gap: 16px; margin: 0 0 12px; }
.house-climate-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.house-climate-head h3 { margin: 2px 0 4px; font-size: 20px; }
.house-climate-head p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; }
@@ -325,7 +325,7 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; }
.plan-loading { grid-column: 1/-1; padding: 18px; color: var(--muted); }
.house-mode-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.house-mode-row button { min-height: 44px; color: var(--muted); background: var(--surface-muted); }
.house-power-panel { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin: 0 0 34px; }
.house-power-panel { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin: -15px 0 12px; }
.house-power-panel h3 { margin: 2px 0 4px; font-size: 18px; }
.house-power-panel p { margin: 0; max-width: 650px; color: var(--muted); font-size: 12px; line-height: 1.45; }
.house-power-actions { display: flex; flex: 0 0 auto; gap: 8px; }
@@ -360,7 +360,7 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; }
.warning-note { color: var(--warning); }
@media (max-width: 620px) {
.house-climate { margin-top: -20px; }
.house-power-panel { margin-top: -20px; }
.house-climate-head { align-items: flex-start; }
.house-climate-head p { max-width: 220px; }
.house-power-panel { align-items: stretch; flex-direction: column; }
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'gree-controller-v065';
const CACHE = 'gree-controller-v066';
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')];