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')];