This commit is contained in:
Mateusz Gruszczyński
2026-09-02 09:17:56 +02:00
parent 43441d2fb7
commit 87b68187e7
13 changed files with 119 additions and 58 deletions
Generated
+1 -1
View File
@@ -633,7 +633,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.9.11"
version = "0.9.13"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
version = "0.9.11"
version = "0.9.13"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+8 -8
View File
@@ -1,11 +1,11 @@
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
71d5043c5c5892bcf5ad33dd0ae73600bc0ff9bbc10d93e963c90c173963c39e ./Cargo.lock
c446b9af75b3d07cdf2c42b7f1286f503b225d5dc2ed3592cce4df74de31db21 ./Cargo.toml
18765f4d1c77c7c44ad4438aa3d31e90030577f5395190d0ec1450c0fadd5987 ./Cargo.lock
d3e13a2ef72326abae6de8eb727d858d191bb2eeb1a1680e2c028c2b560f13ea ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
21b3a4808f6d47b3cb283be03faf8ec1327f2a757b247277c0299468c8a0d74c ./README.md
6955218013ff72cc2b92b930e664f0215d067c0ea88f928df09d244b69958ba9 ./README.md
41dfdc6d099b54f87d4dd51f696122b3c88d3bf3ddf420bea8f4bc621ba920b0 ./build.rs
05eafc8541a867aba3121bd0df1ba475305005841c8a2437a667484a232e80e4 ./docs/API.md
b232adc48c1737bd2a7d6ee47c97ff17750243027f6737ae8209f9111c8e819c ./docs/API.md
5322117989c854763d87a4ea6ec5e957863d370ca4ebdc332e2c8c11f23338d1 ./docs/FLOW.md
a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
@@ -17,7 +17,7 @@ ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./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
b4a1da0fc4912485c419f53b07dc651910a5cffb23f19b9a3290be7ae1946478 ./home-assistant/custom_components/gree_controller/manifest.json
3593dc4b937182dcf7d372c8710f079d99f25304f6a17ab7d5b84508127047ce ./home-assistant/custom_components/gree_controller/manifest.json
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./home-assistant/custom_components/gree_controller/sensor.py
@@ -171,7 +171,7 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
460c3f8599feaf87deb854766b1d815069934132140fd3c48b5c81fe62957415 ./web/index.html
0c10450b9241a9b7a811fe8ad44b4413018e33fcd5b932f024a73e1ba6a3f946 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
21b662914590e720dafefe4dc3dccc91dbeb963cce9ae91f007c835d2d0c1fe5 ./web/js/bootstrap.js
72fa280996ba331a05bdb29a1f49b4e61bdd8964953c8ce1db952156620f08fe ./web/js/charts.js
@@ -179,7 +179,7 @@ e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.
d34953aec21ade81ca9770c9c99873b0b2a4862a1bf671a6e13b507a5dd5b7ed ./web/js/dashboard.js
2887856e35e086bb575e9fc0d8b0ed1160b6912297a2957535d5416bda68b4b7 ./web/js/entities.js
9a0e87911f78cd8a969f4c57ce74774f5fa829fdecd79494d9a7f9d5804f9792 ./web/js/events.js
7373214514dc553c6c793afc88aa62f4c21b02c32e002a81f27d7aeabca82fdf ./web/js/flows.js
ee094f55b3f6d0c40fafaf422776ee87b17e0d83eb228ca6c197dfd3efd4c774 ./web/js/flows.js
f06557c6d338259059b990f8f79c91d68ac8ec1f245c302468f08929b5506901 ./web/js/forms.js
dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js
7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js
@@ -189,6 +189,6 @@ c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/route
3d44b5457200a71f66fbf7bde89c7b9adf416ca185ad0e3c61f2c4e551bceb1e ./web/js/settings-ui.js
a06d718217dc6ffb6d4fb5b002fedac02ba7eea3a5b00066c2241df7c1957e51 ./web/js/settings.js
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
d6a7ebd22557a1dc8c74b14562a3f541c77bc0afcae88508a8e667c4ddbb775e ./web/styles.css
c8ca437e032fe81327f3bfe7457b5510f21bad21c021bfc83caf44527ce9633f ./web/styles.css
ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
+1 -1
View File
@@ -2,7 +2,7 @@
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
**Current release: 0.9.11**
**Current release: 0.9.13**
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
+3 -3
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.9.11**.
HTTP and WebSocket API for GREE Controller **0.9.13**.
[← Main documentation](../README.md)
@@ -213,7 +213,7 @@ Response:
{
"status": "ok",
"name": "gree-controller",
"version": "0.9.11",
"version": "0.9.13",
"uptime_seconds": 1234,
"control_ready": true,
"time": "2026-08-30T06:54:00Z"
@@ -237,7 +237,7 @@ Returns the initial Web UI snapshot:
"settings": {},
"outdoor_temperature": null,
"system": {
"version": "0.9.11",
"version": "0.9.13",
"uptime_seconds": 1234,
"auth_required": false,
"control_ready": true,
+4
View File
@@ -88,3 +88,7 @@ The next useful additions should be implemented with persisted runtime state rat
- trace replay from historical sensor samples.
These need durable state and restart semantics before they should be exposed in the editor.
### Display name / alias
The Flow name edited in `/flows` is a presentation alias. Renaming it changes labels shown in the UI (including the dashboard) but does not change the stable Flow ID or generated `flow-<unique-id>` schedule/automation names.
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.9.11",
"version": "0.9.13",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
+4 -3
View File
@@ -823,7 +823,7 @@
"flow.emptyTitle": "No Flows",
"flow.emptyText": "Create your first Flow and build the logic from blocks.",
"flow.editorEyebrow": "Flow editor",
"flow.namePlaceholder": "Logic name",
"flow.namePlaceholder": "Flow name / alias",
"flow.paletteHint": "Add a block, then connect its output to the next block.",
"flow.time": "Time",
"flow.sensors": "Sensors",
@@ -847,7 +847,7 @@
"flow.noActionsYet": "Add an action to see the interpreted logic.",
"flow.always": "Always",
"flow.compileCount": "{schedules} schedules · {automations} automations",
"flow.nameRequired": "Enter a Flow name",
"flow.nameRequired": "Enter a Flow name / alias",
"flow.saved": "Flow saved and translated into executable logic.",
"flow.deleteConfirm": "Delete Flow “{name}” together with its generated schedules and automations?",
"flow.deleted": "Flow deleted.",
@@ -1041,6 +1041,7 @@
"flow.logMessageBlockedTemporary": "The Flow action was blocked by the temporary thermostat.",
"flow.logMessageBlockedThermostatOwner": "The Flow action was suppressed because the device is controlled by an enabled thermostat zone.",
"flow.logMessageBlockedOwnership": "The Flow action was suppressed because control ownership changed during execution.",
"flow.logMessageConflict": "The Flow action was skipped because another automation already claimed the same target."
"flow.logMessageConflict": "The Flow action was skipped because another automation already claimed the same target.",
"flow.nameAliasHint": "This is only the user-facing display name. The technical Flow ID and generated task names remain unchanged."
}
}
+4 -3
View File
@@ -823,7 +823,7 @@
"flow.emptyTitle": "Brak Flow",
"flow.emptyText": "Utwórz pierwszy przepływ i ułóż logikę z bloków.",
"flow.editorEyebrow": "Edytor Flow",
"flow.namePlaceholder": "Nazwa logiki",
"flow.namePlaceholder": "Nazwa / alias Flow",
"flow.paletteHint": "Dodaj blok, a potem połącz jego wyjście z kolejnym blokiem.",
"flow.time": "Czas",
"flow.sensors": "Czujniki",
@@ -847,7 +847,7 @@
"flow.noActionsYet": "Dodaj akcję, aby zobaczyć interpretację logiki.",
"flow.always": "Zawsze",
"flow.compileCount": "{schedules} harm. · {automations} autom.",
"flow.nameRequired": "Podaj nazwę Flow",
"flow.nameRequired": "Podaj nazwę / alias Flow",
"flow.saved": "Flow zapisany i przetłumaczony na logikę wykonawczą.",
"flow.deleteConfirm": "Usunąć Flow „{name}” razem z wygenerowanymi harmonogramami i automatyzacjami?",
"flow.deleted": "Flow usunięty.",
@@ -1041,6 +1041,7 @@
"flow.logMessageBlockedTemporary": "Akcja Flow została zablokowana przez tymczasowy termostat.",
"flow.logMessageBlockedThermostatOwner": "Akcja Flow została pominięta, ponieważ urządzeniem steruje aktywna strefa termostatu.",
"flow.logMessageBlockedOwnership": "Akcja Flow została pominięta, ponieważ w trakcie wykonania zmienił się właściciel sterowania.",
"flow.logMessageConflict": "Akcja Flow została pominięta z powodu konfliktu z inną automatyzacją dla tego samego celu."
"flow.logMessageConflict": "Akcja Flow została pominięta z powodu konfliktu z inną automatyzacją dla tego samego celu.",
"flow.nameAliasHint": "To tylko nazwa wyświetlana użytkownikowi. Techniczne ID Flow i nazwy wygenerowanych zadań pozostają bez zmian."
}
}
+12 -8
View File
@@ -88,7 +88,7 @@
<div class="dashboard-global-divider"></div>
<div class="house-climate dashboard-house-climate" id="houseClimate"></div>
</div>
<section class="panel dashboard-section-panel">
<section class="panel dashboard-section-panel" id="controlPlanSection">
<div class="dashboard-section-head compact-head">
<div><span class="eyebrow" data-i18n="plan.eyebrow">Control plan</span>
<h2 data-i18n="plan.title">What happens next</h2>
@@ -693,7 +693,7 @@
<section id="flowEditor" class="flow-editor" hidden aria-label="Flow editor">
<header class="flow-editor-bar">
<div class="flow-editor-title"><button type="button" class="icon-button" data-action="close-flow-editor" aria-label="Back"></button><div><span class="eyebrow" data-i18n="flow.editorEyebrow">Flow editor</span><input id="flowName" maxlength="100" data-i18n-placeholder="flow.namePlaceholder" placeholder="My automation"></div></div>
<div class="flow-editor-title"><button type="button" class="icon-button" data-action="close-flow-editor" aria-label="Back"></button><div><span class="eyebrow" data-i18n="flow.editorEyebrow">Flow editor</span><input id="flowName" maxlength="100" data-i18n-placeholder="flow.namePlaceholder" data-i18n-title="flow.nameAliasHint" placeholder="My automation"></div></div>
<div class="flow-editor-actions"><button class="secondary" type="button" data-action="flow-templates" data-i18n="flow.templates">Templates</button><button class="secondary" type="button" data-action="import-flow" data-i18n="flow.import">Import</button><button class="secondary" type="button" data-action="export-flow" data-i18n="flow.export">Export</button><button class="secondary" type="button" data-action="flow-dry-run" data-i18n="flow.dryRun">Dry-run</button><button class="secondary" type="button" data-action="flow-logs" data-i18n="flow.logs">Logs</button><label class="flow-enabled"><input type="checkbox" id="flowEnabled" checked><span data-i18n="common.enabled">Enabled</span></label><span id="flowCompileStatus" class="badge"></span><button class="primary" type="button" data-action="save-flow" data-i18n="actions.save">Save</button></div>
</header>
<div class="flow-editor-body">
@@ -721,7 +721,7 @@
<input id="flowImportFile" type="file" accept="application/json,.json,.flow.json" hidden>
<dialog id="flowSharedInputDialog" class="flow-tool-dialog">
<dialog id="flowSharedInputDialog">
<form id="flowSharedInputForm" class="dialog-form">
<div class="dialog-head"><div><span class="eyebrow" data-i18n="flow.sharedInputEyebrow">Wspólne dane</span><h2 id="flowSharedInputDialogTitle" data-i18n="flow.sharedInputTitle">Wspólne wejście Flow</h2></div><button type="button" data-close>×</button></div>
<input type="hidden" name="id">
@@ -733,11 +733,12 @@
<button type="button" class="secondary" id="flowSharedInputTest" data-i18n="flow.sharedInputTest">Testuj wejście HA</button>
<div id="flowSharedInputTestResult" class="flow-shared-test-result" hidden></div>
</div>
<div class="dialog-actions flow-shared-dialog-actions"><button type="button" class="secondary" data-close data-i18n="actions.cancel">Anuluj</button><button type="submit" class="primary" data-i18n="actions.save">Zapisz</button></div>
<div class="form-actions"><button type="button" class="secondary" data-close data-i18n="actions.cancel">Anuluj</button><button type="submit" class="primary" data-i18n="actions.save">Zapisz</button></div>
</form>
</dialog>
<dialog id="flowTemplateDialog" class="flow-tool-dialog">
<dialog id="flowTemplateDialog" class="dialog-wide">
<div class="dialog-form">
<div class="dialog-head"><div><span class="eyebrow" data-i18n="flow.templateLibrary">Flow library</span><h2 data-i18n="flow.templates">Templates</h2></div><button type="button" data-close>×</button></div>
<p class="field-note" data-i18n="flow.templatesHint">Ready-made layouts create an editable graph using existing zones, devices and groups.</p>
<div class="flow-template-toolbar"><label><span data-i18n="flow.templateSearchLabel">Szukaj presetów</span><input id="flowTemplateSearch" type="search" autocomplete="off" data-i18n-placeholder="flow.templateSearch" placeholder="Szukaj presetów…"></label></div>
@@ -748,17 +749,20 @@
</div>
<aside id="flowTemplatePreview" class="flow-template-preview"><div class="empty compact"><strong data-i18n="flow.templatePreview">Podgląd presetu</strong><span data-i18n="flow.templateSelectPreview">Wybierz preset, aby zobaczyć podgląd.</span></div></aside>
</div>
</div>
</dialog>
<dialog id="flowTestDialog" class="flow-tool-dialog">
<dialog id="flowTestDialog" class="dialog-wide">
<div class="dialog-form">
<div class="dialog-head"><div><span class="eyebrow" data-i18n="flow.testMode">Flow diagnostics</span><h2 id="flowTestTitle" data-i18n="flow.dryRun">Dry-run</h2></div><button type="button" data-close>×</button></div>
<div id="flowSimulationControls" class="flow-simulation-controls">
<p class="field-note" data-i18n="flow.dryRunHint">Dry-run does not change thermostat, device, group, schedule or automation state.</p>
<label><span data-i18n="flow.simulationTime">Simulation date and time</span><input id="flowSimulationAt" type="datetime-local"></label>
<div><strong data-i18n="flow.simulationOverrides">Sensor overrides</strong><p class="field-note" data-i18n="flow.simulationOverridesHint">Leave blank to use the current application or Home Assistant state.</p><div id="flowSimulationOverrides" class="flow-simulation-overrides"></div></div>
<div class="dialog-actions"><button class="primary" type="button" data-action="run-flow-dry-run" data-i18n="flow.runSimulation">Run simulation</button></div>
<div class="form-actions"><button class="primary" type="button" data-action="run-flow-dry-run" data-i18n="flow.runSimulation">Run simulation</button></div>
</div>
<div id="flowTestResults" class="flow-test-results"></div>
</div>
</dialog>
<nav class="bottom-nav" data-i18n-aria="nav.navigation" aria-label="Navigation">
@@ -906,7 +910,7 @@
data-i18n="preset.away">Away</span><input type="text" inputmode="decimal" step="0.5"
name="heat_away_setpoint" value="17"></label></div>
</details>
<div class="two">
<div class="two zone-hysteresis-row">
<div class="hysteresis-config"><label id="commonHysteresisField"><span data-i18n="zones.hysteresis">Hysteresis
°C</span><input type="text" inputmode="decimal" step="0.1" min="0.1" max="5" name="hysteresis"
value="0.6"></label><label class="check" data-i18n-title="zones.separateHysteresisHint"
+18 -5
View File
@@ -78,15 +78,17 @@ function planZoneEffectiveEnabled(zone) {
function renderControlPlan() {
const host = $('#controlPlan'); if (!host) return;
const section = $('#controlPlanSection');
const plan = app.controlPlan;
if (!plan) {
if (section) section.hidden = false;
host.innerHTML = `<div class="panel plan-loading">${esc(tr('plan.loading'))}</div>`;
return;
}
const allZones = plan.zones || [];
const houseEvents = (plan.next_events || []).slice(0, 3);
const house = `<article class="panel plan-card plan-house"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.house'))}</span><h3>${esc(houseModeLabel(plan.house_mode || 'off'))}</h3></div><span class="badge active">${esc((plan.control_strategy || 'setpoint') === 'setpoint' ? tr('plan.strategySetpoint') : (plan.control_strategy || 'setpoint'))}</span></div><p>${esc(tr('plan.houseSummary', { zones: allZones.filter(planZoneEffectiveEnabled).length, demand: allZones.filter(zone => planZoneEffectiveEnabled(zone) && zone.demand).length }))}</p><ul class="plan-events">${houseEvents.length ? houseEvents.map(planEventMarkup).join('') : `<li class="muted">${esc(tr('plan.noEvents'))}</li>`}</ul></article>`;
const house = houseEvents.length ? `<article class="panel plan-card plan-house"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.house'))}</span><h3>${esc(houseModeLabel(plan.house_mode || 'off'))}</h3></div><span class="badge active">${esc((plan.control_strategy || 'setpoint') === 'setpoint' ? tr('plan.strategySetpoint') : (plan.control_strategy || 'setpoint'))}</span></div><p>${esc(tr('plan.houseSummary', { zones: allZones.filter(planZoneEffectiveEnabled).length, demand: allZones.filter(zone => planZoneEffectiveEnabled(zone) && zone.demand).length }))}</p><ul class="plan-events">${houseEvents.map(planEventMarkup).join('')}</ul></article>` : '';
const groupCards = (app.groups || []).map(group => {
const memberIds = new Set(group.zone_ids || []);
@@ -98,7 +100,8 @@ function renderControlPlan() {
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 `<article class="panel plan-card plan-group ${powerEnabled ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('groups.group'))}</span><h3>${esc(group.name)}</h3></div><span class="badge ${powerEnabled ? 'active' : ''}">${esc(powerEnabled ? tr('common.on') : tr('common.off'))}</span></div><div class="plan-group-state"><strong>${esc(mode)}</strong><span>·</span><strong>${esc(preset)}</strong></div><p>${esc(tr('plan.groupSummary', { zones: members.length, demand }))}</p><ul class="plan-events">${events.length ? events.map(planEventMarkup).join('') : `<li class="muted">${esc(tr(powerEnabled ? 'plan.noEvents' : 'plan.groupOff'))}</li>`}</ul></article>`;
if (!events.length) return '';
return `<article class="panel plan-card plan-group ${powerEnabled ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('groups.group'))}</span><h3>${esc(group.name)}</h3></div><span class="badge ${powerEnabled ? 'active' : ''}">${esc(powerEnabled ? tr('common.on') : tr('common.off'))}</span></div><div class="plan-group-state"><strong>${esc(mode)}</strong><span>·</span><strong>${esc(preset)}</strong></div><p>${esc(tr('plan.groupSummary', { zones: members.length, demand }))}</p><ul class="plan-events">${events.map(planEventMarkup).join('')}</ul></article>`;
}).join('');
const zoneGroups = new Map();
@@ -109,16 +112,26 @@ function renderControlPlan() {
}));
const zones = allZones.map(zone => {
const events = (zone.next_events || []).slice(0, 2);
if (!events.length) return '';
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'));
const planState = zoneRuntimeStatusLabel(zone, zone.mode || 'off');
return `<article class="panel plan-card plan-zone ${planZoneEffectiveEnabled(zone) ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(scope)}</span><h3>${esc(zone.zone_name)}</h3></div><span class="badge ${zone.device_manual_override ? 'manual-override' : (planZoneEffectiveEnabled(zone) && zone.demand ? 'active' : '')}">${esc(planState)}</span></div><div class="plan-temp"><span>${fmtTemp(zone.current_temperature)}</span><b>→</b><strong>${esc(target)}<small>°C</small></strong></div><p>${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}</p><ul class="plan-events">${events.length ? events.map(planEventMarkup).join('') : `<li class="muted">${esc(tr('plan.noEvents'))}</li>`}</ul></article>`;
return `<article class="panel plan-card plan-zone ${planZoneEffectiveEnabled(zone) ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(scope)}</span><h3>${esc(zone.zone_name)}</h3></div><span class="badge ${zone.device_manual_override ? 'manual-override' : (planZoneEffectiveEnabled(zone) && zone.demand ? 'active' : '')}">${esc(planState)}</span></div><div class="plan-temp"><span>${fmtTemp(zone.current_temperature)}</span><b>→</b><strong>${esc(target)}<small>°C</small></strong></div><p>${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}</p><ul class="plan-events">${events.map(planEventMarkup).join('')}</ul></article>`;
}).join('');
const rules = (plan.rules || []).filter(rule => rule.enabled);
const ruleCard = rules.length ? `<article class="panel plan-card plan-rules"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.rules'))}</span><h3>${esc(tr('plan.ruleCount', { count: rules.length }))}</h3></div></div><ul class="plan-events">${rules.slice(0, 3).map(rule => `<li><time>${esc(automationTriggerLabel(rule))}</time><span>${esc(rule.name)}${esc(rule.action_group_name ? `${tr('groups.group')}: ${rule.action_group_name}` : (rule.action_device_name || ''))}</span></li>`).join('')}</ul></article>` : '';
host.innerHTML = house + groupCards + zones + ruleCard;
const ruleCard = rules.length ? `<article class="panel plan-card plan-rules"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.rules'))}</span><h3>${esc(tr('plan.ruleCount', { count: rules.length }))}</h3></div></div><ul class="plan-events">${rules.slice(0, 3).map(rule => {
const automation = (app.automations || []).find(item => item.id === rule.id);
const flow = automation?.flow_id ? (app.flows || []).find(item => item.id === automation.flow_id) : null;
const displayName = flow?.name || rule.name;
const target = rule.action_group_name ? `${tr('groups.group')}: ${rule.action_group_name}` : (rule.action_device_name || '');
return `<li><time>${esc(automationTriggerLabel(rule))}</time><span class="plan-rule-summary"><strong>${esc(displayName)}</strong>${target ? `<small>→ ${esc(target)}</small>` : ''}</span></li>`;
}).join('')}</ul></article>` : '';
const content = house + groupCards + zones + ruleCard;
host.innerHTML = content;
if (section) section.hidden = !content;
}
async function loadControlPlan() {
try { app.controlPlan = await api('/api/control-plan'); renderControlPlan(); renderSimulationPage(); }
+1 -1
View File
@@ -477,7 +477,7 @@ function renderFlowPresetPreview(preset) {
<div class="flow-template-preview-stats"><span>${esc(tr('flow.templateNodesCount', { count:preset.flow.nodes.length }))}</span><span>${esc(tr('flow.templateEdgesCount', { count:preset.flow.edges.length }))}</span></div>
${flowPresetPreviewGraph(preset)}
<div class="flow-template-requirements"><strong>${esc(tr('flow.templateRequirements'))}</strong><div>${reqMarkup}</div>${req.missing.length ? `<p class="field-note warning-note">${esc(tr('flow.templateRequirementsMissing', { items:req.missing.join(', ') }))}</p>` : ''}</div>
<div class="dialog-actions"><button type="button" class="primary" data-flow-template-use="${esc(preset.id)}">${esc(tr('flow.templateUse'))}</button></div>`;
<div class="form-actions"><button type="button" class="primary" data-flow-template-use="${esc(preset.id)}">${esc(tr('flow.templateUse'))}</button></div>`;
}
function flowPresetCategoryLabel(key) { return tr(`flow.templateCategory.${key}`) === `flow.templateCategory.${key}` ? key : tr(`flow.templateCategory.${key}`); }
+47 -9
View File
@@ -1195,6 +1195,10 @@ dialog::backdrop {
backdrop-filter: blur(4px);
}
.dialog-wide {
width: min(860px, calc(100% - 24px));
}
.dialog-form {
display: grid;
gap: 14px;
@@ -4869,6 +4873,45 @@ body.simulation-standalone [data-view="simulation"] {
white-space: nowrap;
}
#controlPlan .plan-rules {
gap: 10px;
}
#controlPlan .plan-rules .plan-card-head {
padding-bottom: 2px;
}
#controlPlan .plan-rules .plan-events {
gap: 8px;
padding-top: 9px;
}
#controlPlan .plan-rules .plan-events li {
align-items: start;
gap: 8px;
}
#controlPlan .plan-events .plan-rule-summary {
display: grid;
gap: 2px;
overflow: visible;
text-overflow: clip;
white-space: normal;
}
#controlPlan .plan-rule-summary strong {
color: var(--text);
font-size: 10px;
font-weight: 650;
line-height: 1.35;
}
#controlPlan .plan-rule-summary small {
color: var(--muted);
font-size: 9px;
line-height: 1.35;
}
#controlPlan .plan-events .muted {
white-space: normal;
}
@@ -6194,9 +6237,6 @@ body.flow-editor-open { overflow:hidden; }
}
/* Flow diagnostics, templates and portable Flow files */
.flow-tool-dialog { width:min(860px,calc(100vw - 32px)); max-height:min(820px,calc(100vh - 32px)); overflow:auto; border:1px solid var(--line); border-radius:16px; background:var(--surface); color:var(--text); padding:18px; box-shadow:0 24px 70px rgba(0,0,0,.32); }
.flow-tool-dialog::backdrop { background:rgba(0,0,0,.48); }
.flow-template-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:12px; margin-top:16px; }
.flow-template-card { display:grid; gap:7px; text-align:left; align-content:start; min-height:118px; border:1px solid var(--line); border-radius:14px; background:var(--surface-2); color:var(--text); padding:14px; }
.flow-template-card:hover { border-color:var(--accent); }
.flow-template-card span { color:var(--muted); font-size:.86rem; line-height:1.4; }
@@ -6246,8 +6286,6 @@ body.flow-editor-open { overflow:hidden; }
.flow-shared-input-fields { display:grid; gap:12px; }
.flow-shared-input-fields .two { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
.flow-template-list { display:grid; grid-template-columns:1fr; gap:22px; }
.flow-template-category { display:grid; gap:10px; }
.flow-template-category + .flow-template-category { padding-top:18px; border-top:1px solid var(--line); }
.flow-template-category-head { display:grid; gap:3px; }
.flow-template-category-head span { color:var(--muted); font-size:.84rem; }
.flow-template-category-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:12px; }
@@ -6263,8 +6301,6 @@ body.flow-editor-open { overflow:hidden; }
.flow-shared-settings-head { justify-content:space-between; align-items:flex-start; }
.flow-shared-settings-head > div { min-width:0; }
.flow-shared-add-button { flex:0 0 auto; align-self:flex-start; margin-left:auto; }
.flow-shared-dialog-actions { display:flex; align-items:center; justify-content:space-between; gap:24px; padding-top:14px; margin-top:4px; border-top:1px solid var(--line); }
.flow-shared-dialog-actions > button { min-width:112px; }
.flow-shared-settings-link { margin-top:12px; width:max-content; max-width:100%; }
.flow-shared-settings.is-linked-target { outline:2px solid var(--accent); outline-offset:3px; }
.flow-template-tabs { display:flex; gap:8px; overflow-x:auto; padding:4px 0 10px; margin-top:14px; border-bottom:1px solid var(--line); scrollbar-width:thin; }
@@ -6276,7 +6312,6 @@ body.flow-editor-open { overflow:hidden; }
@media (max-width:760px) {
.flow-shared-settings-head { gap:12px; }
.flow-shared-add-button { width:auto; }
.flow-shared-dialog-actions { gap:16px; }
}
/* v0.9.10: Flow preset library preview, favorites/search and shared-input diagnostics */
@@ -6328,7 +6363,7 @@ body.flow-editor-open { overflow:hidden; }
.flow-template-favorite { width:34px; height:34px; border:0; border-radius:50%; background:transparent; color:var(--muted); cursor:pointer; font-size:1.15rem; line-height:1; }
.flow-template-card > .flow-template-favorite { position:absolute; right:7px; top:7px; }
.flow-template-favorite.active { color:var(--accent); background:var(--accent-soft); }
.flow-template-preview .dialog-actions { margin-top:0; padding-top:10px; border-top:1px solid var(--line); justify-content:flex-end; }
.flow-template-preview .form-actions { margin-top:0; padding-top:10px; border-top:1px solid var(--line); }
@media (max-width:900px) {
.flow-template-browser { grid-template-columns:1fr; }
@@ -6341,3 +6376,6 @@ body.flow-editor-open { overflow:hidden; }
.flow-shared-test-result dl > div { grid-template-columns:1fr; gap:2px; }
.flow-template-preview-canvas { height:220px; }
}
/* Keep zone hysteresis/source controls aligned without stretching selects. */
.zone-hysteresis-row { align-items:start; }