From 4ab878d587302ce953eee63554abe2a6ef9786b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 24 Aug 2026 16:47:23 +0200 Subject: [PATCH] v0.5.5 --- BUILD_REPORT.md | 9 +- Cargo.lock | 2 +- Cargo.toml | 2 +- FILE_MANIFEST.sha256 | 28 +-- README.md | 10 +- docs/API.md | 2 +- home-assistant/README.md | 2 +- .../gree_controller/manifest.json | 2 +- lang/en.json | 10 +- lang/pl.json | 10 +- src/engine.rs | 13 +- web/app.js | 196 ++++++++++++++---- web/index.html | 88 +++++--- web/styles.css | 9 + web/sw.js | 2 +- 15 files changed, 284 insertions(+), 101 deletions(-) diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md index 5835244..cdc3fe4 100644 --- a/BUILD_REPORT.md +++ b/BUILD_REPORT.md @@ -1,11 +1,16 @@ -# GREE Controller v0.5.4 - build and validation report +# GREE Controller v0.5.5 - build and validation report ## Scope -Version 0.5.4 builds on 0.5.3 with Home Assistant sensor-read fixes, full zone climate mode control, GREE capability discovery and native Sleep support during night mode. +Version 0.5.5 builds on 0.5.4 with dedicated Night mode and Home Assistant/Sensors pages, compact unit-feature controls, and room-temperature refresh for disabled zones. ## Implemented changes +- Night-mode settings moved out of application Settings into a dedicated **Night mode** page. +- Home Assistant connection, sensor aliases and integration access tokens moved to a dedicated **Home Assistant / Sensors** page. +- Settings now contains application/technical settings only. +- Unit-feature controls on device cards are smaller, centered and visually aligned with the other compact device controls. +- Disabled thermostat zones continue refreshing and showing room temperature while control commands remain disabled. - Home Assistant sensor aliases are now presentation-only end-to-end: backend reads resolve aliases back to the original `entity_id`, settings are canonicalized, and zone/outdoor polling records the real entity ID. - Outdoor temperature now falls back to the median outdoor reading from online GREE units when the configured Home Assistant outdoor sensor is unavailable. - GREE optional capabilities are discovered from status properties (`Lig`, `Quiet`, `Tur`, `Air`, `Blo`, `Health`, `SwhSlp`) and persisted with each device. diff --git a/Cargo.lock b/Cargo.lock index e0eb817..85b96e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.5.4" +version = "0.5.5" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 763d324..7d46a4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.5.4" +version = "0.5.5" edition = "2021" authors = ["GREE Controller contributors"] description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support" diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index 70dd9f6..686fa92 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,17 +1,17 @@ e027a3bf37a7917e249ebffb980d43b5e18c70e5931113271975cbeda81a88bd ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -7e0bbddcccea0726bdbf7c93a0fa3a892c1d60279b6da8d992a55a7183522b01 ./BUILD_REPORT.md -ce1d4a2d05be90ffecc39af85cdf5383b08484c3a27482e02f387030075db17e ./Cargo.lock -d9e0c071ee11f26bd8e804e6cff976339982ebca45dad2f03614e147c35df327 ./Cargo.toml +52c2c492893ce42e68dac763533a5070170c21b18d67b302a4e1380a4d4cc4ab ./BUILD_REPORT.md +4202057467ffdc3f53ef250c3e7852a929be788bb0af03fbd2366a57ce42b79e ./Cargo.lock +9edc867d716d9d8ce2c54cdf17e16628f9c2b338ee36f88e0bb0fcb4f7884b57 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -e7b879527c9d1eb7d6bbd054813e709026777e4a79e62b4c8912d2d5aa5e7ff3 ./README.md +44675b0d5d9e6e89dd0bae8eff3ab76a190a3b1bdd6507e7402957bbd92cac36 ./README.md a4fa9bfee9735ed8ed95ea31456e0cce503d82502ae3f550108ffca51b0f0c3d ./build.rs -f00f7475d690694c837e29a618c7737199515bd7a64a5a3be5f44291fe73df0c ./docs/API.md +674f8cf1ab9f1a49188e20b8e3977f1e2b7e9b8ef975315266730c44a55bbb48 ./docs/API.md 234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md 7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md 10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md 47e18e6b1723f1f6eb2516be6d83bbebd31b8155d9d3cdfccc0973177f3d8441 ./docs/PROJECT_SPEC.md -cf889651576a0860c3dc383c255aa011306f82d6287533617c523bcc41caca21 ./home-assistant/README.md +925ffa324b1b2cfad7ca40354e8255548f70627f95c4bb345b957d3f7e53694c ./home-assistant/README.md f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py b8e7c0dd722bfac1d1d8d5c2a4b5ff6b2d6fa89525b9ff5c121ee1d09623a7fd ./home-assistant/custom_components/gree_controller/api.py b0314469ddb7c69ee4a96c677815af30cb97713e93911a05597f2fdc3d10f489 ./home-assistant/custom_components/gree_controller/climate.py @@ -19,7 +19,7 @@ b0314469ddb7c69ee4a96c677815af30cb97713e93911a05597f2fdc3d10f489 ./home-assista 071f721b75af2ed68812c9be3a62b7ae63771e56e567a569f59ca976f8ff42f1 ./home-assistant/custom_components/gree_controller/const.py 8dd51322798cad28d4d19938fe459aa2a4d3b64b075532149f3c8b62049a6c8b ./home-assistant/custom_components/gree_controller/coordinator.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py -b3ac1d74c2f8decdc0dcb9689d3a62bea803a9533164929c09d97922c08c605a ./home-assistant/custom_components/gree_controller/manifest.json +f79cc14c65131fa86a5bf41d55f49b60a3aa71a6d15de98027de830e3b11b2a1 ./home-assistant/custom_components/gree_controller/manifest.json d0206766d76f70ea1c75a618e41bf1e8e8c0429f42ee97f40daac1ea12f16658 ./home-assistant/custom_components/gree_controller/number.py 5c914c2faa18a9cb9eb442dc111c9416f89a630578f820beb021d5381aa1b2d0 ./home-assistant/custom_components/gree_controller/sensor.py 7c23374d1c82e4b361089386ff89265322b80def0f4db9d3fd2c039867033105 ./home-assistant/custom_components/gree_controller/switch.py @@ -27,8 +27,8 @@ d0206766d76f70ea1c75a618e41bf1e8e8c0429f42ee97f40daac1ea12f16658 ./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 -e824a016cff1fa0411ab456b74f3cfe597546072c9e334ef92de69f86a868f48 ./lang/en.json -93d26be77deb820dbce29484db8f2ef6c56b5b05a451c4e9d57013e43aa88395 ./lang/pl.json +eb5370fbee1a8edc11a536ff0e68dba2152767734feb2976fef3cb7cc8c885f4 ./lang/en.json +9e4f43dd809f2d92aca8ce6813e843dbf7e2f38adcfd9cdae584a94e22197fa3 ./lang/pl.json 028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md 3640e34a1ed5a93be3e96610848b16b8e748d5ca95975ac7e95d38cbf32695a8 ./scripts/common.sh @@ -44,7 +44,7 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda ca63498714f3e154fd98c0c2157ab355bf36fc2208d7b3f82c201e153749fb04 ./src/api.rs 31ef3ddbc096e580e9212486683e4db86998322ee87190c61b0cab5ae6bc8fc9 ./src/config.rs a89bc49e359715109149130a10ac60abd26bfd2bc18c5ee5142e01696d9f9cdd ./src/db.rs -c2ac8e8b4acc2bd15e9c41af5a003b7ced4424346617946e387ec57ce9cb2583 ./src/engine.rs +9dfea316b39da6ded85cb9c7ec6f6a61e0cd9a51f40469145782d5766c9cf7a4 ./src/engine.rs ae3b496749a3fd723b243d9bea92e5d76249f52814c80359ad9bac53abacb074 ./src/error.rs 081f6909e95839c7da8c40b866af9de237e0540f3e65e1ce739ce70a6fa94846 ./src/home_assistant.rs 190b0a33431539676e5dd7796698077f16c179d42eae4501ca96a91bf797cbf8 ./src/influxdb.rs @@ -56,9 +56,9 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol d1cba5e55c77d44c75a9c9897ae76c327fd96bdf9c01ad27e34322b689348487 ./src/queries.rs e3c02b232bd9d632ca1d3b6ad69a7cc61dc2e51954765d9e963d713d135f7dca ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service -e82ff244991b968a3dd69f117ce9feeeee6e21c00a257d1d42273283b2d7ff99 ./web/app.js +17342e3aa40a55285f5d056502b232e6aeed41a64ff3e1fdf94c0d8278385ffe ./web/app.js e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -3a7b734e25955f148c5ef0518f6b4abbb04c0a1e660cd58270292fba3bee149f ./web/index.html +1474d31bf8266f6b6dc2dc47225644394259dc6e0afc8e09f0eaddfa93db1d82 ./web/index.html c0e628423a82c5037b6d650aad5e8befe30945492c8cb0a5d94433db7b58c77b ./web/manifest.webmanifest -2d20ba68af2b67b1c57104f0fbb67a0144e2f6aeb12ed8fc9b616a32baa89a3e ./web/styles.css -f75381d719eb299730a85406eb86a58f56fd2ec9bd566bcb3cb84ae6a2d38f87 ./web/sw.js +b8a3cecbde31f9aa4ed3afb5755c62cf98ad90a12faf0a616835170ee889e252 ./web/styles.css +f8bda0083c138828a037469d3db189559190d4b893bcff764a6f9bfd9030b265 ./web/sw.js diff --git a/README.md b/README.md index 5d6ec6b..82b3ac3 100644 --- a/README.md +++ b/README.md @@ -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.5.4**. +Current version: **0.5.5**. ## 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.5.4.zip +unzip gree-controller-v0.5.5.zip cd gree-controller chmod +x scripts/*.sh ./scripts/dev.sh @@ -177,6 +177,8 @@ Use `--skip-tests` with `install.sh` or `update.sh` only when you explicitly wan Settings changed from the web panel are stored in SQLite. Explicit history/debug/Influx environment variables override their persisted values at process startup. Supplying an Influx URL enables the archive unless `GREE_CONTROLLER_INFLUX_ENABLED=false` is explicitly set. `GREE_CONTROLLER_APP_TOKEN` is loaded at process startup. +Web UI organization: **Settings** contains controller/application and technical storage/debug options, while **Night mode** and **Home Assistant / Sensors** are separate pages under More. Disabled zones continue reporting room temperature even though thermostat control is stopped. + ## Climate history History is collected independently for physical GREE devices, control zones, and configured Home Assistant temperature sensors. Device samples continue to use the long-standing `readings` table, so upgrading does not require waiting for a new zone-history table to fill. When rich `zone_readings` are not available yet, the API automatically reconstructs a compatible zone timeline from existing device readings. @@ -234,9 +236,9 @@ There are two independent HA directions: 1. **HA as an optional sensor source**: the Rust controller can read a selected HA sensor and use it for a zone. 2. **HA as a client of GREE Controller**: the included custom integration creates `climate` entities whose commands are sent to this Rust application. -For the HA client connection, open **Settings -> Home Assistant integration access** in GREE Controller and press **Create new token**. The secret is shown once. Paste that token into the Home Assistant `GREE Controller` integration together with the controller URL. Managed HA tokens are stored as SHA-256 hashes and are scoped to the dedicated HA device/control-plan/zone-control API; they cannot change controller settings or manage other tokens. +For the HA client connection, open **More -> Home Assistant / Sensors** in GREE Controller and press **Create new token**. The secret is shown once. Paste that token into the Home Assistant `GREE Controller` integration together with the controller URL. Managed HA tokens are stored as SHA-256 hashes and are scoped to the dedicated HA device/control-plan/zone-control API; they cannot change controller settings or manage other tokens. -The outbound HA sensor client also supports local HTTPS endpoints such as `https://10.87.65.2`. If the server uses a self-signed, expired or hostname-mismatched certificate, enable **Settings -> Allow invalid/self-signed HTTPS certificate**. This disables certificate/hostname validation only for the controller's outbound Home Assistant sensor client and should only be used on a trusted LAN. +The outbound HA sensor client also supports local HTTPS endpoints such as `https://192.168.50.25`. If the server uses a self-signed, expired or hostname-mismatched certificate, enable **Home Assistant / Sensors -> Allow invalid/self-signed HTTPS certificate**. This disables certificate/hostname validation only for the controller's outbound Home Assistant sensor client and should only be used on a trusted LAN. The second option is designed to replace the built-in/default GREE integration without changing automation/dashboard references. diff --git a/docs/API.md b/docs/API.md index 6e91453..414eb29 100644 --- a/docs/API.md +++ b/docs/API.md @@ -10,7 +10,7 @@ BASE='http://127.0.0.1:8787' ## Home Assistant access tokens -Create and revoke integration tokens from the controller Web UI under **Settings -> Home Assistant integration access**. The clear-text secret is returned only once and the SQLite database stores only its SHA-256 hash. +Create and revoke integration tokens from the controller Web UI under **More -> Home Assistant / Sensors**. The clear-text secret is returned only once and the SQLite database stores only its SHA-256 hash. Administrator endpoints: diff --git a/home-assistant/README.md b/home-assistant/README.md index 0722e3a..02c90a1 100644 --- a/home-assistant/README.md +++ b/home-assistant/README.md @@ -27,7 +27,7 @@ Copy the directory into your HA configuration: /config/custom_components/gree_controller/ ``` -Before adding the integration, open the standalone controller Web UI and go to **Settings -> Home Assistant integration access -> Create new token**. Copy the generated secret; it is shown only once. +Before adding the integration, open the standalone controller Web UI and go to **More -> Home Assistant / Sensors -> Create new token**. Copy the generated secret; it is shown only once. Restart Home Assistant, then open **Settings -> Devices & services -> Add integration -> GREE Controller** and enter only: diff --git a/home-assistant/custom_components/gree_controller/manifest.json b/home-assistant/custom_components/gree_controller/manifest.json index 123b3c3..de8d109 100644 --- a/home-assistant/custom_components/gree_controller/manifest.json +++ b/home-assistant/custom_components/gree_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "gree_controller", "name": "GREE Controller", - "version": "0.5.4", + "version": "0.5.5", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/lang/en.json b/lang/en.json index 5aaaf5b..a094df6 100644 --- a/lang/en.json +++ b/lang/en.json @@ -490,6 +490,14 @@ "devices.xfan": "X-FAN", "devices.health": "Health", "devices.air": "Air", - "devices.sleep": "Sleep" + "devices.sleep": "Sleep", + "nav.nightMode": "Night mode", + "nav.homeAssistant": "Home Assistant / Sensors", + "night.eyebrow": "Comfort", + "night.description": "Configure quieter thermostat behavior for sleeping hours without mixing it with application settings.", + "night.saveHint": "Save night-mode changes.", + "homeAssistant.eyebrow": "Integration", + "homeAssistant.description": "Connection, temperature sensors, friendly aliases and access tokens for the Home Assistant integration.", + "homeAssistant.saveHint": "Save Home Assistant and sensor changes." } } diff --git a/lang/pl.json b/lang/pl.json index c41ae9a..284fe5c 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -490,6 +490,14 @@ "devices.xfan": "X-FAN", "devices.health": "Health", "devices.air": "Air", - "devices.sleep": "Sleep" + "devices.sleep": "Sleep", + "nav.nightMode": "Tryb nocny", + "nav.homeAssistant": "Home Assistant / Czujniki", + "night.eyebrow": "Komfort", + "night.description": "Ustaw cichszą pracę termostatu w godzinach nocnych bez mieszania tych opcji z ustawieniami aplikacji.", + "night.saveHint": "Zapisz zmiany trybu nocnego.", + "homeAssistant.eyebrow": "Integracja", + "homeAssistant.description": "Połączenie, czujniki temperatury, przyjazne aliasy i tokeny dostępu dla integracji Home Assistant.", + "homeAssistant.saveHint": "Zapisz ustawienia Home Assistant i czujników." } } diff --git a/src/engine.rs b/src/engine.rs index abf8a92..3a2b044 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -354,8 +354,6 @@ async fn control_zones(state: &AppState) -> Result<()> { let night_active = night_mode_active(&settings.night_mode, Local::now().time()); for mut zone in state.db.list_zones()? { - if !zone.enabled { continue; } - if zone.manual_override_until.map(|until| until <= Utc::now()).unwrap_or(false) { zone.manual_preset = None; zone.manual_setpoint = None; @@ -419,6 +417,17 @@ async fn control_zones(state: &AppState) -> Result<()> { })); } + // Disabled zones still refresh and publish their room temperature. Disabling a + // thermostat stops control actions, but it must not make the room sensor disappear + // from the dashboard or zone view. + if !zone.enabled { + zone.demand = false; + record_zone_history(state, &zone, outdoor_temperature, settings.poll_interval_seconds); + state.db.save_zone(&zone)?; + state.broadcast("zone.updated", serde_json::to_value(&zone)?); + continue; + } + // Global Off is the only normal path that intentionally powers units down. if effective_mode == "off" { zone.active_preset = "off".into(); diff --git a/web/app.js b/web/app.js index 19b9756..cfd4900 100644 --- a/web/app.js +++ b/web/app.js @@ -213,6 +213,8 @@ function renderAll() { renderAccessTokens(); fillSelects(); renderSettings(); + renderNightSettings(); + renderHomeAssistantSettings(); renderDebugOverlay(); } @@ -565,6 +567,7 @@ function zoneCard(zone, detailed = true) { const sensorDetails = zone.sensor_source === 'device' ? `${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)}`; + 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 mode = zone.inherit_house_mode ? 'house' : zone.mode; @@ -574,7 +577,7 @@ function zoneCard(zone, detailed = true) { : ``; return `

