node-red-contrib-senec-cloud-v2 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/assets/fonts/senec-sans.ttf +0 -0
  3. package/dist/lib/dashboard-html-renderer.d.ts +18 -0
  4. package/dist/lib/dashboard-html-renderer.d.ts.map +1 -0
  5. package/dist/lib/dashboard-html-renderer.js +604 -0
  6. package/dist/lib/dashboard-html-renderer.js.map +1 -0
  7. package/dist/lib/dashboard-layout.d.ts +152 -0
  8. package/dist/lib/dashboard-layout.d.ts.map +1 -0
  9. package/dist/lib/dashboard-layout.js +201 -0
  10. package/dist/lib/dashboard-layout.js.map +1 -0
  11. package/dist/lib/geocoding-client.d.ts +61 -0
  12. package/dist/lib/geocoding-client.d.ts.map +1 -0
  13. package/dist/lib/geocoding-client.js +77 -0
  14. package/dist/lib/geocoding-client.js.map +1 -0
  15. package/dist/lib/senec-image-renderer.d.ts +107 -0
  16. package/dist/lib/senec-image-renderer.d.ts.map +1 -0
  17. package/dist/lib/senec-image-renderer.js +872 -0
  18. package/dist/lib/senec-image-renderer.js.map +1 -0
  19. package/dist/lib/senec-layout.d.ts +212 -0
  20. package/dist/lib/senec-layout.d.ts.map +1 -0
  21. package/dist/lib/senec-layout.js +252 -0
  22. package/dist/lib/senec-layout.js.map +1 -0
  23. package/dist/lib/weather-client.d.ts +62 -0
  24. package/dist/lib/weather-client.d.ts.map +1 -0
  25. package/dist/lib/weather-client.js +234 -0
  26. package/dist/lib/weather-client.js.map +1 -0
  27. package/dist/nodes/senec-data.js +10 -2
  28. package/dist/nodes/senec-data.js.map +1 -1
  29. package/dist/nodes/senec-image.html +73 -53
  30. package/dist/nodes/senec-image.js +189 -14
  31. package/dist/nodes/senec-image.js.map +1 -1
  32. package/dist/nodes/weather.d.ts +2 -0
  33. package/dist/nodes/weather.d.ts.map +1 -0
  34. package/dist/nodes/weather.html +179 -0
  35. package/dist/nodes/weather.js +138 -0
  36. package/dist/nodes/weather.js.map +1 -0
  37. package/package.json +4 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.2] - 2026-07-05
9
+
10
+ ### Fixed
11
+ - **Weather fetch resilience**: The weather node no longer drops to its
12
+ fallback payload on the first transient Open-Meteo error (e.g. HTTP 503
13
+ Service Unavailable, HTTP 429 rate limiting, timeouts or network errors).
14
+ `WeatherClient.getWeather` now retries transient failures with exponential
15
+ backoff (configurable via `retries` / `retryDelay`, default 2 retries)
16
+ before giving up. Non-transient errors (HTTP 4xx other than 429) still fail
17
+ immediately, and the last-good/sample fallback remains as a last resort.
18
+
19
+ ## [0.2.1] - 2026-07-05
20
+
21
+ ### Fixed
22
+ - **Battery "Heute" (today) value**: The daily battery charge/discharge was
23
+ derived from an energy-balance formula whose two halves were exact negatives
24
+ of each other, so the "aus / ein" split could never be consistent and the
25
+ displayed value was misleading. It now computes a single, physically correct
26
+ net battery energy for the day
27
+ (`net = consumption + gridExport - generation - gridImport`) and shows the
28
+ net discharge/charge accordingly.
29
+
8
30
  ## [0.2.0] - 2026-07-05
9
31
 
10
32
  ### Fixed
