From a8b80bdba9f0e8757989010f1d7723c8e8f8ffd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Tue, 25 Aug 2026 18:25:55 +0200 Subject: [PATCH] v0.6.9 --- BUILD_REPORT.md | 13 ++- Cargo.lock | 2 +- Cargo.toml | 2 +- FILE_MANIFEST.sha256 | 22 +++--- README.md | 4 +- .../gree_controller/manifest.json | 2 +- lang/en.json | 2 + lang/pl.json | 2 + web/app.js | 46 ++++++++--- web/index.html | 7 +- web/styles.css | 79 +++++++++++++++++++ web/sw.js | 2 +- 12 files changed, 150 insertions(+), 33 deletions(-) diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md index 3fc71af..1ba9678 100644 --- a/BUILD_REPORT.md +++ b/BUILD_REPORT.md @@ -1,4 +1,4 @@ -# GREE Controller v0.6.8 - build and validation report +# GREE Controller v0.6.9 - build and validation report ## Scope @@ -180,3 +180,14 @@ Validation performed in this packaging environment: - Kept Control plan, Zones and Devices as collapsed disclosure panels. - Reduced dashboard hero, metric, global-control, group-card and disclosure spacing for a denser layout without removing functionality. - Advanced PWA cache key to `gree-controller-v068-dashboard-wrap`. + + +## 0.6.9 dashboard fit, compact summary and group-aware control plan + +- Reworked dashboard Quick Thermostats so the target-temperature control occupies its own row and measured/device-target temperatures sit below it; labels no longer collide or clip in medium-width cards. +- Kept the quick-thermostat list as a wrapped responsive grid with no horizontal scrolling. +- Compressed the Home / Comfort under control summary into one short card; average temperature and small online/active/demand counters now share the same row instead of consuming a separate large hero plus metrics row. +- Reduced dashboard control-plan blocks to compact simulator-style cards. +- Added explicit climate-group cards to What happens next, combining member demand and upcoming member-zone events. +- Zone plan cards identify their climate group membership, and automation plan rows preserve group targets. +- Advanced the PWA cache key to `gree-controller-v069-dashboard-fit`. diff --git a/Cargo.lock b/Cargo.lock index 9c127cd..69923ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.6.8" +version = "0.6.9" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 9d4374f..e0b7cb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.6.8" +version = "0.6.9" 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 79b54ea..6fb1ca9 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,10 +1,10 @@ d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -4c31bf357041b674d29a7ab5751b6060ad8650178e227aa3137d9195a49a2c03 ./BUILD_REPORT.md -006a65924eb99c5c911edeea96f6f1498f44734eaf638af81d9562f7e41faaf2 ./Cargo.lock -b0dfc13a0710b9f4cc0d2417129335ef277509ce951bcf85781bc38cf8aa892e ./Cargo.toml +e6eb0b2882aa446546fc5a4e63fb11973e79483c263fa053fc10cb2e9aaf53bb ./BUILD_REPORT.md +9227483cd15d1d93b01b312d119179c8348cffda4bd4870571cdabfa8e972b14 ./Cargo.lock +b8fcf5528e25c4823ca90b2565dd025994aafadfa35360af475db66ef53d2359 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -1dbfc1eab7344a021cdce4859b5ae03faf30974d3b9c7c46d49e933bb34cdfdd ./README.md +191d7fd1242937ba1c3df7f0b3014c56a6944f6445cd1453832b4dc8024d37bf ./README.md f2c8eb28fedb26bb8f5636e1805e7a782cc472b87d882f6752d520f16216a248 ./build.rs 74fc74d2bb05541f2137250c6a73a9f39a6fbd838e5d95d8c8efd7ebefc384f0 ./docs/API.md 234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md @@ -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 -3019758a902fbd38b1faf7d25ed2bea8e4ab124dca34cc6fe0d05a9d4e5f3714 ./home-assistant/custom_components/gree_controller/manifest.json +97117268892b7868ff4001017d422c30a94b68f4c656cbb8c5fa1d65c23fd65f ./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 -3de7cd88c5c879ba797e805962afa7d449fbd1a016ca8ecf4740d2348ebd39f7 ./lang/en.json -1a23e91ed6bec4850cd1304dad9669041b08ea75114f3de3ececd5d5eaeee258 ./lang/pl.json +6385512c4d23d53abca0c1e178420733baf30232a20d00e53688c7b8da91ac01 ./lang/en.json +067888c4d220169604303f3236e4832e2935bf2a20633cbff7fa86ec8dc9785d ./lang/pl.json 028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh @@ -62,10 +62,10 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol 66d7a1a12843975506ffef2128d161627b96d665d39cedde2d791651e4598ee1 ./src/queries.rs f74ac398c6f2e64ebb965900330324f3ec7403e70339638a408229f29fbd07ae ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service -d1971287c12f5d022e0607dca6ec065d11903432fa6ec0df5584cb5ad0a524e2 ./web/app.js +dfb89acd9b040c32b24146198d66ca98a0bc3dfbc852ac03b2ca3d252923ec3e ./web/app.js e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -e665efcc1fac4138d6ce83309e5c6766e80901292bd25983d35613ff3c754b9d ./web/index.html +f3b864558d327d769a6d4a33e88b96a518ec900ce39d6af990bd9cabb3b075a5 ./web/index.html fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest -0e9e57efd2ba706c315c34d43c95930455fa3ce720bfac6a506b89a755fc4349 ./web/styles.css -9099a3d1d4b2775241728b22fa81cd07ecebc34cb160a729feeca7af3efcdf03 ./web/sw.js +77c96fe9ca242edb44defe2f7df40b99e7ec2c546d8d13c1c31cc55beea6522c ./web/styles.css +17f83bd268aac0a48e0ad230b970ca868df330f8884cc03747d8bee9aa8aef5d ./web/sw.js d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js diff --git a/README.md b/README.md index 470a85d..77e07c3 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.8**. +Current version: **0.6.9**. ## Highlights @@ -47,7 +47,7 @@ See [`BUILD_REPORT.md`](BUILD_REPORT.md) for package validation details and [`do On Debian, Ubuntu or an LXC container: ```bash -unzip gree-controller-v0.6.8.zip +unzip gree-controller-v0.6.9.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 9ac15ad..0b8f09c 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.8", + "version": "0.6.9", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/lang/en.json b/lang/en.json index 9f03a7f..a4c34da 100644 --- a/lang/en.json +++ b/lang/en.json @@ -362,6 +362,8 @@ "plan.rules": "Extra automations", "plan.ruleCount": "{count} active rules", "plan.event": "Change", + "plan.groupSummary": "Group zones: {zones} · requesting action: {demand}", + "plan.groupOff": "Group is turned off", "zones.enable": "Enable zone", "zones.disable": "Disable zone", "history.90d": "90 days", diff --git a/lang/pl.json b/lang/pl.json index 2441624..c520a9a 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -362,6 +362,8 @@ "plan.rules": "Dodatkowe automatyzacje", "plan.ruleCount": "Aktywne reguły: {count}", "plan.event": "Zmiana", + "plan.groupSummary": "Strefy w grupie: {zones} · żądające działania: {demand}", + "plan.groupOff": "Grupa jest wyłączona", "zones.enable": "Włącz strefę", "zones.disable": "Wyłącz strefę", "history.90d": "90 dni", diff --git a/web/app.js b/web/app.js index 7ff60a7..cb26d1a 100644 --- a/web/app.js +++ b/web/app.js @@ -285,18 +285,42 @@ function renderControlPlan() { host.innerHTML = `
${esc(tr('plan.loading'))}
`; return; } - const houseEvents = (plan.next_events || []).slice(0, 5); - const house = `
${esc(tr('plan.house'))}

