From f8ff19f90c43439606bd2c7ebf05d0704ad2d46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 24 Aug 2026 23:29:05 +0200 Subject: [PATCH] v0.6.2 --- BUILD_REPORT.md | 12 +++- Cargo.lock | 2 +- Cargo.toml | 2 +- FILE_MANIFEST.sha256 | 20 +++--- README.md | 4 +- .../gree_controller/manifest.json | 2 +- lang/en.json | 4 +- lang/pl.json | 4 +- web/app.js | 69 +++++++++++++++++-- web/styles.css | 2 +- web/sw.js | 2 +- 11 files changed, 96 insertions(+), 27 deletions(-) diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md index 8f2e3a1..998941e 100644 --- a/BUILD_REPORT.md +++ b/BUILD_REPORT.md @@ -1,8 +1,8 @@ -# GREE Controller v0.6.1 - build and validation report +# GREE Controller v0.6.2 - build and validation report ## Scope -Version 0.6.1 includes the notification, reverse-proxy, mobile decimal-input, log filtering and thermostat-copy changes described below, plus a Rust type-safety hotfix for communication-failure notifications. +Version 0.6.2 includes the notification, reverse-proxy, mobile decimal-input, log filtering and thermostat-copy changes described below, plus a Rust type-safety hotfix for communication-failure notifications. ## Implemented changes @@ -99,3 +99,11 @@ Validation performed in the build environment: - `bash -n scripts/install.sh` - passed. - Home Assistant icon files generated and verified as 256x256 RGBA PNG. - Rust compilation was not executed because this execution environment does not contain `cargo`/Rust toolchain. Backend changes were therefore statically reviewed but require the normal project CI/build host to run `cargo test`/`cargo build --release` before deployment. + + +## 0.6.2 thermostat copy hotfix + +- The destination zone identity is now stored independently from the form payload while editing. +- Copy-source choices exclude the currently edited zone. +- Copying thermostat settings explicitly preserves the destination zone ID, name, assigned device, sensor strategy/entity and enabled state. +- Zone updates use the independently tracked destination ID, preventing the source profile from being overwritten by a stale or mutated form ID. diff --git a/Cargo.lock b/Cargo.lock index 842dd57..ae2fc64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.6.1" +version = "0.6.2" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 16f01fa..345c59f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.6.1" +version = "0.6.2" 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 7117c20..c4b3370 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,10 +1,10 @@ 623bd769512ef75f51e9bf7fb685d4d59ec84d7ccc75c826fe72c5538be737c5 ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -110e07efa26849462b6f5064f9bbadfc477301390b2303a2ff3dd205f41ef973 ./BUILD_REPORT.md -24efad1c04de0afd647ddb9a6a7d70b3c1e31b91de28dc8643d6be688a5db5e6 ./Cargo.lock -4d03ac4ec8bdf303f4b58c02576fe71f1a1cca8a23f383c08a2a3dcdfc4d2176 ./Cargo.toml +2f89ebf1e9b0dfe837ee4efa6f436370dd28a08b603294ef3f20b869fdc44d8d ./BUILD_REPORT.md +13acfef8c5510afc206dc7bc590e76149d9e6dedc7414c3165e20653992d81b0 ./Cargo.lock +59edac9d91b203c2a65168afbcc81ff5fe6e95c05f9f4cbf9c6aa6fb9f8b6a48 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -39a7ebd7b27b4b1b026ed56becbccfb524822234ecfa68423b68a3bc5659d768 ./README.md +62d29996813c32f86731a5e13635eaa41f698fdba8030ebb42259ce5588c5148 ./README.md f2c8eb28fedb26bb8f5636e1805e7a782cc472b87d882f6752d520f16216a248 ./build.rs 674f8cf1ab9f1a49188e20b8e3977f1e2b7e9b8ef975315266730c44a55bbb48 ./docs/API.md 234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md @@ -23,7 +23,7 @@ b0314469ddb7c69ee4a96c677815af30cb97713e93911a05597f2fdc3d10f489 ./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 -d1873870df2388f43f9cfd8a2e617ca394eac53d93e2eb8cb85df4d3b6a9caa2 ./home-assistant/custom_components/gree_controller/manifest.json +9d0b7de890d62245211e044801b00a6326f208aab1c016ec490b269afcb7c61b ./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 @@ -31,8 +31,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 -ab2f7193ad789a286f0a0ba23bd53b9816eb469dcfdf0860033543760b942bf3 ./lang/en.json -510bd3303d99cea8fbf10261dfbf0351da136d8844b7225f746be8b5b187e19e ./lang/pl.json +883fc8f52e9d79d41a7c75ca18edccd344ac510e0a36027b76467908ede1c198 ./lang/en.json +4d711b95cb86ad8710ed74bc7f4b49888cb82ac592e18d83b9b7e17def0bc029 ./lang/pl.json 028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh @@ -61,10 +61,10 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol d1cba5e55c77d44c75a9c9897ae76c327fd96bdf9c01ad27e34322b689348487 ./src/queries.rs f74ac398c6f2e64ebb965900330324f3ec7403e70339638a408229f29fbd07ae ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service -ef02fe53df75af12cb957e15b945543eeba937a13154a1fc6c5234ee6f25b435 ./web/app.js +ccb5bfc99a59e3b0530eea39beed95a56fdeffdd460d616ea64103647d292c55 ./web/app.js e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg 002c172392a00006a5128fc4cb881b0361a2d3e76298dbd80c0a013f4e1f9dde ./web/index.html fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest -87148c295a8d6ab7017d10dde25528f2d3bcbc50ca15dc2e0869307e0b1604de ./web/styles.css -64355a48ea25c86b6fceae31f9d1e79a7f71cde35a055a001133c91f220b939f ./web/sw.js +ad68c73fc9e29c39c844a9cae38f7a50d96ad09020b81219b2a1f6daf3f635bc ./web/styles.css +639c2898baa081dda54216b5000de4e5449d7aec1acc4f45df846709edfb64c3 ./web/sw.js d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js diff --git a/README.md b/README.md index f4707be..cf62c98 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.6.1**. +Current version: **0.6.2**. ## 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.1.zip +unzip gree-controller-v0.6.2.zip cd gree-controller chmod +x scripts/*.sh ./scripts/dev.sh diff --git a/home-assistant/custom_components/gree_controller/manifest.json b/home-assistant/custom_components/gree_controller/manifest.json index c5249d3..9bb7a0f 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.6.1", + "version": "0.6.2", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/lang/en.json b/lang/en.json index 1c45d49..e5f43f4 100644 --- a/lang/en.json +++ b/lang/en.json @@ -528,6 +528,8 @@ "logs.errors": "Errors", "logs.warnings": "Warnings", "logs.integrations": "Integrations", - "logs.systemOther": "System/other" + "logs.systemOther": "System/other", + "zones.copyDifferent": "Choose a different thermostat profile", + "zones.copiedFrom": "Thermostat settings copied from {name}" } } diff --git a/lang/pl.json b/lang/pl.json index 1c12757..bf46f04 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -528,6 +528,8 @@ "logs.errors": "Błędy", "logs.warnings": "Ostrzeżenia", "logs.integrations": "Integracje", - "logs.systemOther": "System/inne" + "logs.systemOther": "System/inne", + "zones.copyDifferent": "Wybierz inny profil termostatu", + "zones.copiedFrom": "Skopiowano ustawienia termostatu z: {name}" } } diff --git a/web/app.js b/web/app.js index a5b1d9b..a64d4e4 100644 --- a/web/app.js +++ b/web/app.js @@ -632,7 +632,14 @@ function fillSelects() { if (zoneSelect) { const currentZone = zoneSelect.value; zoneSelect.innerHTML = zoneOptions; if ([...zoneSelect.options].some(o => o.value === currentZone)) zoneSelect.value = currentZone; } const presetZone = $('#schedulePresetZone'); if (presetZone) { const currentZone = presetZone.value; presetZone.innerHTML = zoneOptions; if ([...presetZone.options].some(o => o.value === currentZone)) presetZone.value = currentZone; } - const copyZone=$('#copyZoneSource'); if(copyZone){ const current=copyZone.value; copyZone.innerHTML=''+app.zones.map(z=>``).join(''); if([...copyZone.options].some(o=>o.value===current))copyZone.value=current; } + const copyZone=$('#copyZoneSource'); + if(copyZone){ + const current=copyZone.value; + const editingZoneId=$('#zoneForm')?.dataset.editingZoneId || ''; + const sources=app.zones.filter(zone=>zone.id!==editingZoneId); + copyZone.innerHTML=``+sources.map(z=>``).join(''); + copyZone.value=[...copyZone.options].some(option=>option.value===current)?current:''; + } } function renderAccessTokens() { @@ -928,8 +935,12 @@ function updateSchedulePresetField() { function populateZone(id) { const item = app.zones.find(v => v.id === id); if (!item) return; - const form = $('#zoneForm'); form.reset(); fillSelects(); - Object.entries(item).forEach(([key,value]) => { if (form.elements[key] && value != null && typeof value !== 'object') form.elements[key].value = value; }); + const form = $('#zoneForm'); + form.reset(); + form.dataset.editingZoneId = item.id; + form.elements.id.value = item.id; + fillSelects(); + Object.entries(item).forEach(([key,value]) => { if (key !== 'id' && form.elements[key] && value != null && typeof value !== 'object') form.elements[key].value = value; }); form.mode_policy.value = item.inherit_house_mode ? 'house' : (item.mode || 'cool'); if (Number(item.profile_version || 0) === 0) { if ((item.mode || 'cool') === 'heat') form.heat_comfort_setpoint.value = Number(item.setpoint ?? 21); @@ -1360,7 +1371,17 @@ document.addEventListener('click', async event => { return; } if (button.dataset.go) { $('#moreDialog').close(); showView(button.dataset.go); return; } - if (button.dataset.open) { const form = document.getElementById(button.dataset.open.replace('Dialog','Form')); if (form) form.reset(); if (button.dataset.open === 'zoneDialog') updateZoneSensorFields(); if (button.dataset.open === 'scheduleDialog') updateSchedulePresetField(); openDialog(button.dataset.open); return; } + if (button.dataset.open) { + const form = document.getElementById(button.dataset.open.replace('Dialog','Form')); + if (form) form.reset(); + if (button.dataset.open === 'zoneDialog') { + if (form) { form.dataset.editingZoneId = ''; if (form.elements.id) form.elements.id.value = ''; } + updateZoneSensorFields(); + } + if (button.dataset.open === 'scheduleDialog') updateSchedulePresetField(); + openDialog(button.dataset.open); + return; + } if (button.hasAttribute('data-close')) { button.closest('dialog')?.close(); return; } if (button.dataset.historyTab) { showHistoryTab(button.dataset.historyTab); return; } if (button.dataset.historyAction) { await handleHistoryAction(button); return; } @@ -1434,7 +1455,43 @@ $('#scheduleForm [name=preset]').addEventListener('change', updateSchedulePreset $('#testNotifications')?.addEventListener('click', async () => { const f=$('#settingsForm'); const saved=app.settings?.notifications||{}; const body={enabled:true,mode:f.notifications_mode.value,provider:f.notifications_provider.value,pushover_app_token:f.pushover_app_token.value.trim(),pushover_user_key:f.pushover_user_key.value.trim(),slack_webhook_url:f.slack_webhook_url.value.trim(),discord_webhook_url:f.discord_webhook_url.value.trim(),cooldown_seconds:Number(f.notification_cooldown_seconds.value||300),communication_failure_threshold:Number(f.notification_failure_threshold.value||3),target_timeout_minutes:Number(f.notification_target_timeout.value||60)}; try{await api('/api/integrations/notifications/test',{method:'POST',body});toast('Test notification sent');}catch(e){toast(e.message,true);} }); -$('#copyZoneSettings')?.addEventListener('click',()=>{ const source=app.zones.find(z=>z.id===$('#copyZoneSource')?.value); if(!source)return; const f=$('#zoneForm'); ['setpoint','cool_comfort_setpoint','cool_sleep_setpoint','cool_away_setpoint','heat_comfort_setpoint','heat_sleep_setpoint','heat_away_setpoint','hysteresis','min_on_seconds','min_off_seconds','min_adjust_seconds','standby_offset_c','external_sensor_weight_percent','max_sensor_difference'].forEach(name=>{ if(!f[name])return; const value=name==='external_sensor_weight_percent'?(source.external_sensor_weight*100):source[name]; if(value!==undefined&&value!==null)f[name].value=String(value); }); f.smart_fan.checked=source.smart_fan!==false; if(f.mode_policy) f.mode_policy.value=source.inherit_house_mode===false?(source.mode||'cool'):'house'; toast('Thermostat settings copied'); }); +$('#copyZoneSettings')?.addEventListener('click',()=>{ + const f=$('#zoneForm'); + const source=app.zones.find(zone=>zone.id===$('#copyZoneSource')?.value); + if(!source)return; + const targetId=f.dataset.editingZoneId || ''; + if(targetId && source.id===targetId){ toast(tr('zones.copyDifferent'),true); return; } + + // Identity and sensor assignment belong to the destination profile and must never + // be changed by copying thermostat tuning from another profile. + const protectedValues={ + id:f.elements.id.value, + name:f.elements.name.value, + device_id:f.elements.device_id.value, + sensor_source:f.elements.sensor_source.value, + ha_entity_id:f.elements.ha_entity_id.value, + enabled:f.elements.enabled.checked, + }; + const copyFields=['setpoint','cool_comfort_setpoint','cool_sleep_setpoint','cool_away_setpoint','heat_comfort_setpoint','heat_sleep_setpoint','heat_away_setpoint','hysteresis','min_on_seconds','min_off_seconds','min_adjust_seconds','standby_offset_c','external_sensor_weight_percent','max_sensor_difference']; + copyFields.forEach(name=>{ + const input=f.elements[name]; + if(!input)return; + const value=name==='external_sensor_weight_percent'?(source.external_sensor_weight*100):source[name]; + if(value!==undefined&&value!==null)input.value=String(value); + }); + f.elements.smart_fan.checked=source.smart_fan!==false; + if(f.elements.mode_policy) f.elements.mode_policy.value=source.inherit_house_mode===false?(source.mode||'cool'):'house'; + + f.elements.id.value=protectedValues.id; + f.elements.name.value=protectedValues.name; + f.elements.device_id.value=protectedValues.device_id; + f.elements.sensor_source.value=protectedValues.sensor_source; + f.elements.ha_entity_id.value=protectedValues.ha_entity_id; + f.elements.enabled.checked=protectedValues.enabled; + f.dataset.editingZoneId=targetId; + updateZoneSensorFields(); + toast(tr('zones.copiedFrom',{name:source.name})); +}); $('#applyAutomationPreset')?.addEventListener('click',()=>{ const f=$('#automationForm'), p=$('#automationPreset')?.value; if(!p)return; const first=app.devices[0]?.id||''; const presets={hot:{name:'High temperature cooling',trigger_kind:'temperature_above',threshold:'28',action_power:'true',action_mode:'cool',action_target_temperature:'23',cooldown_seconds:'900'},cold:{name:'Low temperature heating',trigger_kind:'temperature_below',threshold:'17',action_power:'true',action_mode:'heat',action_target_temperature:'21',cooldown_seconds:'900'},morning:{name:'Morning comfort',trigger_kind:'time',at_time:'07:00',action_power:'true',action_mode:'auto',action_target_temperature:'22',cooldown_seconds:'3600'},nightoff:{name:'Night power off',trigger_kind:'time',at_time:'23:30',action_power:'false',action_mode:'',action_target_temperature:'',cooldown_seconds:'3600'}}; const x=presets[p]; Object.entries(x).forEach(([k,v])=>{if(f[k])f[k].value=v}); if(!f.trigger_device_id.value)f.trigger_device_id.value=first; if(!f.action_device_id.value)f.action_device_id.value=first; }); @@ -1482,7 +1539,7 @@ $('#deviceForm').addEventListener('submit', async event => { $('#zoneForm').addEventListener('submit', async event => { event.preventDefault(); const form=event.currentTarget, raw=Object.fromEntries(new FormData(form)); - const id=raw.id; const body={ + const id=form.dataset.editingZoneId || raw.id; const body={ name:raw.name,device_id:raw.device_id,enabled:form.enabled.checked, mode:raw.mode_policy==='house'?'cool':raw.mode_policy,inherit_house_mode:raw.mode_policy==='house',setpoint:parseDecimal(raw.setpoint), cool_comfort_setpoint:parseDecimal(raw.cool_comfort_setpoint),cool_sleep_setpoint:parseDecimal(raw.cool_sleep_setpoint),cool_away_setpoint:parseDecimal(raw.cool_away_setpoint), diff --git a/web/styles.css b/web/styles.css index 2d09922..a5f02b6 100644 --- a/web/styles.css +++ b/web/styles.css @@ -613,7 +613,7 @@ html[data-theme='light'] .simulation-metrics > div, html[data-theme='light'] .si .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.6.1: compact, centered unit-feature controls */ +/* v0.6.2: 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%; } diff --git a/web/sw.js b/web/sw.js index 72b8695..e974966 100644 --- a/web/sw.js +++ b/web/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'gree-controller-v061'; +const CACHE = 'gree-controller-v062'; 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')];