Binary file
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Dashboard HTML Renderer
3
+ *
4
+ * Produces the fully resizable 16:9 energy dashboard as a standalone HTML
5
+ * document, faithfully adapting the reference spec in
6
+ * docs/00-input/ux/index.html while binding live SENEC + Weather values
7
+ * from the shared {@link DashboardModel}.
8
+ *
9
+ * The output is a self-contained HTML string (inline CSS, no external
10
+ * assets) suitable for output on the second pin of the senec-image node,
11
+ * for embedding in an iframe, or for serving via an HTTP response node.
12
+ */
13
+ import { DashboardModel } from './dashboard-layout';
14
+ /**
15
+ * Render the complete dashboard HTML document for the given model.
16
+ */
17
+ export declare function renderDashboardHtml(model: DashboardModel): string;
18
+ //# sourceMappingURL=dashboard-html-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-html-renderer.d.ts","sourceRoot":"","sources":["../../src/lib/dashboard-html-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAe,cAAc,EAAa,MAAM,oBAAoB,CAAC;AA2I5E;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAkejE"}
@@ -0,0 +1,604 @@
1
+ "use strict";
2
+ /**
3
+ * Dashboard HTML Renderer
4
+ *
5
+ * Produces the fully resizable 16:9 energy dashboard as a standalone HTML
6
+ * document, faithfully adapting the reference spec in
7
+ * docs/00-input/ux/index.html while binding live SENEC + Weather values
8
+ * from the shared {@link DashboardModel}.
9
+ *
10
+ * The output is a self-contained HTML string (inline CSS, no external
11
+ * assets) suitable for output on the second pin of the senec-image node,
12
+ * for embedding in an iframe, or for serving via an HTTP response node.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.renderDashboardHtml = renderDashboardHtml;
16
+ const dashboard_layout_1 = require("./dashboard-layout");
17
+ /** Escape a string for safe insertion into HTML text/attributes. */
18
+ function esc(value) {
19
+ return String(value)
20
+ .replace(/&/g, '&')
21
+ .replace(/</g, '&lt;')
22
+ .replace(/>/g, '&gt;')
23
+ .replace(/"/g, '&quot;')
24
+ .replace(/'/g, '&#39;')
25
+ .replace(/`/g, '&#96;')
26
+ .replace(/\//g, '&#47;');
27
+ }
28
+ /** Render a single energy node for the "Aktuell" (current) card. */
29
+ function currentNode(cls, posCls, v) {
30
+ return (`<div class="energy-node ${cls} ${posCls}">` +
31
+ `<div class="label">${esc(v.label)}</div>` +
32
+ `<div class="value">${esc(v.value)}</div>` +
33
+ `<div class="unit">${esc(v.unit)}</div>` +
34
+ `</div>`);
35
+ }
36
+ /** Render a single energy node for the "Heute" (today) card. */
37
+ function todayNode(cls, posCls, v) {
38
+ if (v.dual) {
39
+ return (`<div class="energy-node ${cls} ${posCls}">` +
40
+ `<div class="label">${esc(v.label)}</div>` +
41
+ `<div class="small-value">${esc(v.dual)}</div>` +
42
+ `<div class="hint">${esc(v.hint || '')}</div>` +
43
+ `<div class="unit">${esc(v.unit)}</div>` +
44
+ `</div>`);
45
+ }
46
+ return (`<div class="energy-node ${cls} ${posCls}">` +
47
+ `<div class="label">${esc(v.label)}</div>` +
48
+ `<div class="value">${esc(v.value)}</div>` +
49
+ `<div class="unit">${esc(v.unit)}</div>` +
50
+ `</div>`);
51
+ }
52
+ /** Coerce to a safe integer percentage (0..100) for inline output. */
53
+ function pct(value) {
54
+ const n = Number(value);
55
+ if (!isFinite(n)) {
56
+ return 0;
57
+ }
58
+ return Math.min(100, Math.max(0, Math.round(n)));
59
+ }
60
+ /** Degrees for a conic ring given a percentage. */
61
+ function ringDeg(percent) {
62
+ return Math.round((pct(percent) / 100) * 360);
63
+ }
64
+ /** Short HH:MM (local) from an ISO timestamp, or "--:--" if absent. */
65
+ function shortTime(iso) {
66
+ if (!iso) {
67
+ return '--:--';
68
+ }
69
+ const d = new Date(iso);
70
+ if (isNaN(d.getTime())) {
71
+ return '--:--';
72
+ }
73
+ const hh = String(d.getHours()).padStart(2, '0');
74
+ const mm = String(d.getMinutes()).padStart(2, '0');
75
+ return `${hh}:${mm}`;
76
+ }
77
+ /** Render one status chip (SENEC / Weather). */
78
+ function statusChip(label, s) {
79
+ const ok = !!s.ok;
80
+ const dotColor = ok ? '#1fb141' : s.fallback ? '#ff8a00' : '#e5484d';
81
+ const stateText = ok ? 'OK' : s.fallback ? 'Fallback' : 'Fehler';
82
+ return (`<div class="status-chip">` +
83
+ `<span class="status-dot" style="background:${dotColor}"></span>` +
84
+ `<span class="status-label">${esc(label)}</span>` +
85
+ `<span class="status-state">${esc(stateText)}</span>` +
86
+ `<span class="status-time">${esc(shortTime(s.timestamp))}</span>` +
87
+ `</div>`);
88
+ }
89
+ function renderWeatherCard(model) {
90
+ const w = model.weather;
91
+ if (!w) {
92
+ return (`<section class="card">` +
93
+ `<h1 class="title">${esc(dashboard_layout_1.CARD_TITLES.weather)}</h1>` +
94
+ `<div class="weather-layout"><div class="current-weather">` +
95
+ `<div class="weather-icon">❓</div>` +
96
+ `<div><div class="temp">--</div><div class="condition">Keine Wetterdaten</div></div>` +
97
+ `</div></div></section>`);
98
+ }
99
+ const forecast = w.forecast
100
+ .slice(0, 5)
101
+ .map((f) => `<div class="forecast-item">` +
102
+ `<div class="forecast-day">${esc(f.day)}</div>` +
103
+ `<div class="forecast-ico">${esc(f.icon)}</div>` +
104
+ `<div class="forecast-temp">${esc(String(f.temp))}°</div>` +
105
+ `</div>`)
106
+ .join('');
107
+ const locationLine = w.location
108
+ ? `<div class="weather-location">📍 ${esc(w.location)}</div>`
109
+ : '';
110
+ return (`<section class="card">` +
111
+ `<h1 class="title">${esc(dashboard_layout_1.CARD_TITLES.weather)}</h1>` +
112
+ locationLine +
113
+ `<div class="weather-layout">` +
114
+ `<div class="current-weather">` +
115
+ `<div class="weather-icon">${esc(w.icon)}</div>` +
116
+ `<div><div class="temp">${esc(String(w.temperature))}°C</div>` +
117
+ `<div class="condition">${esc(w.condition)}</div></div>` +
118
+ `</div>` +
119
+ `<div class="metrics">` +
120
+ `<div class="metric"><div class="mi">🌡️</div><div><div class="mv">${esc(String(w.tempMax))}° / ${esc(String(w.tempMin))}°</div><div class="ml">max / min</div></div></div>` +
121
+ `<div class="metric"><div class="mi">💧</div><div><div class="mv">${esc(String(w.humidity))}%</div><div class="ml">Luftfeuchte</div></div></div>` +
122
+ `<div class="metric"><div class="mi">🌬️</div><div><div class="mv">${esc(String(w.windSpeed))} km/h</div><div class="ml">${esc(w.windDirection)}</div></div></div>` +
123
+ `</div>` +
124
+ `<div class="forecast">${forecast}</div>` +
125
+ `</div></section>`);
126
+ }
127
+ /**
128
+ * Render the complete dashboard HTML document for the given model.
129
+ */
130
+ function renderDashboardHtml(model) {
131
+ const c = model.current;
132
+ const t = model.today;
133
+ const a = model.autarky;
134
+ const currentCard = `<section class="card">` +
135
+ `<h1 class="title">${esc(dashboard_layout_1.CARD_TITLES.current)}</h1>` +
136
+ `<div class="flow-area">` +
137
+ `<div class="connector c-solar"></div>` +
138
+ `<div class="connector c-grid"></div>` +
139
+ `<div class="connector c-battery"></div>` +
140
+ `<div class="connector c-wallbox"></div>` +
141
+ `<div class="connector c-consumption"></div>` +
142
+ `<div class="hub"></div>` +
143
+ currentNode('solar', 'pos-solar', c.solar) +
144
+ currentNode('grid', 'pos-grid', c.grid) +
145
+ currentNode('battery', 'pos-battery', c.battery) +
146
+ currentNode('orange', 'pos-wallbox', c.wallbox) +
147
+ currentNode('orange', 'pos-consumption', c.consumption) +
148
+ `</div></section>`;
149
+ const todayCard = `<section class="card">` +
150
+ `<h1 class="title">${esc(dashboard_layout_1.CARD_TITLES.today)}</h1>` +
151
+ `<div class="flow-area">` +
152
+ `<div class="connector c-solar"></div>` +
153
+ `<div class="connector c-grid"></div>` +
154
+ `<div class="connector c-battery"></div>` +
155
+ `<div class="connector c-wallbox"></div>` +
156
+ `<div class="connector c-consumption"></div>` +
157
+ `<div class="hub"></div>` +
158
+ todayNode('solar', 'pos-solar', t.solar) +
159
+ todayNode('grid', 'pos-grid', t.grid) +
160
+ todayNode('battery', 'pos-battery', t.battery) +
161
+ todayNode('orange', 'pos-wallbox', t.wallbox) +
162
+ todayNode('orange', 'pos-consumption', t.consumption) +
163
+ `</div></section>`;
164
+ const autarkyCard = `<section class="card">` +
165
+ `<h1 class="title">${esc(dashboard_layout_1.CARD_TITLES.autarky)}</h1>` +
166
+ `<div class="autarky-grid">` +
167
+ `<div class="kpi"><div class="kpi-label">Heute</div><div class="percent">${pct(a.todayPercent)}%</div>` +
168
+ `<div class="ring" style="--p:${ringDeg(a.todayPercent)}deg; --ring-color:#1fb141" data-icon="🍃"></div></div>` +
169
+ `<div class="kpi"><div class="kpi-label">Aktuell</div><div class="percent">${pct(a.currentPercent)}%</div>` +
170
+ `<div class="ring" style="--p:${ringDeg(a.currentPercent)}deg; --ring-color:#1fb141" data-icon="⚡"></div></div>` +
171
+ `<div class="kpi"><div class="kpi-label">Speicherstatus</div><div class="percent" style="color:#2e7bea">${pct(a.batterySocPercent)}%</div>` +
172
+ `<div class="ring" style="--p:${ringDeg(a.batterySocPercent)}deg; --ring-color:#2e7bea" data-icon="🔋"></div></div>` +
173
+ `</div></section>`;
174
+ const weatherCard = renderWeatherCard(model);
175
+ const statusBar = `<div class="status-bar">` +
176
+ statusChip('SENEC', model.status.senec) +
177
+ statusChip('Wetter-API', model.status.weather) +
178
+ `</div>`;
179
+ return `<!doctype html>
180
+ <html lang="de">
181
+ <head>
182
+ <meta charset="utf-8" />
183
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
184
+ <title>Energy Dashboard</title>
185
+ <style>
186
+ :root {
187
+ --blue: #2438c5;
188
+ --solar: #fdb913;
189
+ --solar-hi: #ffd84d;
190
+ --battery: #1237e6;
191
+ --battery-hi: #3157ff;
192
+ --orange: #ff8a00;
193
+ --orange-hi: #ffad33;
194
+ --gray: #8a8a8a;
195
+ --gray-hi: #a8a8a8;
196
+ --green: #1fb141;
197
+ --status-blue: #2e7bea;
198
+ --ring-bg: #e6eaf0;
199
+ --border: #e8e8e8;
200
+ --muted: #6b7280;
201
+ --main: #111827;
202
+ --page: #f3f4f6;
203
+ --white: #ffffff;
204
+ }
205
+ * { box-sizing: border-box; }
206
+ html, body {
207
+ margin: 0;
208
+ min-height: 100%;
209
+ background: var(--page);
210
+ font-family: "Segoe UI", Arial, sans-serif;
211
+ color: var(--main);
212
+ }
213
+ body {
214
+ min-height: 100vh;
215
+ display: grid;
216
+ place-items: center;
217
+ padding: 2vmin;
218
+ }
219
+ .dashboard-wrap {
220
+ width: min(96vw, 1600px);
221
+ max-height: 96vh;
222
+ aspect-ratio: 16 / 9;
223
+ container-type: inline-size;
224
+ }
225
+ .dashboard {
226
+ --u: 1cqw;
227
+ --outer: clamp(10px, calc(var(--u) * 1.5), 24px);
228
+ --gap: clamp(10px, calc(var(--u) * 1.5), 24px);
229
+ --card-radius: clamp(10px, calc(var(--u) * 1.125), 18px);
230
+ --title-font: clamp(16px, calc(var(--u) * 1.875), 30px);
231
+ --title-line: calc(var(--title-font) * 1.13);
232
+ --header-h: clamp(30px, calc(var(--u) * 2.875), 46px);
233
+ --card-pad-x: clamp(10px, calc(var(--u) * 1.25), 20px);
234
+ --card-pad-top: clamp(10px, calc(var(--u) * 1.125), 18px);
235
+ --card-pad-bottom: clamp(10px, calc(var(--u) * 1.25), 20px);
236
+ --node-d: clamp(58px, calc(var(--u) * 7.0), 112px);
237
+ --hub-d: calc(var(--node-d) * .375);
238
+ --connector-h: calc(var(--node-d) * .152);
239
+ --node-label-font: calc(var(--node-d) * .152);
240
+ --node-value-font: calc(var(--node-d) * .357);
241
+ --node-unit-font: calc(var(--node-d) * .134);
242
+ --node-small-font: calc(var(--node-d) * .223);
243
+ --node-hint-font: calc(var(--node-d) * .107);
244
+ --ring-d: clamp(78px, calc(var(--u) * 8.0), 128px);
245
+ --ring-stroke: calc(var(--ring-d) * .105);
246
+ width: 100%;
247
+ height: 100%;
248
+ aspect-ratio: 16 / 9;
249
+ background: var(--white);
250
+ padding: var(--outer);
251
+ display: grid;
252
+ grid-template-columns: 1fr 1fr;
253
+ grid-template-rows: 1fr 1fr;
254
+ gap: var(--gap);
255
+ overflow: hidden;
256
+ }
257
+ .card {
258
+ position: relative;
259
+ min-width: 0;
260
+ min-height: 0;
261
+ background: var(--white);
262
+ border: 1px solid var(--border);
263
+ border-radius: var(--card-radius);
264
+ box-shadow: 0 calc(var(--u) * .18) calc(var(--u) * 1.125) rgba(0,0,0,.08);
265
+ overflow: hidden;
266
+ padding: var(--card-pad-top) var(--card-pad-x) var(--card-pad-bottom);
267
+ }
268
+ .title {
269
+ margin: 0;
270
+ height: var(--header-h);
271
+ line-height: var(--title-line);
272
+ text-align: center;
273
+ font-size: var(--title-font);
274
+ color: var(--blue);
275
+ font-weight: 650;
276
+ letter-spacing: .01em;
277
+ }
278
+ .flow-area {
279
+ position: absolute;
280
+ left: var(--card-pad-x);
281
+ right: var(--card-pad-x);
282
+ top: calc(var(--card-pad-top) + var(--header-h));
283
+ bottom: var(--card-pad-bottom);
284
+ }
285
+ .pos-solar { left: 50.0%; top: 16.0%; }
286
+ .pos-grid { left: 23.5%; top: 49.0%; }
287
+ .pos-battery { left: 76.5%; top: 49.0%; }
288
+ .pos-wallbox { left: 34.0%; top: 81.5%; }
289
+ .pos-consumption { left: 66.0%; top: 81.5%; }
290
+ .energy-node {
291
+ position: absolute;
292
+ width: var(--node-d);
293
+ height: var(--node-d);
294
+ transform: translate(-50%, -50%);
295
+ border-radius: 50%;
296
+ display: grid;
297
+ grid-template-rows: auto auto auto auto;
298
+ align-content: center;
299
+ justify-items: center;
300
+ text-align: center;
301
+ color: #fff;
302
+ z-index: 10;
303
+ padding-top: calc(var(--node-d) * .025);
304
+ box-shadow: 0 calc(var(--node-d) * .045) calc(var(--node-d) * .14) rgba(0,0,0,.18);
305
+ }
306
+ .energy-node .label {
307
+ font-size: var(--node-label-font);
308
+ line-height: 1.02;
309
+ font-weight: 450;
310
+ max-width: 92%;
311
+ white-space: nowrap;
312
+ }
313
+ .energy-node .value {
314
+ font-size: var(--node-value-font);
315
+ line-height: .92;
316
+ font-weight: 760;
317
+ letter-spacing: -.035em;
318
+ margin-top: calc(var(--node-d) * .045);
319
+ }
320
+ .energy-node .unit {
321
+ font-size: var(--node-unit-font);
322
+ line-height: 1;
323
+ font-weight: 450;
324
+ margin-top: calc(var(--node-d) * .054);
325
+ }
326
+ .energy-node .small-value {
327
+ font-size: var(--node-small-font);
328
+ line-height: .95;
329
+ font-weight: 760;
330
+ letter-spacing: -.035em;
331
+ margin-top: calc(var(--node-d) * .054);
332
+ }
333
+ .energy-node .hint {
334
+ font-size: var(--node-hint-font);
335
+ line-height: 1;
336
+ opacity: .95;
337
+ margin-top: calc(var(--node-d) * .036);
338
+ }
339
+ .solar { background: radial-gradient(circle at 35% 25%, var(--solar-hi), var(--solar)); }
340
+ .battery { background: radial-gradient(circle at 35% 25%, var(--battery-hi), var(--battery)); }
341
+ .grid { background: radial-gradient(circle at 35% 25%, var(--gray-hi), var(--gray)); }
342
+ .orange { background: radial-gradient(circle at 35% 25%, var(--orange-hi), var(--orange)); }
343
+ .hub {
344
+ position: absolute;
345
+ left: 50%;
346
+ top: 50%;
347
+ width: var(--hub-d);
348
+ height: var(--hub-d);
349
+ transform: translate(-50%, -50%);
350
+ border-radius: 50%;
351
+ background: #fff;
352
+ box-shadow: 0 calc(var(--hub-d) * .07) calc(var(--hub-d) * .31) rgba(0,0,0,.18);
353
+ z-index: 5;
354
+ }
355
+ .connector {
356
+ position: absolute;
357
+ left: 50%;
358
+ top: 50%;
359
+ height: var(--connector-h);
360
+ border-radius: 999px;
361
+ transform-origin: left center;
362
+ z-index: 2;
363
+ overflow: hidden;
364
+ opacity: .84;
365
+ }
366
+ .connector::after {
367
+ content: "› › › › ›";
368
+ position: absolute;
369
+ left: 0;
370
+ top: calc(var(--connector-h) * -.40);
371
+ color: rgba(255,255,255,.44);
372
+ font-size: calc(var(--connector-h) * 1.47);
373
+ letter-spacing: calc(var(--connector-h) * .52);
374
+ white-space: nowrap;
375
+ animation: flow 1.25s linear infinite;
376
+ }
377
+ @keyframes flow {
378
+ from { transform: translateX(calc(var(--connector-h) * -1.4)); }
379
+ to { transform: translateX(calc(var(--connector-h) * 1.2)); }
380
+ }
381
+ /*
382
+ * Horizontal / diagonal connectors run from the central hub (left/top 50%)
383
+ * outward to each node. transform-origin is the hub end (left center); each
384
+ * bar is rotated toward its node. Lengths are tuned so the bars stop at the
385
+ * node edge rather than overshooting past it.
386
+ */
387
+ .c-grid { width: 19.0%; transform: rotate(180deg); background: linear-gradient(90deg, rgba(138,138,138,.12), rgba(138,138,138,.72)); }
388
+ .c-battery { width: 19.0%; transform: rotate(0deg); background: linear-gradient(90deg, rgba(18,55,230,.12), rgba(18,55,230,.72)); }
389
+ .c-wallbox { width: 21.0%; transform: rotate(125deg); background: linear-gradient(90deg, rgba(255,138,0,.12), rgba(255,138,0,.72)); }
390
+ .c-consumption { width: 21.0%; transform: rotate(55deg); background: linear-gradient(90deg, rgba(255,138,0,.12), rgba(255,138,0,.72)); }
391
+ /*
392
+ * The solar connector is vertical. Rotating a width-based bar overshoots on
393
+ * wide flow-areas (this produced a stray line running down through the
394
+ * centre). Instead it is drawn as a real vertical bar whose HEIGHT spans the
395
+ * gap between the solar node and the hub, so it can never overshoot.
396
+ */
397
+ .c-solar {
398
+ left: 50%;
399
+ top: 22.0%;
400
+ width: var(--connector-h);
401
+ height: 28.0%;
402
+ transform: translateX(-50%);
403
+ transform-origin: center top;
404
+ background: linear-gradient(180deg, rgba(253,185,19,.75), rgba(253,185,19,.10));
405
+ }
406
+ .c-solar::after { display: none; }
407
+ .autarky-grid {
408
+ height: calc(100% - var(--header-h));
409
+ display: grid;
410
+ grid-template-columns: repeat(3, 1fr);
411
+ align-items: center;
412
+ gap: calc(var(--u) * .5);
413
+ padding: 0 calc(var(--u) * .4) calc(var(--u) * .6);
414
+ }
415
+ .kpi {
416
+ min-width: 0;
417
+ height: 100%;
418
+ display: grid;
419
+ grid-template-rows: calc(var(--node-d) * .26) calc(var(--node-d) * .46) calc(var(--ring-d) * 1.02);
420
+ justify-items: center;
421
+ align-content: center;
422
+ text-align: center;
423
+ }
424
+ .kpi-label {
425
+ color: var(--blue);
426
+ font-size: calc(var(--node-d) * .168);
427
+ line-height: 1.25;
428
+ font-weight: 650;
429
+ white-space: nowrap;
430
+ }
431
+ .percent {
432
+ font-size: calc(var(--node-d) * .375);
433
+ line-height: 1.12;
434
+ font-weight: 800;
435
+ letter-spacing: -.04em;
436
+ color: var(--green);
437
+ }
438
+ .ring {
439
+ --p: 90deg;
440
+ --ring-color: var(--green);
441
+ width: var(--ring-d);
442
+ height: var(--ring-d);
443
+ border-radius: 50%;
444
+ background: conic-gradient(var(--ring-color) var(--p), var(--ring-bg) 0);
445
+ display: grid;
446
+ place-items: center;
447
+ }
448
+ .ring::before {
449
+ content: attr(data-icon);
450
+ width: calc(var(--ring-d) - 2 * var(--ring-stroke));
451
+ height: calc(var(--ring-d) - 2 * var(--ring-stroke));
452
+ border-radius: 50%;
453
+ background: white;
454
+ display: grid;
455
+ place-items: center;
456
+ font-size: calc(var(--ring-d) * .327);
457
+ }
458
+ .weather-layout {
459
+ height: calc(100% - var(--header-h));
460
+ display: grid;
461
+ grid-template-columns: 1.18fr .92fr;
462
+ grid-template-rows: 1.06fr .84fr;
463
+ gap: calc(var(--u) * .5) calc(var(--u) * 1.375);
464
+ padding: calc(var(--u) * .125) calc(var(--u) * .5) calc(var(--u) * .125);
465
+ }
466
+ .current-weather {
467
+ display: grid;
468
+ grid-template-columns: calc(var(--node-d) * 1.09) 1fr;
469
+ align-items: center;
470
+ gap: calc(var(--u) * .5);
471
+ }
472
+ .weather-icon {
473
+ font-size: calc(var(--node-d) * .768);
474
+ line-height: 1;
475
+ text-align: right;
476
+ }
477
+ .temp {
478
+ font-size: calc(var(--node-d) * .589);
479
+ line-height: .97;
480
+ font-weight: 300;
481
+ letter-spacing: -.06em;
482
+ }
483
+ .condition {
484
+ color: var(--muted);
485
+ font-size: calc(var(--node-d) * .214);
486
+ line-height: 1.25;
487
+ margin-top: calc(var(--node-d) * .054);
488
+ white-space: nowrap;
489
+ }
490
+ .metrics {
491
+ display: grid;
492
+ align-content: center;
493
+ gap: calc(var(--node-d) * .098);
494
+ }
495
+ .metric {
496
+ display: grid;
497
+ grid-template-columns: calc(var(--node-d) * .30) 1fr;
498
+ column-gap: calc(var(--node-d) * .071);
499
+ align-items: center;
500
+ }
501
+ .mi {
502
+ color: var(--blue);
503
+ font-size: calc(var(--node-d) * .223);
504
+ text-align: center;
505
+ line-height: 1;
506
+ }
507
+ .mv {
508
+ font-size: calc(var(--node-d) * .241);
509
+ line-height: 1.07;
510
+ font-weight: 720;
511
+ letter-spacing: -.03em;
512
+ }
513
+ .ml {
514
+ color: var(--muted);
515
+ font-size: calc(var(--node-d) * .125);
516
+ line-height: 1.15;
517
+ }
518
+ .forecast {
519
+ grid-column: 1 / -1;
520
+ display: grid;
521
+ grid-template-columns: repeat(5, 1fr);
522
+ align-items: center;
523
+ border-top: 1px solid #edf0f4;
524
+ padding-top: calc(var(--node-d) * .098);
525
+ }
526
+ .forecast-item {
527
+ text-align: center;
528
+ border-right: 1px solid #edf0f4;
529
+ }
530
+ .forecast-item:last-child { border-right: 0; }
531
+ .forecast-day {
532
+ color: var(--blue);
533
+ font-size: calc(var(--node-d) * .152);
534
+ line-height: 1.18;
535
+ font-weight: 700;
536
+ }
537
+ .forecast-ico {
538
+ font-size: calc(var(--node-d) * .304);
539
+ line-height: 1.12;
540
+ margin: calc(var(--node-d) * .027) 0;
541
+ }
542
+ .forecast-temp {
543
+ font-size: calc(var(--node-d) * .152);
544
+ line-height: 1.18;
545
+ color: var(--muted);
546
+ font-weight: 650;
547
+ }
548
+ .weather-location {
549
+ text-align: center;
550
+ color: var(--muted);
551
+ font-size: clamp(9px, calc(var(--u) * 1.0), 15px);
552
+ line-height: 1.2;
553
+ margin-top: calc(var(--header-h) * -0.2);
554
+ margin-bottom: calc(var(--u) * 0.2);
555
+ }
556
+ .status-bar {
557
+ width: min(96vw, 1600px);
558
+ display: flex;
559
+ justify-content: flex-end;
560
+ gap: 1.5vmin;
561
+ padding: 0.6vmin 0.4vmin 0;
562
+ font-family: "Segoe UI", Arial, sans-serif;
563
+ }
564
+ .status-chip {
565
+ display: inline-flex;
566
+ align-items: center;
567
+ gap: 0.6vmin;
568
+ font-size: clamp(9px, 1.2vmin, 14px);
569
+ color: var(--muted);
570
+ }
571
+ .status-dot {
572
+ width: clamp(6px, 1vmin, 10px);
573
+ height: clamp(6px, 1vmin, 10px);
574
+ border-radius: 50%;
575
+ display: inline-block;
576
+ }
577
+ .status-label { font-weight: 650; color: var(--main); }
578
+ .status-state { font-weight: 600; }
579
+ .status-time { font-variant-numeric: tabular-nums; }
580
+ @supports not (width: 1cqw) {
581
+ .dashboard { --u: min(1vw, 16px); }
582
+ }
583
+ @media (max-width: 760px) {
584
+ body { padding: 0; }
585
+ .dashboard-wrap { width: 100vw; max-height: none; }
586
+ .energy-node .label { font-weight: 500; }
587
+ .condition { white-space: normal; }
588
+ }
589
+ </style>
590
+ </head>
591
+ <body>
592
+ <div class="dashboard-wrap">
593
+ <main class="dashboard" aria-label="Energy dashboard">
594
+ ${currentCard}
595
+ ${todayCard}
596
+ ${autarkyCard}
597
+ ${weatherCard}
598
+ </main>
599
+ ${statusBar}
600
+ </div>
601
+ </body>
602
+ </html>`;
603
+ }
604
+ //# sourceMappingURL=dashboard-html-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-html-renderer.js","sourceRoot":"","sources":["../../src/lib/dashboard-html-renderer.ts"],"names":[],"mappings":"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"}