${esc(houseModeLabel(plan.house_mode || 'off'))}

${esc(plan.control_strategy || 'setpoint')}

${esc(tr('plan.houseSummary', {zones:(plan.zones || []).filter(zone=>zone.enabled).length, demand:(plan.zones || []).filter(zone=>zone.enabled && zone.demand).length}))}

`; - const zones = (plan.zones || []).map(zone => { - const events = (zone.next_events || []).slice(0, 3); - const target = zone.target_temperature == null ? '--' : Number(zone.target_temperature).toFixed(1); - return `
${esc(zone.device_name || tr('common.noDevice'))}

${esc(zone.zone_name)}

${esc(zone.enabled ? (zone.demand ? tr('zones.requesting') : tr('zones.satisfied')) : tr('common.disabled'))}
${fmtTemp(zone.current_temperature)}${esc(target)}°C

${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}

`; - }).join(''); - const rules = (plan.rules || []).filter(rule => rule.enabled); - const ruleCard = rules.length ? `
${esc(tr('plan.rules'))}

${esc(tr('plan.ruleCount', {count:rules.length}))}

` : ''; - host.innerHTML = house + zones + ruleCard; -} + const allZones = plan.zones || []; + const houseEvents = (plan.next_events || []).slice(0, 3); + const house = `
${esc(tr('plan.house'))}

${esc(houseModeLabel(plan.house_mode || 'off'))}

${esc(plan.control_strategy || 'setpoint')}

${esc(tr('plan.houseSummary', {zones:allZones.filter(zone=>zone.enabled).length, demand:allZones.filter(zone=>zone.enabled && zone.demand).length}))}