${esc(zone.name)}

${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(zone.active_preset))}

${enabledControl}
-
${esc(tr('zones.measurement'))}${fmtTemp(zone.current_temperature)}
${Number.isFinite(target)?target.toFixed(1):'--'}°C
${esc(tr('zones.deviceTarget'))}${fmtTemp(zone.device_setpoint)}
+
${esc(tr('zones.measurement'))}${fmtTemp(roomTemperature)}
${Number.isFinite(target)?target.toFixed(1):'--'}°C
${esc(tr('zones.deviceTarget'))}${fmtTemp(zone.device_setpoint)}
${['auto','comfort','sleep','away'].map(preset=>``).join('')}
@@ -663,6 +666,7 @@ function renderLogRetention() { function renderSettings() { if (!app.settings) return; const form = $('#settingsForm'); + if (!form) return; form.controller_id.value = app.settings.controller_id || ''; form.poll_interval_seconds.value = app.settings.poll_interval_seconds || 15; form.zone_interval_seconds.value = app.settings.zone_interval_seconds || 5; @@ -673,12 +677,6 @@ function renderSettings() { form.event_log_retention_days.value = app.settings.event_log_retention_days || 30; form.history_compaction_enabled.checked = app.settings.history_compaction_enabled !== false; form.suppress_device_beep.checked = !!app.settings.suppress_device_beep; - form.night_mode_enabled.checked = !!app.settings.night_mode?.enabled; - form.night_mode_start.value = app.settings.night_mode?.start_time || '22:00'; - form.night_mode_end.value = app.settings.night_mode?.end_time || '06:00'; - form.night_mode_max_fan_speed.value = String(app.settings.night_mode?.max_fan_speed || 1); - form.night_mode_force_quiet.checked = app.settings.night_mode?.force_quiet !== false; - form.night_mode_native_sleep.checked = app.settings.night_mode?.use_native_sleep !== false; form.influx_enabled.checked = !!app.settings.influxdb?.enabled; form.influx_version.value = String(app.settings.influxdb?.version || '2'); form.influx_threshold_days.value = app.settings.influxdb?.history_threshold_days || 30; @@ -693,6 +691,27 @@ function renderSettings() { form.influx_token.placeholder = app.settings.influxdb?.token_configured ? tr('settings.secretSaved') : tr('settings.secretKeep'); form.debug_overlay_enabled.checked = !!app.settings.debug?.overlay_enabled; form.debug_gree_frames.checked = !!app.settings.debug?.gree_frames; + updateInfluxFields(); + renderLogRetention(); + $('#systemInfo').innerHTML = `

${esc(tr('settings.systemState'))}

${esc(tr('settings.version'))}: ${esc(app.system.version || '—')}
${esc(tr('settings.uptime'))}: ${esc(formatDuration(app.system.uptime_seconds || 0))}
${esc(tr('settings.apiAuth'))}: ${esc(app.system.auth_required ? tr('settings.enabled') : tr('settings.disabled'))}
`; +} + +function renderNightSettings() { + if (!app.settings) return; + const form = $('#nightModeForm'); + if (!form) return; + form.night_mode_enabled.checked = !!app.settings.night_mode?.enabled; + form.night_mode_start.value = app.settings.night_mode?.start_time || '22:00'; + form.night_mode_end.value = app.settings.night_mode?.end_time || '06:00'; + form.night_mode_max_fan_speed.value = String(app.settings.night_mode?.max_fan_speed || 1); + form.night_mode_force_quiet.checked = app.settings.night_mode?.force_quiet !== false; + form.night_mode_native_sleep.checked = app.settings.night_mode?.use_native_sleep !== false; +} + +function renderHomeAssistantSettings() { + if (!app.settings) return; + const form = $('#homeAssistantForm'); + if (!form) return; form.ha_url.value = app.settings.home_assistant?.url || ''; form.ha_token.value = ''; form.ha_token.placeholder = app.settings.home_assistant?.token_configured ? tr('settings.haTokenSaved') : tr('settings.haLongLivedToken'); @@ -700,10 +719,8 @@ function renderSettings() { form.ha_outdoor_entity_id.value = app.settings.home_assistant?.outdoor_entity_id || ''; form.ha_allow_invalid_tls.checked = !!app.settings.home_assistant?.allow_invalid_tls; form.outdoor_assist_enabled.checked = !!app.settings.outdoor_assist_enabled; - updateInfluxFields(); renderSensorAliases(); - renderLogRetention(); - $('#systemInfo').innerHTML = `

${esc(tr('settings.systemState'))}

${esc(tr('settings.version'))}: ${esc(app.system.version || '—')}
${esc(tr('settings.uptime'))}: ${esc(formatDuration(app.system.uptime_seconds || 0))}
${esc(tr('settings.apiAuth'))}: ${esc(app.system.auth_required ? tr('settings.enabled') : tr('settings.disabled'))}
`; + renderAccessTokens(); } function updateInfluxFields() { @@ -747,7 +764,7 @@ function formatDuration(seconds) { return `${days ? `${days}d ` : ''}${hours}h ${minutes}m`; } -const VIEW_ROUTES = {dashboard:'/dashboard', devices:'/devices', zones:'/zones', schedules:'/schedules', automations:'/automations', simulation:'/simulation', settings:'/settings', logs:'/events'}; +const VIEW_ROUTES = {dashboard:'/dashboard', devices:'/devices', zones:'/zones', schedules:'/schedules', automations:'/automations', simulation:'/simulation', night:'/night-mode', homeassistant:'/home-assistant', settings:'/settings', logs:'/events'}; const HISTORY_TABS = ['overview','zones','devices','sensors','custom']; function currentHistoryPath() { @@ -772,7 +789,7 @@ function updateBrowserUrl(path, replace=false) { function showView(name, {push=true, scroll=true}={}) { app.currentView = name; $$('.view').forEach(view => view.classList.toggle('active', view.dataset.view === name)); - $$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['schedules','automations','simulation','settings','logs'].includes(name)))); + $$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['schedules','automations','simulation','night','homeassistant','settings','logs'].includes(name)))); if (push) updateBrowserUrl(name === 'history' ? currentHistoryPath() : (VIEW_ROUTES[name] || '/dashboard')); if (scroll) window.scrollTo({top: 0, behavior: 'smooth'}); if (name === 'history') { renderHistoryNavigation(); loadHistory(); } @@ -1302,7 +1319,7 @@ function connectWebSocket() { else if (message.event === 'device.deleted') { app.devices=app.devices.filter(v=>v.id!==data.id); renderAll(); scheduleControlPlanLoad(); } else if (message.event === 'devices.discovered') { (data.devices||[]).forEach(updateDevice); renderAll(); scheduleControlPlanLoad(); } else if (message.event === 'zone.updated') { const i=app.zones.findIndex(v=>v.id===data.id); if(i>=0) app.zones[i]=data; else app.zones.push(data); renderSummary(); renderHouseClimate(); renderZones(); scheduleControlPlanLoad(); } - else if (message.event === 'settings.updated') { app.settings=data; app.sensorAliases={...(app.settings?.home_assistant?.sensor_aliases||{})}; renderSettings(); renderHouseClimate(); renderDebugOverlay(); if(app.settings?.debug?.overlay_enabled) loadDebugBacklog(); scheduleControlPlanLoad(); } + else if (message.event === 'settings.updated') { app.settings=data; app.sensorAliases={...(app.settings?.home_assistant?.sensor_aliases||{})}; renderSettings(); renderNightSettings(); renderHomeAssistantSettings(); renderHouseClimate(); renderDebugOverlay(); if(app.settings?.debug?.overlay_enabled) loadDebugBacklog(); scheduleControlPlanLoad(); } else if (message.event === 'debug.settings') { app.settings = app.settings || {}; app.settings.debug = data; renderSettings(); renderDebugOverlay(); if(data.overlay_enabled) loadDebugBacklog(); } else if (message.event === 'outdoor.updated') { app.outdoorTemperature=Number.isFinite(Number(data.temperature))?Number(data.temperature):null; renderHouseClimate(); scheduleControlPlanLoad(); } else if (message.event === 'gree.frame') { if(app.settings?.debug?.overlay_enabled) debugLine('GREE', `${data.direction || '?'} ${data.protocol_version || ''}`, data.device_name || data.device_id || data.target || '', message.timestamp, data.payload); } @@ -1463,38 +1480,141 @@ $('#automationForm').addEventListener('submit', async event => { catch(error){toast(error.message,true);} }); -function settingsBodyFromForm(form) { - const raw=Object.fromEntries(new FormData(form)); +function currentSettingsBody() { + const settings = app.settings || {}; + const influx = settings.influxdb || {}; + const ha = settings.home_assistant || {}; return { - controller_id:raw.controller_id, simulator_enabled:form.simulator_enabled.checked, - poll_interval_seconds:Number(raw.poll_interval_seconds), zone_interval_seconds:Number(raw.zone_interval_seconds), - discovery_timeout_ms:Number(raw.discovery_timeout_ms), discovery_broadcast:raw.discovery_broadcast, - house_mode:app.settings?.house_mode||'cool', control_strategy:'setpoint', outdoor_assist_enabled:form.outdoor_assist_enabled.checked, - history_retention_days:Number(raw.history_retention_days), history_compaction_enabled:form.history_compaction_enabled.checked, - event_log_retention_days:Number(raw.event_log_retention_days), - suppress_device_beep:form.suppress_device_beep.checked, - night_mode:{ - enabled:form.night_mode_enabled.checked,start_time:raw.night_mode_start,end_time:raw.night_mode_end, - max_fan_speed:Number(raw.night_mode_max_fan_speed),force_quiet:form.night_mode_force_quiet.checked, - use_native_sleep:form.night_mode_native_sleep.checked, + controller_id: settings.controller_id || 'gree-controller', + simulator_enabled: !!settings.simulator_enabled, + poll_interval_seconds: Number(settings.poll_interval_seconds || 15), + zone_interval_seconds: Number(settings.zone_interval_seconds || 5), + discovery_timeout_ms: Number(settings.discovery_timeout_ms || 3000), + discovery_broadcast: settings.discovery_broadcast || '255.255.255.255:7000', + house_mode: settings.house_mode || 'cool', + control_strategy: 'setpoint', + outdoor_assist_enabled: !!settings.outdoor_assist_enabled, + history_retention_days: Number(settings.history_retention_days || 30), + history_compaction_enabled: settings.history_compaction_enabled !== false, + event_log_retention_days: Number(settings.event_log_retention_days || 30), + suppress_device_beep: !!settings.suppress_device_beep, + night_mode: { + enabled: !!settings.night_mode?.enabled, + start_time: settings.night_mode?.start_time || '22:00', + end_time: settings.night_mode?.end_time || '06:00', + max_fan_speed: Number(settings.night_mode?.max_fan_speed || 1), + force_quiet: settings.night_mode?.force_quiet !== false, + use_native_sleep: settings.night_mode?.use_native_sleep !== false, }, - influxdb:{ - enabled:form.influx_enabled.checked, version:raw.influx_version, url:raw.influx_url, - database:raw.influx_database, username:raw.influx_username, password:raw.influx_password, - org:raw.influx_org, bucket:raw.influx_bucket, token:raw.influx_token, - history_threshold_days:Number(raw.influx_threshold_days), + influxdb: { + enabled: !!influx.enabled, + version: String(influx.version || '2'), + url: influx.url || '', + database: influx.database || 'gree_controller', + username: influx.username || '', + password: '', + org: influx.org || '', + bucket: influx.bucket || 'gree_controller', + token: '', + history_threshold_days: Number(influx.history_threshold_days || 30), + }, + debug: { + overlay_enabled: !!settings.debug?.overlay_enabled, + gree_frames: !!settings.debug?.gree_frames, + }, + home_assistant: { + url: ha.url || '', + token: '', + default_entity_id: ha.default_entity_id || '', + outdoor_entity_id: ha.outdoor_entity_id || '', + allow_invalid_tls: !!ha.allow_invalid_tls, + sensor_aliases: {...(ha.sensor_aliases || {})}, }, - debug:{overlay_enabled:form.debug_overlay_enabled.checked, gree_frames:form.debug_gree_frames.checked}, - home_assistant:{url:raw.ha_url,token:raw.ha_token,default_entity_id:raw.ha_entity_id,outdoor_entity_id:raw.ha_outdoor_entity_id,allow_invalid_tls:form.ha_allow_invalid_tls.checked,sensor_aliases:{...(app.sensorAliases||{})}}, }; } -async function saveSettingsForm(form, notify=true) { - app.settings=await api('/api/settings',{method:'PUT',body:settingsBodyFromForm(form)}); app.sensorAliases={...(app.settings?.home_assistant?.sensor_aliases||{})}; renderSettings(); renderHouseClimate(); renderDebugOverlay(); scheduleControlPlanLoad(); if(app.settings?.debug?.overlay_enabled) loadDebugBacklog(); if(notify) toast(tr('common.saved')); return app.settings; +function settingsBodyFromForm(form) { + const raw = Object.fromEntries(new FormData(form)); + const body = currentSettingsBody(); + body.controller_id = raw.controller_id; + body.simulator_enabled = form.simulator_enabled.checked; + body.poll_interval_seconds = Number(raw.poll_interval_seconds); + body.zone_interval_seconds = Number(raw.zone_interval_seconds); + body.discovery_timeout_ms = Number(raw.discovery_timeout_ms); + body.discovery_broadcast = raw.discovery_broadcast; + body.history_retention_days = Number(raw.history_retention_days); + body.history_compaction_enabled = form.history_compaction_enabled.checked; + body.event_log_retention_days = Number(raw.event_log_retention_days); + body.suppress_device_beep = form.suppress_device_beep.checked; + body.influxdb = { + enabled: form.influx_enabled.checked, version: raw.influx_version, url: raw.influx_url, + database: raw.influx_database, username: raw.influx_username, password: raw.influx_password, + org: raw.influx_org, bucket: raw.influx_bucket, token: raw.influx_token, + history_threshold_days: Number(raw.influx_threshold_days), + }; + body.debug = {overlay_enabled: form.debug_overlay_enabled.checked, gree_frames: form.debug_gree_frames.checked}; + return body; +} + +function nightSettingsBodyFromForm(form) { + const raw = Object.fromEntries(new FormData(form)); + const body = currentSettingsBody(); + body.night_mode = { + enabled: form.night_mode_enabled.checked, + start_time: raw.night_mode_start, + end_time: raw.night_mode_end, + max_fan_speed: Number(raw.night_mode_max_fan_speed), + force_quiet: form.night_mode_force_quiet.checked, + use_native_sleep: form.night_mode_native_sleep.checked, + }; + return body; +} + +function homeAssistantSettingsBodyFromForm(form) { + const raw = Object.fromEntries(new FormData(form)); + const body = currentSettingsBody(); + body.outdoor_assist_enabled = form.outdoor_assist_enabled.checked; + body.home_assistant = { + url: raw.ha_url, + token: raw.ha_token, + default_entity_id: raw.ha_entity_id, + outdoor_entity_id: raw.ha_outdoor_entity_id, + allow_invalid_tls: form.ha_allow_invalid_tls.checked, + sensor_aliases: {...(app.sensorAliases || {})}, + }; + return body; +} + +async function saveRuntimeSettings(body, notify = true) { + app.settings = await api('/api/settings', {method:'PUT', body}); + app.sensorAliases = {...(app.settings?.home_assistant?.sensor_aliases || {})}; + renderSettings(); + renderNightSettings(); + renderHomeAssistantSettings(); + renderHouseClimate(); + renderDebugOverlay(); + scheduleControlPlanLoad(); + if (app.settings?.debug?.overlay_enabled) loadDebugBacklog(); + if (notify) toast(tr('common.saved')); + return app.settings; } $('#settingsForm').addEventListener('submit', async event => { - event.preventDefault(); try { await saveSettingsForm(event.currentTarget, true); } catch(error){toast(error.message,true);} + event.preventDefault(); + try { await saveRuntimeSettings(settingsBodyFromForm(event.currentTarget), true); } + catch(error) { toast(error.message, true); } +}); + +$('#nightModeForm')?.addEventListener('submit', async event => { + event.preventDefault(); + try { await saveRuntimeSettings(nightSettingsBodyFromForm(event.currentTarget), true); } + catch(error) { toast(error.message, true); } +}); + +$('#homeAssistantForm')?.addEventListener('submit', async event => { + event.preventDefault(); + try { await saveRuntimeSettings(homeAssistantSettingsBodyFromForm(event.currentTarget), true); } + catch(error) { toast(error.message, true); } }); $('#addSensorAlias')?.addEventListener('click', () => { @@ -1555,9 +1675,9 @@ $('#copyAccessToken').addEventListener('click', async () => { }); $('#haTest').addEventListener('click', async () => { - const form=$('#settingsForm'); + const form=$('#homeAssistantForm'); try { - await saveSettingsForm(form, false); + await saveRuntimeSettings(homeAssistantSettingsBodyFromForm(form), false); const entity = form.ha_entity_id.value || form.ha_outdoor_entity_id.value || null; const result=await api('/api/integrations/home-assistant/test',{method:'POST',body:{entity_id:entity}}); toast(tr('toast.haTemperature',{temperature:result.temperature_c.toFixed(1)})); diff --git a/web/index.html b/web/index.html index 3335195..5438598 100644 --- a/web/index.html +++ b/web/index.html @@ -135,6 +135,56 @@
+ +
+
Comfort

Night mode

+

Configure quieter thermostat behavior for sleeping hours without mixing it with application settings.

+
+
+

Night mode

During selected hours the thermostat limits fan speed and can request Quiet on supported units.

+
+ + + + + + +
+
+
Save night-mode changes.
+
+
+ +
+
Integration

Home Assistant / Sensors

+

Connection, temperature sensors, friendly aliases and access tokens for the Home Assistant integration.

+
+
+

Home Assistant sensor input

Optional. Used when a room zone reads an external Home Assistant temperature sensor.

+
+ + + + + + +

Use only for a trusted local Home Assistant server, for example https://192.168.50.25.

+
+
Sensor aliasesFriendly names are used in charts and selectors; the original entity_id remains unchanged.
+
+
+
+
+
+
+
+

Home Assistant integration access

Create a controller token and paste it into the GREE Controller integration in Home Assistant.

+
+
+
Save Home Assistant and sensor changes.
+
+
+
System

Settings

@@ -150,17 +200,7 @@
-
-

Night mode

During selected hours the thermostat limits fan speed and can request Quiet on supported units.

-
- - - - - - -
-
+

Metrics and logs

SQLite keeps recent data locally and compacts older samples to the resolution used by charts.

@@ -204,29 +244,9 @@
-
-

Home Assistant sensor input

Optional. Used when a room zone reads an external Home Assistant temperature sensor.

-
- - - - - - -

Use only for a trusted local Home Assistant server, for example https://192.168.50.25.

-
-
Sensor aliasesFriendly names are used in charts and selectors; the original entity_id remains unchanged.
-
-
-
-
-
-
+ -
-

Home Assistant integration access

Create a controller token and paste it into the GREE Controller integration in Home Assistant.

-
-
+

Configuration backup

Export/import application configuration. Exported files can contain GREE device keys plus Home Assistant and InfluxDB secrets; metrics and API access tokens are not included.

@@ -259,6 +279,8 @@ + + diff --git a/web/styles.css b/web/styles.css index b71115e..39fcd6b 100644 --- a/web/styles.css +++ b/web/styles.css @@ -612,3 +612,12 @@ html[data-theme='light'] .simulation-metrics > div, html[data-theme='light'] .si .device-capability-buttons { display:flex; flex-wrap:wrap; gap:7px; } .device-capability-buttons button { min-height:34px; padding:7px 11px; border:1px solid var(--line); border-radius:11px; background:var(--surface-muted); color:var(--text); } .device-capability-buttons button.active { border-color:var(--accent); color:var(--accent); } + +/* v0.5.5: compact, centered unit-feature controls */ +.device-capability-panel { justify-items:center; text-align:center; padding:11px 0 3px; } +.device-capability-panel > small { text-align:center; } +.device-capability-buttons { justify-content:center; align-items:center; gap:6px; max-width:100%; } +.device-capability-buttons button { min-height:28px; padding:5px 9px; border-radius:10px; color:var(--muted); font-size:10px; line-height:1.1; background:transparent; } +.device-capability-buttons button:hover { color:var(--text); background:var(--surface-muted); } +.device-capability-buttons button.active { color:var(--accent); background:color-mix(in srgb, var(--accent) 12%, var(--surface)); border-color:color-mix(in srgb, var(--accent) 45%, var(--line)); } +.standalone-settings-form { max-width:1100px; } diff --git a/web/sw.js b/web/sw.js index a17f562..ef50b71 100644 --- a/web/sw.js +++ b/web/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'gree-controller-v054'; +const CACHE = 'gree-controller-v055'; const ASSETS = ['/', '/styles.css', '/app.js', '/favicon.svg', '/manifest.webmanifest', '/lang/index.json', '/lang/en.json']; self.addEventListener('install', event => event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(ASSETS)).then(() => self.skipWaiting()))); self.addEventListener('activate', event => event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))).then(() => self.clients.claim())));