v0.12.1
This commit is contained in:
Generated
+1
-1
@@ -700,7 +700,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+16
-16
@@ -1,14 +1,14 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
6877eacd3931f7922f3f379d46e8cd9de7a609a65060ce2285c09b9c95f230aa ./Cargo.lock
|
||||
ebe64386eb553c298994df40f73a56b4980eedcd88a84cb310134070c1753eb3 ./Cargo.toml
|
||||
0696b5112a35943786aa4dbe0547b745d158309795a8e8967946eed07cb0d1e7 ./Cargo.lock
|
||||
000dd8d1a535f357667cf812f817a2abbc72eb79fe062ae2dcf8fef65d6b7491 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
92633f07b839dcdeafa96cb3ffdaaa600f65387a9113ed595987e68982fed0d6 ./README.md
|
||||
c353091e7695cd19f77bdcad80b41438710a7315c5f2696016dc482000023a21 ./README.md
|
||||
d56cfff4179a6511aa764a91c6ddebe21fcd35f36b7298e62d012e11f62bfeb9 ./build.rs
|
||||
3596e68d53d614ff784cdaa7a26eabd651cf02909da770bc863518f1218299a0 ./docs/API.md
|
||||
9596e50efb3ce09afd823d2507c814cfbffe812a622b45c9e0d53526e8cd996a ./docs/API.md
|
||||
1b522e1b61643733accf0a45b8b862cfb96e1468343c9819600e263fe67a2b96 ./docs/CODE_AUDIT_0.12.0.md
|
||||
2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md
|
||||
3ba861e16d523f962574334ed74d53d6be2669dcb05851578d7a5434e8856f4e ./docs/openapi.json
|
||||
933f560266bd309be0b1eaaa943e86874646e0b9ada5e805bf47a7a62f7063b8 ./docs/openapi.json
|
||||
a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./home-assistant/README.md
|
||||
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
|
||||
6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py
|
||||
@@ -19,7 +19,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
|
||||
9538b4a08e6c76dbd5850f3720f582253fd8e4542871891a2cf5552759fb334a ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
fec5ff50552f331bc0f994d9980059f1b1c309a57419e20f785c04d93dcd14f9 ./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
|
||||
@@ -31,9 +31,9 @@ c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./home-assista
|
||||
810bf9a9483277cbde14454b6b88dbfe79b873a6f34737031a7e10d23fda9299 ./lang/en.json
|
||||
ad48d11875aa041c0f593aa6e322f8d953aef137d4df4c369c5f82eec5aebd11 ./lang/pl.json
|
||||
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
||||
1e7b1bcf4194abfb8bad0d601268af0547d58541731e98b74a3500d7629d0be0 ./mock/index.html
|
||||
007c78e0cb404778985980b23bb5f8bbc7a8f04c80015204e24c321970e3b15e ./mock/mock.js
|
||||
c5d515b987d96ebfbc7593e9bac9aff64c8d54420f23cc8df5e2c84b14a69852 ./mock/styles.css
|
||||
daf25cf3faf39865033e57cc7f79a34b0e0dbca174a37d349611f14d77143e15 ./mock/index.html
|
||||
efe0fb67500857bfcebb2dcfba77fdc551e5c2e32d2ffff4b092c19528dc679e ./mock/mock.js
|
||||
6abcb2d3607aa76d763f4ccaa17523f6bc69faaf19e89f0b306559750ff2f066 ./mock/styles.css
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./mock/theme-init.js
|
||||
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
|
||||
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
|
||||
@@ -74,7 +74,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
|
||||
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
|
||||
966c7b81c07c07d9073f797388f695164095a520753910eb26c5daa64caaae02 ./scripts/api_dev_test.py
|
||||
cd2eb8c851ab3cacd00efcb5cdbd404f03279e75126c46f025e132ba1e9d4ce1 ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
@@ -181,13 +181,13 @@ df2f13c98b9d7f84916dcbf9e01a479a6f392f888413fe22bae586526747f704 ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
1f3abc20b7373fbb8995fe4331d5b448afb3e8c5a8722b3b0417cb89c684f3de ./web/index.html
|
||||
d0388bf22f34ae28fb63074b0bc15c087a3be298bb702d006d4814ebb786b36e ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
||||
8fb20fdba11df68feb0b096bb5b4c8f936db974aed5d345a0d01455054f31f95 ./web/js/bootstrap.js
|
||||
7dcf8d722407ce0b73dbcfc4982a2a2f3ad5fa82eb9ece227ad850f2117e3d76 ./web/js/charts.js
|
||||
f75449450a179f5853b8b08c69dec93205985e0dcbc806d355fd6060237761f6 ./web/js/core.js
|
||||
dfc123885a9b6b5c1d07c12d2d2625ff15aa0844e5cd3736092ccd0a360da39c ./web/js/dashboard.js
|
||||
563ce41a849574ddfbed281828f34cdcb498bbd04009056c73a378b84bad822a ./web/js/entities.js
|
||||
badde8a5214a0322b2c45803c02a1cb723d53b25e57c687369cd804f509d9744 ./web/js/charts.js
|
||||
af92b2140f6248bc636d7c75e37a9460f4128ba59adf1b7842d25244a607555c ./web/js/core.js
|
||||
361d68a9f7dda66a628ebde25d130a26024d809aef521d1b3ad8d8bdf5d39098 ./web/js/dashboard.js
|
||||
d6fbd8ef4a67269afadb2f59f19fe9a359f7ec4d7bd1473ecff77df117b9558d ./web/js/entities.js
|
||||
e332b3f811958a938105272c1e8e28d6ca8ba69303001da165b4b0b85727695c ./web/js/events.js
|
||||
1b21b3ae43a6848f9fc13fd4eacaa6a1a50d44c65aa37ca737be60aa4eca8a38 ./web/js/flows.js
|
||||
f06557c6d338259059b990f8f79c91d68ac8ec1f245c302468f08929b5506901 ./web/js/forms.js
|
||||
@@ -199,6 +199,6 @@ c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/route
|
||||
427376a35832cb3644206c6e89e1654bd807eec78352f80bd0dd43f74b4ec810 ./web/js/settings-ui.js
|
||||
9dd9fbcc5f00584562bae6a840189495f830d1086d9679763dbad6e9c18c4fdb ./web/js/settings.js
|
||||
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
|
||||
08e3301706c2ec3ec642dd9c3fc1f8cc1c12ad30113d98fc77ade35bd827d947 ./web/styles.css
|
||||
9976b7dd54d0075c3bc153f520740d25b7870042c32020b96ac8547669a1f148 ./web/styles.css
|
||||
ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||
|
||||
@@ -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.12.0**
|
||||
**Current release: 0.12.1**
|
||||
|
||||
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
|
||||
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# GREE Controller API reference
|
||||
|
||||
HTTP and WebSocket API for GREE Controller **0.12.0**.
|
||||
HTTP and WebSocket API for GREE Controller **0.12.1**.
|
||||
|
||||
[← Main documentation](../README.md)
|
||||
|
||||
@@ -230,7 +230,7 @@ Response:
|
||||
{
|
||||
"status": "ok",
|
||||
"name": "gree-controller",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"uptime_seconds": 1234,
|
||||
"control_ready": true,
|
||||
"time": "2026-08-30T06:54:00Z"
|
||||
@@ -254,7 +254,7 @@ Returns the initial Web UI snapshot:
|
||||
"house": {"mode": "cool"},
|
||||
"outdoor_temperature": null,
|
||||
"system": {
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"uptime_seconds": 1234,
|
||||
"auth_required": false,
|
||||
"control_ready": true,
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "GREE Controller API",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"summary": "Local HTTP/WebSocket API for GREE HVAC control",
|
||||
"description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nNormal administrator endpoints accept `Authorization: Bearer <APP_TOKEN>` or `x-api-token: <APP_TOKEN>` when `GREE_CONTROLLER_APP_TOKEN` is configured. If the app token is empty, those endpoints intentionally run in trusted-LAN mode without authentication.\n\nThe restricted `/api/integrations/home-assistant/*` control surface always requires either a generated access token or the administrator app token. Generated tokens do **not** grant normal administrator API access.\n\nIn Swagger UI use **Authorize** and enter the token in either the bearer field or `x-api-token` field.\n\n### Control ownership\nThermostat zones, groups, house rules, direct device control, schedules and visual Flows share an ownership/arbitration model. Prefer zone/group/house endpoints for thermostat intent; direct device commands are technical/manual control and can deliberately take ownership.\n\n### 0.12.0 breaking API\nRuntime settings are split into functional `/api/settings/*` resources. The former `/api/settings`, `/api/debug`, `/api/events/retention`, `/api/settings/export` and `/api/settings/import` endpoints were removed without compatibility aliases.",
|
||||
"license": {
|
||||
@@ -4854,7 +4854,7 @@
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "0.12.0"
|
||||
"example": "0.12.1"
|
||||
},
|
||||
"uptime_seconds": {
|
||||
"type": "integer",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
+15
-9
@@ -270,10 +270,10 @@
|
||||
</select></label><label><span data-i18n="simulation.show">Show</span><select
|
||||
id="simulationTarget"></select></label></div>
|
||||
<div class="simulation-summary-grid" id="simulationSummary"></div>
|
||||
<div class="panel simulation-flow-shell">
|
||||
<div class="simulation-flow-head">
|
||||
<section class="panel simulation-section-panel" aria-labelledby="simulationFlowTitle">
|
||||
<div class="simulation-panel-head">
|
||||
<div><span class="eyebrow" data-i18n="simulation.flowEyebrow">Live flow</span>
|
||||
<h2 data-i18n="simulation.flowTitle">Automation flow board</h2>
|
||||
<h2 id="simulationFlowTitle" data-i18n="simulation.flowTitle">Automation flow board</h2>
|
||||
</div>
|
||||
<div class="flow-legend"><span><i class="flow-legend-dot input"></i><b
|
||||
data-i18n="simulation.legendInput">Input</b></span><span><i class="flow-legend-dot logic"></i><b
|
||||
@@ -283,21 +283,27 @@
|
||||
<div class="simulation-flow-scroll">
|
||||
<div class="simulation-flow-board" id="simulationFlowBoard"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel simulation-timeline-panel">
|
||||
<div class="simulation-section-head">
|
||||
</section>
|
||||
<section class="panel simulation-section-panel" aria-labelledby="simulationTimelineTitle">
|
||||
<div class="simulation-panel-head">
|
||||
<div><span class="eyebrow" data-i18n="simulation.timelineEyebrow">Timeline</span>
|
||||
<h2 data-i18n="simulation.timelineTitle">What will happen next</h2>
|
||||
<h2 id="simulationTimelineTitle" data-i18n="simulation.timelineTitle">What will happen next</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="simulation-panel-body">
|
||||
<div class="simulation-timeline" id="simulationTimeline"></div>
|
||||
</div>
|
||||
<div class="simulation-section-head">
|
||||
</section>
|
||||
<section class="panel simulation-section-panel" aria-labelledby="simulationRulesTitle">
|
||||
<div class="simulation-panel-head">
|
||||
<div><span class="eyebrow" data-i18n="simulation.rulesEyebrow">Rules</span>
|
||||
<h2 data-i18n="simulation.rulesTitle">Automation rules</h2>
|
||||
<h2 id="simulationRulesTitle" data-i18n="simulation.rulesTitle">Automation rules</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="simulation-panel-body">
|
||||
<div class="simulation-rules-grid" id="simulationRules"></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="history">
|
||||
|
||||
@@ -10,6 +10,14 @@ document.addEventListener('DOMContentLoaded',()=>{
|
||||
document.querySelector('#houseClimate').innerHTML='<div class="house-climate-head"><div><h3>Tryb domu</h3><p>Wspólne ustawienia dla stref.</p></div><span class="outside-pill">Na zewnątrz 12.8°C</span></div><div class="house-mode-row"><button class="active">Grzanie</button><button>Chłodzenie</button></div><div class="house-preset-row"><button>Auto</button><button class="active">Komfort</button><button>Sen</button><button>Poza domem</button></div>';
|
||||
document.querySelector('#controlPlan').innerHTML='<article class="list-card"><div class="list-card-head"><div><h3>Salon</h3><p>Utrzymuj 21.5°C do 22:30</p></div><span class="badge">Aktywne</span></div></article><article class="list-card"><div class="list-card-head"><div><h3>Sypialnia</h3><p>Tryb nocny od 22:30</p></div><span class="badge">za 2h</span></div></article>';
|
||||
}
|
||||
if(view==='simulation'){
|
||||
document.querySelector('#simulationTarget').innerHTML='<option>Cały dom</option><option>Salon</option><option>Sypialnia</option>';
|
||||
document.querySelector('#simulationSummary').innerHTML='<article class="panel simulation-summary-card"><small>Tryb domu</small><strong>Grzanie</strong><span>Strategia: temperatura zadana</span></article><article class="panel simulation-summary-card"><small>Tryb nocny</small><strong>Wyłączony</strong><span>22:00 → 06:00</span></article><article class="panel simulation-summary-card"><small>Na zewnątrz</small><strong>12.8°C</strong><span>Aktualizacja 08:30</span></article><article class="panel simulation-summary-card"><small>Aktywne strefy</small><strong>2</strong><span>1 strefa zgłasza zapotrzebowanie</span></article>';
|
||||
const board=document.querySelector('#simulationFlowBoard'); board.style.width='1325px'; board.style.height='390px';
|
||||
board.innerHTML='<div class="flow-lane-label" style="top:128px"><strong>Salon</strong><span>GREE Salon</span></div><article class="flow-node input" style="left:45px;top:155px;width:190px;min-height:106px"><div class="flow-node-top"><span>Temperatura</span><b class="flow-node-badge active">●</b></div><h3>Salon</h3><strong>20.8°C</strong><p>Home Assistant</p></article><article class="flow-node logic" style="left:305px;top:155px;width:190px;min-height:106px"><div class="flow-node-top"><span>Termostat</span></div><h3>Salon</h3><strong>21.5°C</strong><p>Komfort</p></article><article class="flow-node logic decision demand" style="left:565px;top:155px;width:190px;min-height:106px"><div class="flow-node-top"><span>Decyzja</span><b class="flow-node-badge active">▶</b></div><h3>Grzanie</h3><strong>Potrzeba ciepła</strong><p>Poniżej celu</p></article><article class="flow-node action" style="left:825px;top:155px;width:190px;min-height:106px"><div class="flow-node-top"><span>Polecenie</span></div><h3>GREE Salon</h3><strong>22.0°C · Auto</strong><p>Quiet: wył.</p></article>';
|
||||
document.querySelector('#simulationTimeline').innerHTML='<article class="simulation-timeline-item"><time>08:45</time><div><strong>Salon</strong><p>Komfort · 21.5°C · GREE Salon</p></div></article><article class="simulation-timeline-item"><time>22:00</time><div><strong>Cały dom</strong><p>Tryb nocny</p></div></article><article class="simulation-timeline-item"><time>06:00 jutro</time><div><strong>Sypialnia</strong><p>Komfort · 20.5°C · GREE Sypialnia</p></div></article>';
|
||||
document.querySelector('#simulationRules').innerHTML='<article class="simulation-rule-card"><div class="simulation-rule-head"><div><span class="eyebrow">Automatyzacja</span><h3>Otwarte okno</h3></div><span class="badge active">Stan</span></div><p>Urządzenie: GREE Salon</p><div class="simulation-rule-action">Wyłącz termostat, gdy okno pozostaje otwarte.</div><div class="simulation-rule-meta"><small>Ostatnio: 07:12</small><small>Gotowa: teraz</small></div></article><article class="simulation-rule-card"><div class="simulation-rule-head"><div><span class="eyebrow">Automatyzacja</span><h3>Tani prąd</h3></div><span class="badge active">Próg</span></div><p>Urządzenie: GREE Sypialnia</p><div class="simulation-rule-action">Podnieś temperaturę docelową o 0.5°C.</div><div class="simulation-rule-meta"><small>Ostatnio: wczoraj</small><small>Gotowa: teraz</small></div></article>';
|
||||
}
|
||||
if(view==='flows'){
|
||||
document.querySelector('#flowListCount').textContent='3 flow';
|
||||
document.querySelector('#flowList').innerHTML=`<article class="list-card flow-card"><div class="list-card-head"><div><h3>Okno w salonie</h3><p>Wyłącza grzanie, gdy okno jest otwarte.</p></div><button class="zone-enable-toggle active"><span>✓</span>Włączone</button></div><div class="card-stats"><div class="card-stat"><small>Bloki</small><strong>5</strong></div><div class="card-stat"><small>Harmonogramy</small><strong>0</strong></div><div class="card-stat"><small>Automatyzacje</small><strong>1</strong></div></div><div class="card-footer"><small>1 automatyzacja</small><div class="card-menu"><button>Otwórz edytor</button><button class="danger">Usuń</button></div></div></article><article class="list-card flow-card is-disabled"><div class="list-card-head"><div><h3>Tani prąd</h3><p>Podbija temperaturę przy niskiej cenie energii.</p></div><button class="zone-enable-toggle"><span>○</span>Wyłączone</button></div><div class="card-stats"><div class="card-stat"><small>Bloki</small><strong>7</strong></div><div class="card-stat"><small>Harmonogramy</small><strong>1</strong></div><div class="card-stat"><small>Automatyzacje</small><strong>2</strong></div></div><div class="card-footer"><small>1 harmonogram · 2 automatyzacje</small><div class="card-menu"><button>Otwórz edytor</button><button class="danger">Usuń</button></div></div></article>`;
|
||||
|
||||
+59
-456
@@ -176,18 +176,6 @@ button:disabled {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toolbar-select {
|
||||
width: auto;
|
||||
min-width: 58px;
|
||||
min-height: 40px;
|
||||
padding: 7px 28px 7px 10px;
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.theme-select {
|
||||
min-width: 94px;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 13px;
|
||||
@@ -638,16 +626,6 @@ h3 {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.device-network-info {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.device-network-error {
|
||||
color: var(--danger) !important;
|
||||
}
|
||||
|
||||
.card-menu {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
@@ -867,30 +845,6 @@ h3 {
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.dot.target {
|
||||
background: var(--warning);
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.form-grid h3,
|
||||
.form-grid hr,
|
||||
.form-grid .wide {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.form-grid hr,
|
||||
.dialog-form hr {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
@@ -1389,16 +1343,6 @@ legend {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-grid h3,
|
||||
.form-grid hr,
|
||||
.form-grid .wide {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.log-row {
|
||||
grid-template-columns: 62px 1fr;
|
||||
}
|
||||
@@ -1407,9 +1351,6 @@ legend {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.theme-select {
|
||||
min-width: 86px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 410px) {
|
||||
@@ -1435,13 +1376,6 @@ legend {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.toolbar-select {
|
||||
min-width: 54px;
|
||||
}
|
||||
|
||||
.theme-select {
|
||||
min-width: 82px;
|
||||
}
|
||||
}
|
||||
|
||||
.token-manager {
|
||||
@@ -1502,17 +1436,6 @@ legend {
|
||||
}
|
||||
}
|
||||
|
||||
/* Zone quick controls */
|
||||
.zone-quick-control {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin-top: 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.temperature-control.compact {
|
||||
padding: 4px 0 7px;
|
||||
gap: 16px;
|
||||
@@ -1528,10 +1451,6 @@ legend {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.zone-quick-control .mode-row {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.discovery-name-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@@ -1760,27 +1679,6 @@ legend {
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.house-power-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin: -15px 0 12px;
|
||||
}
|
||||
|
||||
.house-power-panel h3 {
|
||||
margin: 2px 0 4px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.house-power-panel p {
|
||||
margin: 0;
|
||||
max-width: 650px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.house-power-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
@@ -1935,9 +1833,6 @@ legend {
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.house-power-panel {
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
.house-climate-head {
|
||||
align-items: flex-start;
|
||||
@@ -1947,11 +1842,6 @@ legend {
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.house-power-panel {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.house-power-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -2663,18 +2553,19 @@ legend {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.simulation-summary-card {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 16px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.simulation-summary-card small {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.simulation-summary-card strong {
|
||||
@@ -2688,74 +2579,81 @@ legend {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.simulation-section-head {
|
||||
.simulation-section-panel {
|
||||
margin-bottom: 18px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.simulation-panel-head {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 24px 0 12px;
|
||||
gap: 16px;
|
||||
padding: 15px 18px 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: color-mix(in srgb, var(--surface-muted) 52%, var(--surface));
|
||||
}
|
||||
|
||||
.simulation-section-head h2 {
|
||||
margin: 4px 0 0;
|
||||
.simulation-panel-head > div:first-child {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.simulation-timeline-panel {
|
||||
padding: 16px;
|
||||
margin-bottom: 22px;
|
||||
.simulation-panel-head .eyebrow {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.simulation-panel-head h2 {
|
||||
margin: 3px 0 0;
|
||||
font-size: 19px;
|
||||
letter-spacing: -.025em;
|
||||
}
|
||||
|
||||
.simulation-panel-body {
|
||||
padding: 16px 18px 18px;
|
||||
}
|
||||
|
||||
.simulation-timeline {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.simulation-timeline-item {
|
||||
display: grid;
|
||||
grid-template-columns: 110px 1fr;
|
||||
grid-template-columns: 110px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.simulation-timeline-item:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.simulation-timeline-item:last-child {
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.simulation-timeline-item time {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.simulation-timeline-item strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.simulation-timeline-item p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.simulation-zone-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.simulation-zone-card {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.simulation-zone-card.disabled {
|
||||
opacity: .68;
|
||||
}
|
||||
|
||||
.simulation-zone-head,
|
||||
.simulation-rule-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -2763,140 +2661,62 @@ legend {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.simulation-zone-head h3,
|
||||
.simulation-rule-head h3 {
|
||||
margin: 3px 0 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.simulation-zone-head p,
|
||||
.simulation-rule-head p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.simulation-flow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.simulation-step {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, .02);
|
||||
min-height: 112px;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .simulation-step {
|
||||
background: rgba(0, 0, 0, .02);
|
||||
}
|
||||
|
||||
.simulation-step.accent {
|
||||
box-shadow: inset 0 0 0 1px rgba(101, 163, 255, .28);
|
||||
}
|
||||
|
||||
.simulation-step small,
|
||||
.simulation-metrics small,
|
||||
.simulation-rule-meta small {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.simulation-step strong {
|
||||
font-size: 15px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.simulation-step span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.simulation-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.simulation-metrics>div,
|
||||
.simulation-rule-action {
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255, 255, 255, .02);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .simulation-metrics>div,
|
||||
html[data-theme='light'] .simulation-rule-action {
|
||||
background: rgba(0, 0, 0, .02);
|
||||
}
|
||||
|
||||
.simulation-metrics strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.simulation-events {
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.simulation-rules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
|
||||
gap: 14px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.simulation-rule-card {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.simulation-rule-card > p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.simulation-rule-action {
|
||||
font-size: 14px;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--surface);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.simulation-rule-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
gap: 6px 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
.simulation-flow,
|
||||
.simulation-metrics {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.simulation-timeline-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.simulation-flow,
|
||||
.simulation-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Compact connection state: a colored dot replaces the Connected/Disconnected label. */
|
||||
.brand-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3050,25 +2870,6 @@ html[data-theme='light'] .simulation-rule-action {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.settings-subhead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-subhead strong,
|
||||
.settings-subhead small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-subhead small {
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.settings-save-bar {
|
||||
position: sticky;
|
||||
bottom: 18px;
|
||||
@@ -3157,25 +2958,6 @@ html[data-theme='light'] .simulation-rule-action {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.simulation-flow-shell {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.simulation-flow-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 18px 18px 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.simulation-flow-head h2 {
|
||||
margin: 3px 0 0;
|
||||
}
|
||||
|
||||
.flow-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -3465,7 +3247,7 @@ html[data-theme='light'] .simulation-rule-action {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.simulation-flow-head {
|
||||
.simulation-panel-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -3642,29 +3424,6 @@ html[data-theme='light'] .simulation-rule-action {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.dashboard-group-panel {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.dashboard-group-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dashboard-group-head h3 {
|
||||
margin: 3px 0 5px;
|
||||
}
|
||||
|
||||
.dashboard-group-head p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.group-quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 330px), 1fr));
|
||||
@@ -3859,7 +3618,6 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
|
||||
@media (max-width:720px) {
|
||||
|
||||
.dashboard-group-head,
|
||||
.simulation-page-head {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
@@ -4105,84 +3863,6 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.dashboard-secondary-stack {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dashboard-disclosure {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dashboard-disclosure>summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 13px 15px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dashboard-disclosure>summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dashboard-disclosure>summary h3 {
|
||||
margin: 2px 0 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dashboard-disclosure>summary .eyebrow {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.dashboard-summary-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dashboard-summary-side .badge {
|
||||
min-width: 28px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dashboard-disclosure-chevron {
|
||||
display: inline-block;
|
||||
color: var(--muted);
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
transition: transform .18s ease;
|
||||
}
|
||||
|
||||
.dashboard-disclosure[open]>summary {
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.dashboard-disclosure[open] .dashboard-disclosure-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dashboard-disclosure-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.dashboard-disclosure-body .automation-plan-grid,
|
||||
.dashboard-disclosure-body .dashboard-zones {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dashboard-disclosure-body .device-grid,
|
||||
.dashboard-disclosure-body .list-grid,
|
||||
.dashboard-disclosure-body .automation-plan-grid {
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
|
||||
}
|
||||
|
||||
/* Dashboard quick controls share one compact visual language. */
|
||||
.quick-control-card {
|
||||
--quick-control-height: 28px;
|
||||
@@ -4197,10 +3877,6 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
padding: 2px 0 5px;
|
||||
}
|
||||
|
||||
.quick-control-card .quick-control-row-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.quick-control-card .quick-control-row-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
@@ -4449,14 +4125,6 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.dashboard-disclosure>summary {
|
||||
padding: 12px 13px;
|
||||
}
|
||||
|
||||
.dashboard-disclosure-body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.quick-thermostat-control .list-card-head {
|
||||
align-items: center;
|
||||
}
|
||||
@@ -5157,57 +4825,6 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.language-picker {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
width: 58px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 11px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--text-soft);
|
||||
transition: border-color .15s ease, background .15s ease, transform .15s ease;
|
||||
}
|
||||
|
||||
.language-picker:hover {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.language-picker:active {
|
||||
transform: scale(.97);
|
||||
}
|
||||
|
||||
.language-label {
|
||||
font-size: 11px;
|
||||
font-weight: 850;
|
||||
letter-spacing: .06em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.language-chevron {
|
||||
margin-top: -2px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.language-select {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar-icon-control {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
@@ -5232,8 +4849,7 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
.language-picker:focus-within {
|
||||
button:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@@ -5338,12 +4954,6 @@ button:focus-visible,
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.language-picker {
|
||||
width: 50px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.toolbar-icon-control {
|
||||
flex-basis: 38px;
|
||||
width: 38px;
|
||||
@@ -5352,9 +4962,6 @@ button:focus-visible,
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.language-chevron {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
@@ -6004,7 +5611,6 @@ textarea[aria-invalid="true"] {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
||||
/* v0.9.1: global compressor queue overview. */
|
||||
.queue-overview-button {
|
||||
display: inline-flex;
|
||||
@@ -6450,7 +6056,6 @@ body.flow-editor-open { overflow:hidden; }
|
||||
.flow-template-preview-node-time, .flow-template-preview-node-timeop { border-top:3px solid var(--blue); }
|
||||
.flow-template-preview-node-haaction { border-top:3px solid var(--accent); }
|
||||
|
||||
|
||||
/* Compact Flow state control and mobile action entry point. */
|
||||
.flow-enabled { white-space:nowrap; }
|
||||
.flow-enabled input { width:16px; min-height:16px; accent-color:var(--accent); }
|
||||
@@ -6730,8 +6335,7 @@ textarea:focus { border-color:var(--accent); outline:2px solid color-mix(in srgb
|
||||
.flow-generated-card { border-left:3px solid var(--accent-border); }
|
||||
.flow-generated-card .card-footer > small { color:var(--accent); }
|
||||
|
||||
|
||||
/* 0.12.0 production layout guards and in-app chart preview. */
|
||||
/* 0.12.x production layout guards and in-app chart preview. */
|
||||
html,body { max-width:100%; overflow-x:clip; }
|
||||
main,.view { min-width:0; max-width:100%; }
|
||||
.flow-info-panel,.flow-info-panel > div { min-width:0; }
|
||||
@@ -7112,8 +6716,7 @@ body.chart-fullscreen-open::before { content:""; position:fixed; inset:0; z-inde
|
||||
.ping-stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
|
||||
/* 0.12.0 diagnostics, manual-control safety and technical connection checks. */
|
||||
/* 0.12.x diagnostics, manual-control safety and technical connection checks. */
|
||||
.manual-device-problem {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.12.0.
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.12.1.
|
||||
|
||||
Default mode is read-only and safe to run against a real controller.
|
||||
Use --settings-write to additionally round-trip all split settings resources and
|
||||
@@ -26,7 +26,7 @@ from typing import Any, Callable, Iterable, Optional
|
||||
|
||||
DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787")
|
||||
DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "")
|
||||
DEFAULT_EXPECTED_VERSION = "0.12.0"
|
||||
DEFAULT_EXPECTED_VERSION = "0.12.1"
|
||||
|
||||
SETTINGS_PATHS: dict[str, set[str]] = {
|
||||
"/api/settings/application": {"simulator_enabled"},
|
||||
@@ -331,7 +331,7 @@ def test_openapi(client: ApiClient, expected_version: str) -> str:
|
||||
raise TestFailure("OpenAPI has no paths object")
|
||||
missing = sorted(EXPECTED_OPENAPI_PATHS - set(paths))
|
||||
if missing:
|
||||
raise TestFailure(f"OpenAPI missing 0.12.0 paths: {missing}")
|
||||
raise TestFailure(f"OpenAPI missing required paths: {missing}")
|
||||
forbidden = sorted(set(REMOVED_0120_PATHS) & set(paths))
|
||||
if forbidden:
|
||||
raise TestFailure(f"OpenAPI still exposes removed paths: {forbidden}")
|
||||
@@ -503,7 +503,7 @@ def run_suite(args: argparse.Namespace) -> int:
|
||||
print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n")
|
||||
|
||||
runner.run("public health", lambda: test_health(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.12.0 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.12.1 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("protected API requires auth", lambda: test_protected_auth(client))
|
||||
|
||||
for path in SAFE_GET_PATHS:
|
||||
@@ -571,7 +571,7 @@ def run_suite(args: argparse.Namespace) -> int:
|
||||
|
||||
def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Developer smoke/integration tests for GREE Controller API 0.12.0.",
|
||||
description="Developer smoke/integration tests for GREE Controller API 0.12.1.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""Examples:
|
||||
python3 scripts/api_dev_test.py
|
||||
|
||||
+15
-9
@@ -280,10 +280,10 @@
|
||||
</select></label><label><span data-i18n="simulation.show">Show</span><select
|
||||
id="simulationTarget"></select></label></div>
|
||||
<div class="simulation-summary-grid" id="simulationSummary"></div>
|
||||
<div class="panel simulation-flow-shell">
|
||||
<div class="simulation-flow-head">
|
||||
<section class="panel simulation-section-panel" aria-labelledby="simulationFlowTitle">
|
||||
<div class="simulation-panel-head">
|
||||
<div><span class="eyebrow" data-i18n="simulation.flowEyebrow">Live flow</span>
|
||||
<h2 data-i18n="simulation.flowTitle">Automation flow board</h2>
|
||||
<h2 id="simulationFlowTitle" data-i18n="simulation.flowTitle">Automation flow board</h2>
|
||||
</div>
|
||||
<div class="flow-legend"><span><i class="flow-legend-dot input"></i><b
|
||||
data-i18n="simulation.legendInput">Input</b></span><span><i class="flow-legend-dot logic"></i><b
|
||||
@@ -293,21 +293,27 @@
|
||||
<div class="simulation-flow-scroll">
|
||||
<div class="simulation-flow-board" id="simulationFlowBoard"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel simulation-timeline-panel">
|
||||
<div class="simulation-section-head">
|
||||
</section>
|
||||
<section class="panel simulation-section-panel" aria-labelledby="simulationTimelineTitle">
|
||||
<div class="simulation-panel-head">
|
||||
<div><span class="eyebrow" data-i18n="simulation.timelineEyebrow">Timeline</span>
|
||||
<h2 data-i18n="simulation.timelineTitle">What will happen next</h2>
|
||||
<h2 id="simulationTimelineTitle" data-i18n="simulation.timelineTitle">What will happen next</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="simulation-panel-body">
|
||||
<div class="simulation-timeline" id="simulationTimeline"></div>
|
||||
</div>
|
||||
<div class="simulation-section-head">
|
||||
</section>
|
||||
<section class="panel simulation-section-panel" aria-labelledby="simulationRulesTitle">
|
||||
<div class="simulation-panel-head">
|
||||
<div><span class="eyebrow" data-i18n="simulation.rulesEyebrow">Rules</span>
|
||||
<h2 data-i18n="simulation.rulesTitle">Automation rules</h2>
|
||||
<h2 id="simulationRulesTitle" data-i18n="simulation.rulesTitle">Automation rules</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="simulation-panel-body">
|
||||
<div class="simulation-rules-grid" id="simulationRules"></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="history">
|
||||
|
||||
+14
-90
@@ -151,6 +151,9 @@ function updateChartFullscreenButton(card) {
|
||||
function closeChartPreview(card) {
|
||||
if (!card) return;
|
||||
card.classList.remove('chart-fullscreen-fallback');
|
||||
card.removeAttribute('role');
|
||||
card.removeAttribute('aria-modal');
|
||||
card.removeAttribute('aria-label');
|
||||
document.body.classList.remove('chart-fullscreen-open');
|
||||
updateChartFullscreenButton(card);
|
||||
const canvas = card.querySelector('canvas[id]');
|
||||
@@ -158,6 +161,7 @@ function closeChartPreview(card) {
|
||||
}
|
||||
|
||||
function toggleChartFullscreen(id) {
|
||||
if (!window.matchMedia('(min-width: 761px)').matches) return;
|
||||
const canvas = document.getElementById(id);
|
||||
const card = canvas?.closest('.history-chart-card');
|
||||
if (!card) return;
|
||||
@@ -172,12 +176,10 @@ function toggleChartFullscreen(id) {
|
||||
}
|
||||
|
||||
card.classList.add('chart-fullscreen-fallback');
|
||||
card.setAttribute('role', 'dialog');
|
||||
card.setAttribute('aria-modal', 'true');
|
||||
card.setAttribute('aria-label', card.querySelector('h3')?.textContent || tr('history.fullscreen'));
|
||||
document.body.classList.add('chart-fullscreen-open');
|
||||
if (window.matchMedia('(max-width: 760px)').matches) {
|
||||
app.chartZooms[id] = 1;
|
||||
canvas.dataset.chartZoom = '1';
|
||||
if (canvas.parentElement) canvas.parentElement.scrollLeft = 0;
|
||||
}
|
||||
updateChartFullscreenButton(card);
|
||||
requestAnimationFrame(() => redrawHistoryChart(id));
|
||||
}
|
||||
@@ -186,15 +188,12 @@ function prepareCanvas(canvas, height) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const wrap = canvas.parentElement;
|
||||
const wrapWidth = Math.floor(wrap?.clientWidth || rect.width || 720);
|
||||
const card = canvas.closest('.history-chart-card');
|
||||
const fullscreen = chartCardIsFullscreen(card);
|
||||
const mobileFullscreen = fullscreen && window.matchMedia('(max-width: 760px)').matches;
|
||||
const fullscreen = chartCardIsFullscreen(canvas.closest('.history-chart-card'));
|
||||
const wrapHeight = fullscreen ? Math.floor(wrap?.clientHeight || 0) : 0;
|
||||
const naturalMobileHeight = Math.round(clamp(wrapWidth * .72, 260, 360));
|
||||
const actualHeight = mobileFullscreen ? Math.min(wrapHeight || naturalMobileHeight, naturalMobileHeight) : (wrapHeight || height);
|
||||
const actualHeight = wrapHeight || height;
|
||||
const zoom = clamp(Number(canvas.dataset.chartZoom || 1), 1, MAX_CHART_ZOOM);
|
||||
// Normal charts may scroll horizontally. Fullscreen 100% always fits the viewport width.
|
||||
const baseWidth = fullscreen ? Math.max(1, wrapWidth) : Math.max(720, wrapWidth);
|
||||
// Keep the original chart geometry on narrow screens and let the wrapper scroll horizontally.
|
||||
const baseWidth = Math.max(720, wrapWidth);
|
||||
const width = Math.max(1, Math.floor(baseWidth * zoom));
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = width * dpr; canvas.height = actualHeight * dpr;
|
||||
@@ -262,10 +261,7 @@ function bindChartTooltip(canvas, series, sortedRows, geometry) {
|
||||
.filter(point => Number.isFinite(point.ts) && Number.isFinite(point.value))
|
||||
.sort((a, b) => a.ts - b.ts)
|
||||
}));
|
||||
const touches = new Map();
|
||||
let drag = null;
|
||||
let pinch = null;
|
||||
let touchPan = null;
|
||||
|
||||
const hide = () => { tooltip.hidden = true; line.hidden = true; };
|
||||
const hideSelection = () => { if (selection) selection.hidden = true; };
|
||||
@@ -323,77 +319,26 @@ function bindChartTooltip(canvas, series, sortedRows, geometry) {
|
||||
};
|
||||
|
||||
canvas.onpointerdown = event => {
|
||||
if (event.pointerType === 'mouse') {
|
||||
if (event.button !== 0) return;
|
||||
if (event.pointerType !== 'mouse' || event.button !== 0) return;
|
||||
const startX = canvasX(event.clientX);
|
||||
drag = { pointerId: event.pointerId, startX, lastX: startX, moved: false };
|
||||
hide(); hideSelection();
|
||||
canvas.setPointerCapture?.(event.pointerId);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.pointerType === 'touch') {
|
||||
touches.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
||||
if (touches.size === 1) {
|
||||
touchPan = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, scrollLeft: wrap.scrollLeft, moved: false };
|
||||
showFromClientX(event.clientX);
|
||||
}
|
||||
if (touches.size === 2) {
|
||||
touchPan = null;
|
||||
const points = [...touches.values()];
|
||||
const distance = Math.hypot(points[0].x - points[1].x, points[0].y - points[1].y);
|
||||
const midX = (points[0].x + points[1].x) / 2;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const wrapRect = wrap.getBoundingClientRect();
|
||||
pinch = {
|
||||
startDistance: Math.max(1, distance),
|
||||
startZoom: clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM),
|
||||
baseWidth: (rect.width || width) / clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM),
|
||||
anchorRatio: clamp((midX - rect.left) / Math.max(1, rect.width), 0, 1),
|
||||
viewportX: clamp(midX - wrapRect.left, 0, wrap.clientWidth),
|
||||
previewZoom: clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM),
|
||||
};
|
||||
hide();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
canvas.onpointermove = event => {
|
||||
if (event.pointerType === 'touch') return;
|
||||
if (event.pointerType === 'mouse' && drag?.pointerId === event.pointerId) {
|
||||
updateSelection(canvasX(event.clientX));
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.pointerType === 'touch' && touches.has(event.pointerId)) {
|
||||
touches.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
||||
if (pinch && touches.size >= 2) {
|
||||
const points = [...touches.values()].slice(0, 2);
|
||||
const distance = Math.hypot(points[0].x - points[1].x, points[0].y - points[1].y);
|
||||
pinch.previewZoom = clamp(pinch.startZoom * (distance / pinch.startDistance), 1, MAX_CHART_ZOOM);
|
||||
const previewWidth = pinch.baseWidth * pinch.previewZoom;
|
||||
canvas.style.width = `${previewWidth}px`;
|
||||
wrap.scrollLeft = Math.max(0, pinch.anchorRatio * previewWidth - pinch.viewportX);
|
||||
const reset = canvas.closest('.history-chart-card')?.querySelector('[data-chart-zoom="reset"]');
|
||||
if (reset) reset.textContent = `${Math.round(pinch.previewZoom * 100)}%`;
|
||||
event.preventDefault();
|
||||
} else if (touchPan?.pointerId === event.pointerId && touches.size === 1) {
|
||||
const dx = event.clientX - touchPan.startX;
|
||||
const dy = event.clientY - touchPan.startY;
|
||||
if (Math.abs(dx) > 6 && Math.abs(dx) > Math.abs(dy)) {
|
||||
touchPan.moved = true;
|
||||
hide();
|
||||
wrap.scrollLeft = Math.max(0, touchPan.scrollLeft - dx);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
showFromClientX(event.clientX);
|
||||
};
|
||||
|
||||
const finishPointer = event => {
|
||||
if (event.pointerType === 'mouse' && drag?.pointerId === event.pointerId) {
|
||||
if (event.pointerType !== 'mouse' || drag?.pointerId !== event.pointerId) return;
|
||||
const currentX = canvasX(event.clientX);
|
||||
const selectedWidth = Math.abs(currentX - drag.startX);
|
||||
const center = (currentX + drag.startX) / 2;
|
||||
@@ -405,32 +350,11 @@ function bindChartTooltip(canvas, series, sortedRows, geometry) {
|
||||
const factor = Math.max(1, wrap.clientWidth / Math.max(1, selectedWidth));
|
||||
setChartZoom(canvas.id, currentZoom * factor, center / Math.max(1, width), wrap.clientWidth / 2);
|
||||
} else showFromClientX(event.clientX);
|
||||
return;
|
||||
}
|
||||
if (event.pointerType === 'touch') {
|
||||
const wasPan = touchPan?.pointerId === event.pointerId ? touchPan : null;
|
||||
touches.delete(event.pointerId);
|
||||
if (pinch && touches.size < 2) {
|
||||
const completed = pinch;
|
||||
pinch = null;
|
||||
setChartZoom(canvas.id, completed.previewZoom, completed.anchorRatio, completed.viewportX);
|
||||
} else if (wasPan && !wasPan.moved) showFromClientX(event.clientX);
|
||||
if (touches.size === 0) touchPan = null;
|
||||
}
|
||||
};
|
||||
|
||||
canvas.onpointerup = finishPointer;
|
||||
canvas.onpointercancel = event => {
|
||||
if (event.pointerType === 'mouse' && drag?.pointerId === event.pointerId) { drag = null; hideSelection(); }
|
||||
if (event.pointerType === 'touch') {
|
||||
touches.delete(event.pointerId);
|
||||
if (touchPan?.pointerId === event.pointerId) touchPan = null;
|
||||
if (pinch && touches.size < 2) {
|
||||
const completed = pinch;
|
||||
pinch = null;
|
||||
setChartZoom(canvas.id, completed.previewZoom, completed.anchorRatio, completed.viewportX);
|
||||
} else hide();
|
||||
}
|
||||
};
|
||||
canvas.onpointerleave = event => { if (event.pointerType !== 'touch' && !drag) hide(); };
|
||||
canvas.onkeydown = event => {
|
||||
|
||||
@@ -307,11 +307,6 @@ function setTheme(theme) {
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
function cycleTheme() {
|
||||
const modes = ['system', 'light', 'dark'];
|
||||
const index = modes.indexOf(app.theme);
|
||||
setTheme(modes[(index + 1) % modes.length]);
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const headers = new Headers(options.headers || {});
|
||||
|
||||
+1
-1
@@ -406,7 +406,7 @@ function renderSimulationPage() {
|
||||
}).join('') : `<div class="empty">${esc(tr('plan.noEvents'))}</div>`;
|
||||
|
||||
const activeRules = (plan.rules || []).filter(rule => rule.enabled && simulationRuleVisible(rule, zones));
|
||||
rulesHost.innerHTML = activeRules.length ? activeRules.map(rule => `<article class="panel simulation-rule-card"><div class="simulation-rule-head"><div><span class="eyebrow">${esc(tr('common.automation'))}</span><h3>${esc(rule.name)}</h3></div><span class="badge active">${esc(automationTriggerLabel(rule))}</span></div><p>${esc(tr('simulation.ruleOnDevice', { device: rule.action_device_name || tr('common.noDevice') }))}</p><div class="simulation-rule-action">${esc(simulationRuleAction(rule))}</div><div class="simulation-rule-meta"><small>${esc(tr('automations.last'))}: ${esc(dateTime(rule.last_fired_at))}</small><small>${esc(tr('simulation.nextReady'))}: ${esc(dateTime(rule.next_ready_at))}</small></div></article>`).join('') : `<div class="empty">${esc(tr('simulation.noRules'))}</div>`;
|
||||
rulesHost.innerHTML = activeRules.length ? activeRules.map(rule => `<article class="simulation-rule-card"><div class="simulation-rule-head"><div><span class="eyebrow">${esc(tr('common.automation'))}</span><h3>${esc(rule.name)}</h3></div><span class="badge active">${esc(automationTriggerLabel(rule))}</span></div><p>${esc(tr('simulation.ruleOnDevice', { device: rule.action_device_name || tr('common.noDevice') }))}</p><div class="simulation-rule-action">${esc(simulationRuleAction(rule))}</div><div class="simulation-rule-meta"><small>${esc(tr('automations.last'))}: ${esc(dateTime(rule.last_fired_at))}</small><small>${esc(tr('simulation.nextReady'))}: ${esc(dateTime(rule.next_ready_at))}</small></div></article>`).join('') : `<div class="empty">${esc(tr('simulation.noRules'))}</div>`;
|
||||
}
|
||||
|
||||
function scheduleControlPlanLoad() {
|
||||
|
||||
@@ -10,9 +10,6 @@ function deviceFeaturePanel(device) {
|
||||
return `<div class="device-capability-panel"><small>${esc(tr('devices.features'))}</small><div class="device-capability-buttons">${features.map(([field, , label]) => `<button class="${device[field] ? 'active' : ''}" data-action="toggle" data-field="${field}" data-device="${esc(device.id)}">${esc(label)}</button>`).join('')}</div></div>`;
|
||||
}
|
||||
|
||||
function deviceZoneDisabled(deviceId) {
|
||||
return app.zones.some(zone => zone.device_id === deviceId && zone.enabled === false);
|
||||
}
|
||||
|
||||
function zoneForDevice(deviceId) {
|
||||
return app.zones.find(zone => zone.device_id === deviceId) || null;
|
||||
|
||||
+75
-606
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user