`; + + const groupCards = (app.groups || []).map(group => { + const memberIds = new Set(group.zone_ids || []); + const members = allZones.filter(zone => memberIds.has(zone.zone_id)); + const state = groupState(group); + const powerEnabled = group.power_enabled !== false; + const demand = members.filter(zone => zone.enabled && zone.demand).length; + const mode = state.mode === 'house' ? tr('groups.followHouse') : state.mode === 'mixed' ? tr('groups.mixed') : modeLabel(state.mode); + const preset = state.preset === 'mixed' ? tr('groups.mixed') : zonePresetLabel(state.preset); + const events = members.flatMap(zone => (zone.next_events || []).map(event => ({...event, label:`${zone.zone_name}: ${event.label}`}))) + .sort((a,b) => new Date(a.at) - new Date(b.at)).slice(0, 2); + return `
${esc(tr('groups.group'))}

${esc(group.name)}

${esc(powerEnabled ? tr('common.on') : tr('common.off'))}
${esc(mode)}·${esc(preset)}

${esc(tr('plan.groupSummary', {zones:members.length, demand}))}

`; + }).join(''); + + const zoneGroups = new Map(); + (app.groups || []).forEach(group => (group.zone_ids || []).forEach(zoneId => { + const names = zoneGroups.get(zoneId) || []; + names.push(group.name); + zoneGroups.set(zoneId, names); + })); + const zones = allZones.map(zone => { + const events = (zone.next_events || []).slice(0, 2); + const target = zone.target_temperature == null ? '--' : Number(zone.target_temperature).toFixed(1); + const groupNames = zoneGroups.get(zone.zone_id) || []; + const scope = groupNames.length ? `${tr('groups.group')}: ${groupNames.join(' · ')}` : (zone.device_name || tr('common.noDevice')); + return `
${esc(scope)}

${esc(zone.zone_name)}

${esc(zone.enabled ? (zone.demand ? tr('zones.requesting') : tr('zones.satisfied')) : tr('common.disabled'))}
${fmtTemp(zone.current_temperature)}${esc(target)}°C

${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}

`; + }).join(''); + + const rules = (plan.rules || []).filter(rule => rule.enabled); + const ruleCard = rules.length ? `
${esc(tr('plan.rules'))}

${esc(tr('plan.ruleCount', {count:rules.length}))}

` : ''; + host.innerHTML = house + groupCards + zones + ruleCard; +} async function loadControlPlan() { try { app.controlPlan = await api('/api/control-plan'); renderControlPlan(); renderSimulationPage(); } catch (error) { console.warn('Unable to load control plan:', error); } diff --git a/web/index.html b/web/index.html index 4b99a4f..be9df60 100644 --- a/web/index.html +++ b/web/index.html @@ -32,11 +32,10 @@
-
-
Home

Comfort under control

Loading devices…

-
--°C
+
+
Home

Comfort under control

Loading devices…

+
--°C
-
Global power

All air conditioners

Master power for all enabled units. Whole-house mode or profile selection automatically switches it back on.

diff --git a/web/styles.css b/web/styles.css index f1d9d0b..9bf1deb 100644 --- a/web/styles.css +++ b/web/styles.css @@ -762,3 +762,82 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36 .dashboard-disclosure > summary { padding:12px 13px; } .dashboard-disclosure-body { padding:10px; } } + +/* 0.6.9 dashboard density and fit fixes */ +[data-view="dashboard"] .dashboard-summary-card { + min-height:0; + align-items:center; + gap:14px; + padding:13px 16px; + border-radius:18px; +} +[data-view="dashboard"] .dashboard-summary-card::after { width:100px; height:100px; right:-44px; bottom:-64px; border-width:18px; } +[data-view="dashboard"] .dashboard-summary-copy { min-width:0; } +[data-view="dashboard"] .dashboard-summary-copy .eyebrow { font-size:9px; } +[data-view="dashboard"] .dashboard-summary-copy h1 { margin:2px 0 3px; font-size:20px; letter-spacing:-.025em; } +[data-view="dashboard"] .dashboard-summary-copy p { font-size:11px; line-height:1.3; } +[data-view="dashboard"] .dashboard-summary-status { z-index:1; display:flex; align-items:center; justify-content:flex-end; min-width:0; gap:10px; } +[data-view="dashboard"] .dashboard-summary-status .metrics { display:flex; align-items:center; gap:5px; margin:0; } +[data-view="dashboard"] .dashboard-summary-status .metric { display:flex; align-items:baseline; gap:5px; min-width:0; padding:6px 8px; border-radius:11px; background:var(--surface-muted); } +[data-view="dashboard"] .dashboard-summary-status .metric span { margin:0; font-size:9px; white-space:nowrap; } +[data-view="dashboard"] .dashboard-summary-status .metric strong { font-size:13px; line-height:1; } +[data-view="dashboard"] .dashboard-summary-status .hero-temp { min-width:72px; font-size:31px; letter-spacing:-.045em; text-align:right; } + +/* Quick thermostat: target control gets its own row, readings no longer collide with it. */ +#dashboardZones { grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr)); } +#dashboardZones .zone-thermostat { min-width:0; } +#dashboardZones .thermostat-main { + grid-template-columns:minmax(0,1fr) minmax(0,1fr); + grid-template-areas:"control control" "room device"; + gap:5px 10px; + margin:8px 0 7px; +} +#dashboardZones .thermostat-main > div:first-child { grid-area:room; min-width:0; text-align:left; } +#dashboardZones .thermostat-main > .temperature-control { grid-area:control; } +#dashboardZones .thermostat-main > div:last-child { grid-area:device; min-width:0; text-align:right; } +#dashboardZones .thermostat-main small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +#dashboardZones .thermostat-main strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:14px; } +#dashboardZones .thermostat-main .temperature-control.compact { gap:12px; padding:0 0 2px; } +#dashboardZones .thermostat-main .temperature-control.compact button { width:36px; height:36px; font-size:18px; } +#dashboardZones .thermostat-main .target-temp.compact { min-width:80px; font-size:30px; } +#dashboardZones .list-card-head h3 { font-size:16px; } +#dashboardZones .list-card-head p { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:11px; } +#dashboardZones .zone-enable-toggle { flex:0 0 auto; } +#dashboardZones .zone-state-line { gap:8px; font-size:10px; } +#dashboardZones .zone-state-line span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } + +/* Smaller Node-RED/simulator-like plan blocks with explicit group context. */ +#controlPlan.automation-plan-grid { grid-template-columns:repeat(auto-fit,minmax(min(100%,210px),1fr)); gap:8px; } +#controlPlan .plan-card { min-height:0; gap:7px; padding:11px; border-radius:14px; } +#controlPlan .plan-card-head { gap:7px; } +#controlPlan .plan-card-head h3 { margin-top:1px; font-size:15px; line-height:1.2; } +#controlPlan .plan-card-head .eyebrow { font-size:8px; line-height:1.25; } +#controlPlan .plan-card .badge { max-width:100px; padding:4px 6px; overflow:hidden; font-size:9px; text-overflow:ellipsis; white-space:nowrap; } +#controlPlan .plan-card > p { font-size:10px; line-height:1.3; } +#controlPlan .plan-temp { gap:5px; } +#controlPlan .plan-temp > span { font-size:15px; } +#controlPlan .plan-temp > strong { font-size:21px; } +#controlPlan .plan-group-state { display:flex; align-items:center; gap:5px; min-width:0; color:var(--text-soft); font-size:11px; } +#controlPlan .plan-group-state strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +#controlPlan .plan-group { border-color:color-mix(in srgb,var(--accent) 20%,var(--line)); background:color-mix(in srgb,var(--surface) 97%,var(--accent)); } +#controlPlan .plan-events { gap:4px; padding-top:6px; } +#controlPlan .plan-events li { grid-template-columns:62px minmax(0,1fr); gap:5px; font-size:9px; line-height:1.3; } +#controlPlan .plan-events li span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +#controlPlan .plan-events .muted { white-space:normal; } + +@media (max-width: 900px) { + [data-view="dashboard"] .dashboard-summary-card { align-items:flex-start; } + [data-view="dashboard"] .dashboard-summary-status { flex-wrap:wrap; } + [data-view="dashboard"] .dashboard-summary-status .metrics { order:2; width:100%; justify-content:flex-end; } +} + +@media (max-width: 700px) { + [data-view="dashboard"] .dashboard-summary-card { min-height:0; padding:12px 13px; } + [data-view="dashboard"] .dashboard-summary-copy h1 { font-size:18px; } + [data-view="dashboard"] .dashboard-summary-status { align-self:center; } + [data-view="dashboard"] .dashboard-summary-status .metrics { display:none; } + [data-view="dashboard"] .dashboard-summary-status .hero-temp { min-width:64px; font-size:27px; } + #dashboardZones { grid-template-columns:1fr; } + #controlPlan.automation-plan-grid { grid-template-columns:repeat(auto-fit,minmax(min(100%,180px),1fr)); } +} +[data-view="dashboard"] .dashboard-summary-card { margin-bottom:10px; } diff --git a/web/sw.js b/web/sw.js index f5bc303..7887d98 100644 --- a/web/sw.js +++ b/web/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'gree-controller-v068-dashboard-wrap'; +const CACHE = 'gree-controller-v069-dashboard-fit'; 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')];