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

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 +11 -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 +42 -0
  24. package/dist/lib/weather-client.d.ts.map +1 -0
  25. package/dist/lib/weather-client.js +193 -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
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ /**
3
+ * Weather Client
4
+ *
5
+ * Fetches current weather + a short forecast from the free Open-Meteo API
6
+ * (https://open-meteo.com) which requires no API key, and maps the raw
7
+ * response onto the normalized {@link WeatherData} structure consumed by
8
+ * the dashboard renderer.
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.WeatherClient = void 0;
15
+ const axios_1 = __importDefault(require("axios"));
16
+ const WEATHER_CODES = {
17
+ 0: { icon: '☀️', de: 'Klar', en: 'Clear sky' },
18
+ 1: { icon: '🌤️', de: 'Überwiegend klar', en: 'Mainly clear' },
19
+ 2: { icon: '⛅', de: 'Teilweise bewölkt', en: 'Partly cloudy' },
20
+ 3: { icon: '☁️', de: 'Bewölkt', en: 'Overcast' },
21
+ 45: { icon: '🌫️', de: 'Nebel', en: 'Fog' },
22
+ 48: { icon: '🌫️', de: 'Reifnebel', en: 'Depositing rime fog' },
23
+ 51: { icon: '🌦️', de: 'Leichter Nieselregen', en: 'Light drizzle' },
24
+ 53: { icon: '🌦️', de: 'Nieselregen', en: 'Moderate drizzle' },
25
+ 55: { icon: '🌧️', de: 'Starker Nieselregen', en: 'Dense drizzle' },
26
+ 56: { icon: '🌧️', de: 'Gefrierender Nieselregen', en: 'Freezing drizzle' },
27
+ 57: { icon: '🌧️', de: 'Gefrierender Nieselregen', en: 'Freezing drizzle' },
28
+ 61: { icon: '🌧️', de: 'Leichter Regen', en: 'Slight rain' },
29
+ 63: { icon: '🌧️', de: 'Regen', en: 'Moderate rain' },
30
+ 65: { icon: '🌧️', de: 'Starker Regen', en: 'Heavy rain' },
31
+ 66: { icon: '🌧️', de: 'Gefrierender Regen', en: 'Freezing rain' },
32
+ 67: { icon: '🌧️', de: 'Gefrierender Regen', en: 'Freezing rain' },
33
+ 71: { icon: '🌨️', de: 'Leichter Schneefall', en: 'Slight snow' },
34
+ 73: { icon: '🌨️', de: 'Schneefall', en: 'Moderate snow' },
35
+ 75: { icon: '❄️', de: 'Starker Schneefall', en: 'Heavy snow' },
36
+ 77: { icon: '🌨️', de: 'Schneegriesel', en: 'Snow grains' },
37
+ 80: { icon: '🌦️', de: 'Leichte Schauer', en: 'Slight showers' },
38
+ 81: { icon: '🌧️', de: 'Schauer', en: 'Moderate showers' },
39
+ 82: { icon: '⛈️', de: 'Starke Schauer', en: 'Violent showers' },
40
+ 85: { icon: '🌨️', de: 'Schneeschauer', en: 'Slight snow showers' },
41
+ 86: { icon: '❄️', de: 'Starke Schneeschauer', en: 'Heavy snow showers' },
42
+ 95: { icon: '⛈️', de: 'Gewitter', en: 'Thunderstorm' },
43
+ 96: { icon: '⛈️', de: 'Gewitter mit Hagel', en: 'Thunderstorm with hail' },
44
+ 99: { icon: '⛈️', de: 'Gewitter mit Hagel', en: 'Thunderstorm with hail' },
45
+ };
46
+ const DEFAULT_CODE = { icon: '❓', de: 'Unbekannt', en: 'Unknown' };
47
+ /** Weekday short labels per language. Index 0 = Sunday (JS getDay()). */
48
+ const WEEKDAYS = {
49
+ de: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
50
+ en: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
51
+ };
52
+ /** 16-point compass labels per language. */
53
+ const COMPASS = {
54
+ de: [
55
+ 'N', 'NNO', 'NO', 'ONO', 'O', 'OSO', 'SO', 'SSO',
56
+ 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW',
57
+ ],
58
+ en: [
59
+ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
60
+ 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW',
61
+ ],
62
+ };
63
+ class WeatherClient {
64
+ /**
65
+ * Fetch current weather + a 5-day forecast for the given coordinates.
66
+ */
67
+ async getWeather(options) {
68
+ // Validate coordinates defensively. The request URL host is a fixed
69
+ // constant (no SSRF), but we still guard against non-finite / out-of
70
+ // -range values being serialized into the query string.
71
+ const lat = Number(options.latitude);
72
+ const lon = Number(options.longitude);
73
+ if (!isFinite(lat) || lat < -90 || lat > 90) {
74
+ throw new Error('Invalid latitude (expected -90..90)');
75
+ }
76
+ if (!isFinite(lon) || lon < -180 || lon > 180) {
77
+ throw new Error('Invalid longitude (expected -180..180)');
78
+ }
79
+ const language = (options.language || 'de').toLowerCase();
80
+ const timeout = options.timeout ?? 10000;
81
+ const url = 'https://api.open-meteo.com/v1/forecast';
82
+ const params = {
83
+ latitude: options.latitude,
84
+ longitude: options.longitude,
85
+ current: 'temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,wind_direction_10m',
86
+ daily: 'weather_code,temperature_2m_max,temperature_2m_min',
87
+ timezone: 'auto',
88
+ forecast_days: 5,
89
+ wind_speed_unit: 'kmh',
90
+ };
91
+ const response = await axios_1.default.get(url, { params, timeout });
92
+ const mapped = WeatherClient.mapResponse(response.data, language);
93
+ mapped.latitude = options.latitude;
94
+ mapped.longitude = options.longitude;
95
+ mapped.location =
96
+ options.location && options.location.length > 0
97
+ ? options.location
98
+ : WeatherClient.formatCoords(options.latitude, options.longitude);
99
+ return mapped;
100
+ }
101
+ /**
102
+ * A deterministic sample weather payload, used ONLY as a fallback when
103
+ * the Open-Meteo server cannot be reached.
104
+ */
105
+ static sample(options) {
106
+ const lat = options?.latitude ?? 52.52;
107
+ const lon = options?.longitude ?? 13.405;
108
+ return {
109
+ temperature: 18,
110
+ condition: (options?.language || 'de') === 'en' ? 'Partly cloudy' : 'Leicht bewölkt',
111
+ icon: '🌤️',
112
+ tempMax: 20,
113
+ tempMin: 11,
114
+ humidity: 45,
115
+ windSpeed: 15,
116
+ windDirection: 'WSW',
117
+ forecast: [
118
+ { day: 'Di', icon: '🌤️', temp: 20 },
119
+ { day: 'Mi', icon: '☁️', temp: 19 },
120
+ { day: 'Do', icon: '🌧️', temp: 17 },
121
+ { day: 'Fr', icon: '⛅', temp: 21 },
122
+ { day: 'Sa', icon: '☀️', temp: 24 },
123
+ ],
124
+ latitude: lat,
125
+ longitude: lon,
126
+ location: options?.location && options.location.length > 0
127
+ ? options.location
128
+ : WeatherClient.formatCoords(lat, lon),
129
+ };
130
+ }
131
+ static formatCoords(lat, lon) {
132
+ const fmt = (v) => (Math.round(v * 100) / 100).toFixed(2);
133
+ return `${fmt(lat)}, ${fmt(lon)}`;
134
+ }
135
+ /**
136
+ * Map a raw Open-Meteo forecast response to normalized WeatherData.
137
+ * Exposed statically so it can be unit-tested without network access.
138
+ */
139
+ static mapResponse(data, language = 'de') {
140
+ const lang = WEEKDAYS[language] ? language : 'en';
141
+ const current = data?.current ?? {};
142
+ const daily = data?.daily ?? {};
143
+ const currentCode = Number(current.weather_code ?? 0);
144
+ const info = WEATHER_CODES[currentCode] ?? DEFAULT_CODE;
145
+ const condition = lang === 'de' ? info.de : info.en;
146
+ const maxArr = Array.isArray(daily.temperature_2m_max)
147
+ ? daily.temperature_2m_max
148
+ : [];
149
+ const minArr = Array.isArray(daily.temperature_2m_min)
150
+ ? daily.temperature_2m_min
151
+ : [];
152
+ const codeArr = Array.isArray(daily.weather_code) ? daily.weather_code : [];
153
+ const timeArr = Array.isArray(daily.time) ? daily.time : [];
154
+ const forecast = [];
155
+ for (let i = 0; i < Math.min(5, timeArr.length || maxArr.length); i++) {
156
+ const dayCode = Number(codeArr[i] ?? currentCode);
157
+ const dayInfo = WEATHER_CODES[dayCode] ?? DEFAULT_CODE;
158
+ forecast.push({
159
+ day: WeatherClient.weekdayLabel(timeArr[i], lang),
160
+ icon: dayInfo.icon,
161
+ temp: Math.round(Number(maxArr[i] ?? current.temperature_2m ?? 0)),
162
+ });
163
+ }
164
+ return {
165
+ temperature: Math.round(Number(current.temperature_2m ?? 0)),
166
+ condition,
167
+ icon: info.icon,
168
+ tempMax: Math.round(Number(maxArr[0] ?? current.temperature_2m ?? 0)),
169
+ tempMin: Math.round(Number(minArr[0] ?? current.temperature_2m ?? 0)),
170
+ humidity: Math.round(Number(current.relative_humidity_2m ?? 0)),
171
+ windSpeed: Math.round(Number(current.wind_speed_10m ?? 0)),
172
+ windDirection: WeatherClient.compassLabel(Number(current.wind_direction_10m ?? 0), lang),
173
+ forecast,
174
+ };
175
+ }
176
+ static weekdayLabel(isoDate, lang) {
177
+ const labels = WEEKDAYS[lang] ?? WEEKDAYS.en;
178
+ if (!isoDate) {
179
+ return labels[0];
180
+ }
181
+ const d = new Date(`${isoDate}T00:00:00`);
182
+ const idx = isNaN(d.getTime()) ? 0 : d.getDay();
183
+ return labels[idx];
184
+ }
185
+ static compassLabel(degrees, lang) {
186
+ const labels = COMPASS[lang] ?? COMPASS.en;
187
+ const normalized = ((degrees % 360) + 360) % 360;
188
+ const idx = Math.round(normalized / 22.5) % 16;
189
+ return labels[idx];
190
+ }
191
+ }
192
+ exports.WeatherClient = WeatherClient;
193
+ //# sourceMappingURL=weather-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"weather-client.js","sourceRoot":"","sources":["../../src/lib/weather-client.ts"],"names":[],"mappings":"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"}
@@ -31,18 +31,26 @@ module.exports = function (RED) {
31
31
  async function fetchData() {
32
32
  if (!node.apiClient)
33
33
  return;
34
+ const timestamp = new Date().toISOString();
34
35
  try {
35
36
  node.status({ fill: 'blue', shape: 'dot', text: 'fetching...' });
36
37
  const parsedData = await node.apiClient.getData();
37
38
  node.send({
38
39
  payload: parsedData,
39
- topic: 'senec/data'
40
+ topic: 'senec/data',
41
+ status: { ok: true, timestamp, fallback: false }
40
42
  });
41
- node.status({ fill: 'green', shape: 'dot', text: 'success' });
43
+ node.status({ fill: 'green', shape: 'dot', text: `ok ${timestamp.slice(11, 16)}` });
42
44
  }
43
45
  catch (error) {
44
46
  node.error('Failed to fetch data: ' + error.message);
45
47
  node.status({ fill: 'red', shape: 'ring', text: 'error' });
48
+ // Report the failed status so downstream (image) can render it.
49
+ node.send({
50
+ payload: null,
51
+ topic: 'senec/status',
52
+ status: { ok: false, timestamp, error: error.message, fallback: false }
53
+ });
46
54
  }
47
55
  }
48
56
  // Handle input messages
@@ -1 +1 @@
1
- {"version":3,"file":"senec-data.js","sourceRoot":"","sources":["../../src/nodes/senec-data.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"senec-data.js","sourceRoot":"","sources":["../../src/nodes/senec-data.ts"],"names":[],"mappings":"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"}
@@ -4,17 +4,21 @@
4
4
  color: '#00A0E3',
5
5
  defaults: {
6
6
  name: { value: '' },
7
- width: { value: 800, validate: RED.validators.number() },
8
- height: { value: 480, validate: RED.validators.number() },
9
- layout: { value: 'default' }
7
+ width: { value: 1600, validate: RED.validators.number() },
8
+ height: { value: 0 },
9
+ layout: { value: 'dashboard' },
10
+ outputFormat: { value: 'buffer' },
11
+ filePath: { value: '' }
10
12
  },
11
- inputs: 1,
12
- outputs: 1,
13
+ inputs: 2,
14
+ outputs: 2,
15
+ inputLabels: ['SENEC', 'Weather'],
16
+ outputLabels: ['PNG', 'HTML'],
13
17
  icon: 'font-awesome/fa-image',
14
- label: function() {
18
+ label: function () {
15
19
  return this.name || 'SENEC Image';
16
20
  },
17
- labelStyle: function() {
21
+ labelStyle: function () {
18
22
  return this.name ? 'node_label_italic' : '';
19
23
  }
20
24
  });
@@ -27,68 +31,84 @@
27
31
  </div>
28
32
  <div class="form-row">
29
33
  <label for="node-input-width"><i class="fa fa-arrows-h"></i> Width</label>
30
- <input type="number" id="node-input-width" placeholder="800" min="100">
34
+ <input type="number" id="node-input-width" placeholder="1600" min="200">
31
35
  </div>
32
36
  <div class="form-row">
33
37
  <label for="node-input-height"><i class="fa fa-arrows-v"></i> Height</label>
34
- <input type="number" id="node-input-height" placeholder="480" min="100">
38
+ <input type="number" id="node-input-height" placeholder="0 = auto (16:9)" min="0">
35
39
  </div>
36
40
  <div class="form-row">
37
41
  <label for="node-input-layout"><i class="fa fa-th-large"></i> Layout</label>
38
42
  <select id="node-input-layout">
39
- <option value="default">Default</option>
40
- <option value="compact">Compact</option>
41
- <option value="trmnl">TRMNL Display</option>
43
+ <option value="dashboard">Energy Dashboard (4 cards, 16:9)</option>
42
44
  </select>
43
45
  </div>
46
+ <div class="form-row">
47
+ <label for="node-input-outputFormat"><i class="fa fa-download"></i> PNG Output</label>
48
+ <select id="node-input-outputFormat">
49
+ <option value="buffer">Buffer (PNG)</option>
50
+ <option value="base64">Base64 Data URL</option>
51
+ <option value="file">File</option>
52
+ </select>
53
+ </div>
54
+ <div class="form-row" id="node-row-filePath">
55
+ <label for="node-input-filePath"><i class="fa fa-folder-open"></i> File Path</label>
56
+ <input type="text" id="node-input-filePath" placeholder="/path/to/dashboard.png">
57
+ </div>
44
58
  </script>
45
59
 
46
60
  <script type="text/html" data-help-name="senec-image">
47
- <p>Generates a PNG image visualization of SENEC system data.</p>
48
-
61
+ <p>Renders a 16:9 energy dashboard combining SENEC energy data and weather
62
+ data. Produces a PNG image on output 1 and a fully resizable HTML document
63
+ on output 2, in parallel.</p>
64
+
49
65
  <h3>Inputs</h3>
50
- <dl class="message-properties">
51
- <dt>payload <span class="property-type">object</span></dt>
52
- <dd>SENEC data object (from senec-data node)</dd>
53
- </dl>
54
-
66
+ <ol class="node-ports">
67
+ <li>SENEC
68
+ <dl class="message-properties">
69
+ <dt>payload <span class="property-type">object</span></dt>
70
+ <dd>SENEC data object (from the <b>senec-data</b> node).</dd>
71
+ </dl>
72
+ </li>
73
+ <li>Weather
74
+ <dl class="message-properties">
75
+ <dt>payload <span class="property-type">object</span></dt>
76
+ <dd>Weather data object (from the <b>weather</b> node). Optional; the
77
+ weather card renders a placeholder until data arrives.</dd>
78
+ </dl>
79
+ </li>
80
+ </ol>
81
+
55
82
  <h3>Outputs</h3>
56
- <dl class="message-properties">
57
- <dt>payload <span class="property-type">buffer</span></dt>
58
- <dd>PNG image as a Buffer</dd>
59
-
60
- <dt>contentType <span class="property-type">string</span></dt>
61
- <dd>Set to "image/png"</dd>
62
-
63
- <dt>filename <span class="property-type">string</span></dt>
64
- <dd>Suggested filename for the image</dd>
65
- </dl>
66
-
67
- <h3>Configuration</h3>
68
- <dl class="message-properties">
69
- <dt>Width</dt>
70
- <dd>Image width in pixels (default: 800)</dd>
71
-
72
- <dt>Height</dt>
73
- <dd>Image height in pixels (default: 480)</dd>
74
-
75
- <dt>Layout</dt>
76
- <dd>Image layout template:
77
- <ul>
78
- <li><b>Default</b> - Standard layout with all data</li>
79
- <li><b>Compact</b> - Minimal layout for small displays</li>
80
- <li><b>TRMNL Display</b> - Optimized for TRMNL e-ink display (800x480)</li>
81
- </ul>
82
- </dd>
83
- </dl>
84
-
83
+ <ol class="node-ports">
84
+ <li>PNG
85
+ <dl class="message-properties">
86
+ <dt>payload <span class="property-type">buffer | string</span></dt>
87
+ <dd>PNG image as a Buffer (or base64 data URL / file path depending
88
+ on the PNG Output setting).</dd>
89
+ <dt>contentType <span class="property-type">string</span></dt>
90
+ <dd>Set to "image/png".</dd>
91
+ </dl>
92
+ </li>
93
+ <li>HTML
94
+ <dl class="message-properties">
95
+ <dt>payload <span class="property-type">string</span></dt>
96
+ <dd>Self-contained, resizable HTML document of the dashboard.</dd>
97
+ <dt>contentType <span class="property-type">string</span></dt>
98
+ <dd>Set to "text/html".</dd>
99
+ </dl>
100
+ </li>
101
+ </ol>
102
+
85
103
  <h3>Details</h3>
86
- <p>This node generates a PNG image visualization of your SENEC system data.</p>
87
- <p>Connect it after a senec-data node to visualize the data graphically.</p>
88
- <p>The generated image can be saved to file, sent via HTTP, or displayed on e-ink displays like TRMNL.</p>
89
-
104
+ <p>The node caches the latest SENEC and weather messages and re-renders on
105
+ each input, so it can be driven from two independent sources. Rendering
106
+ begins once SENEC data has been received at least once.</p>
107
+
90
108
  <h3>Example Flow</h3>
91
109
  <pre>
92
- [senec-data] → [senec-image] → [file out]
110
+ [senec-data] ──────────┐
111
+ ├→ [senec-image] ─┬→ [file out] (PNG)
112
+ [weather] ─────────────┘ └→ [http res] (HTML)
93
113
  </pre>
94
114
  </script>
@@ -1,31 +1,206 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const senec_image_renderer_1 = require("../lib/senec-image-renderer");
4
+ const dashboard_html_renderer_1 = require("../lib/dashboard-html-renderer");
5
+ const dashboard_layout_1 = require("../lib/dashboard-layout");
6
+ /**
7
+ * Coerce an arbitrary payload into a SenecData object. Kept permissive so
8
+ * partial payloads still render.
9
+ */
10
+ function coerceSenecData(payload) {
11
+ const num = (v) => (typeof v === 'number' && isFinite(v) ? v : 0);
12
+ const branch = (b) => ({
13
+ today: num(b?.today),
14
+ now: num(b?.now),
15
+ });
16
+ return {
17
+ steuereinheitState: payload?.steuereinheitState ?? 'OK',
18
+ gridimport: branch(payload?.gridimport),
19
+ powergenerated: branch(payload?.powergenerated),
20
+ consumption: branch(payload?.consumption),
21
+ gridexport: branch(payload?.gridexport),
22
+ acculevel: { now: num(payload?.acculevel?.now) },
23
+ };
24
+ }
25
+ /** Heuristic: does this payload look like a SENEC energy payload? */
26
+ function looksLikeSenec(payload) {
27
+ return (payload &&
28
+ typeof payload === 'object' &&
29
+ ('powergenerated' in payload ||
30
+ 'acculevel' in payload ||
31
+ 'gridimport' in payload ||
32
+ 'consumption' in payload));
33
+ }
34
+ /** Heuristic: does this payload look like a weather payload? */
35
+ function looksLikeWeather(payload) {
36
+ return (payload &&
37
+ typeof payload === 'object' &&
38
+ ('temperature' in payload || 'forecast' in payload || 'condition' in payload));
39
+ }
40
+ /**
41
+ * Clamp an untrusted string to a maximum length to bound the size of the
42
+ * rendered HTML/PNG (defense against oversized upstream payloads).
43
+ */
44
+ function safeStr(value, max = 64) {
45
+ const s = value === undefined || value === null ? '' : String(value);
46
+ return s.length > max ? s.slice(0, max) : s;
47
+ }
48
+ function safeNum(value) {
49
+ const n = Number(value);
50
+ return isFinite(n) ? n : 0;
51
+ }
52
+ function coerceWeatherData(payload) {
53
+ return {
54
+ temperature: safeNum(payload?.temperature),
55
+ condition: safeStr(payload?.condition, 64),
56
+ icon: safeStr(payload?.icon, 16),
57
+ tempMax: safeNum(payload?.tempMax),
58
+ tempMin: safeNum(payload?.tempMin),
59
+ humidity: safeNum(payload?.humidity),
60
+ windSpeed: safeNum(payload?.windSpeed),
61
+ windDirection: safeStr(payload?.windDirection, 8),
62
+ // Cap the forecast to 5 entries to bound render size.
63
+ forecast: Array.isArray(payload?.forecast)
64
+ ? payload.forecast.slice(0, 5).map((f) => ({
65
+ day: safeStr(f?.day, 8),
66
+ icon: safeStr(f?.icon, 16),
67
+ temp: safeNum(f?.temp),
68
+ }))
69
+ : [],
70
+ location: payload?.location !== undefined ? safeStr(payload.location, 48) : undefined,
71
+ latitude: payload?.latitude !== undefined ? safeNum(payload.latitude) : undefined,
72
+ longitude: payload?.longitude !== undefined ? safeNum(payload.longitude) : undefined,
73
+ };
74
+ }
3
75
  module.exports = function (RED) {
4
76
  function SenecImageNode(config) {
5
77
  RED.nodes.createNode(this, config);
6
78
  const node = this;
7
- node.width = config.width || 800;
8
- node.height = config.height || 480;
9
- node.layout = config.layout || 'default';
10
- node.on('input', async function (msg) {
79
+ node.width = config.width || 1600;
80
+ node.height = config.height || 0; // 0 => derive from 16:9 aspect ratio
81
+ node.layout = config.layout || 'dashboard';
82
+ node.outputFormat = config.outputFormat || 'buffer';
83
+ node.filePath = config.filePath;
84
+ node.lastSenec = null;
85
+ node.lastWeather = null;
86
+ node.lastSenecStatus = (0, dashboard_layout_1.unknownStatus)();
87
+ node.lastWeatherStatus = (0, dashboard_layout_1.unknownStatus)();
88
+ /**
89
+ * Determine which input the message arrived on.
90
+ *
91
+ * Node-RED delivers all inputs to the single input handler. We identify
92
+ * the source by (in priority order):
93
+ * 1. Explicit port index provided by the runtime (msg._input?.index).
94
+ * 2. The message topic ("weather/..." vs "senec/...").
95
+ * 3. The payload shape.
96
+ */
97
+ function classify(msg) {
98
+ const idx = typeof msg?._input?.index === 'number'
99
+ ? msg._input.index
100
+ : typeof msg?.inputIndex === 'number'
101
+ ? msg.inputIndex
102
+ : undefined;
103
+ if (idx === 0) {
104
+ return 'senec';
105
+ }
106
+ if (idx === 1) {
107
+ return 'weather';
108
+ }
109
+ const topic = String(msg?.topic || '').toLowerCase();
110
+ if (topic.includes('weather') || topic.includes('wetter')) {
111
+ return 'weather';
112
+ }
113
+ if (topic.includes('senec') || topic.includes('energy')) {
114
+ return 'senec';
115
+ }
116
+ if (looksLikeWeather(msg?.payload) && !looksLikeSenec(msg?.payload)) {
117
+ return 'weather';
118
+ }
119
+ if (looksLikeSenec(msg?.payload)) {
120
+ return 'senec';
121
+ }
122
+ return 'unknown';
123
+ }
124
+ node.on('input', async function (msg, send, done) {
125
+ send = send || node.send.bind(node);
126
+ done =
127
+ done ||
128
+ function (err) {
129
+ if (err) {
130
+ node.error(err, msg);
131
+ }
132
+ };
11
133
  try {
12
- if (!msg.payload || typeof msg.payload !== 'object') {
13
- node.error('Input payload must be a SENEC data object');
134
+ const source = classify(msg);
135
+ const incomingStatus = msg.status && typeof msg.status === 'object' ? msg.status : undefined;
136
+ // A status-only message (payload null/absent) still updates health.
137
+ const hasData = msg.payload && typeof msg.payload === 'object';
138
+ if (!hasData && !incomingStatus) {
139
+ node.status({ fill: 'red', shape: 'ring', text: 'invalid payload' });
140
+ done(new Error('Input payload must be a SENEC or Weather data object'));
141
+ return;
142
+ }
143
+ // Update cached data + status based on the source.
144
+ if (source === 'weather') {
145
+ if (hasData) {
146
+ node.lastWeather = coerceWeatherData(msg.payload);
147
+ }
148
+ node.lastWeatherStatus =
149
+ incomingStatus ??
150
+ { ok: hasData, timestamp: new Date().toISOString(), fallback: false };
151
+ }
152
+ else {
153
+ if (hasData) {
154
+ node.lastSenec = coerceSenecData(msg.payload);
155
+ }
156
+ node.lastSenecStatus =
157
+ incomingStatus ??
158
+ { ok: hasData, timestamp: new Date().toISOString(), fallback: false };
159
+ }
160
+ // Require at least SENEC data before rendering.
161
+ if (!node.lastSenec) {
162
+ node.status({ fill: 'yellow', shape: 'ring', text: 'waiting for SENEC data' });
163
+ done();
14
164
  return;
15
165
  }
16
166
  node.status({ fill: 'blue', shape: 'dot', text: 'generating...' });
17
- // TODO: Implement image generation using pureimage
18
- // For now, send a placeholder response
19
- const imageBuffer = Buffer.from('PNG image generation not yet implemented', 'utf-8');
20
- msg.payload = imageBuffer;
21
- msg.contentType = 'image/png';
22
- msg.filename = `senec-${Date.now()}.png`;
23
- node.send(msg);
167
+ const statuses = {
168
+ senec: node.lastSenecStatus,
169
+ weather: node.lastWeatherStatus,
170
+ };
171
+ const model = (0, dashboard_layout_1.buildDashboardModel)(node.lastSenec, node.lastWeather, statuses);
172
+ const renderer = new senec_image_renderer_1.SenecImageRenderer(node.width, node.height && node.height > 0 ? node.height : undefined);
173
+ const imageBuffer = await renderer.render(model);
174
+ const html = (0, dashboard_html_renderer_1.renderDashboardHtml)(model);
175
+ const filename = `energy-dashboard-${Date.now()}.png`;
176
+ // Build the PNG message for output 1.
177
+ const pngMsg = { ...msg, contentType: 'image/png', filename, status: statuses };
178
+ if (node.outputFormat === 'base64') {
179
+ pngMsg.payload = `data:image/png;base64,${imageBuffer.toString('base64')}`;
180
+ }
181
+ else if (node.outputFormat === 'file' && node.filePath) {
182
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
183
+ const fs = require('fs').promises;
184
+ await fs.writeFile(node.filePath, imageBuffer);
185
+ pngMsg.payload = node.filePath;
186
+ }
187
+ else {
188
+ pngMsg.payload = imageBuffer;
189
+ }
190
+ // Build the HTML message for output 2.
191
+ const htmlMsg = {
192
+ ...msg,
193
+ payload: html,
194
+ contentType: 'text/html',
195
+ status: statuses,
196
+ };
197
+ send([pngMsg, htmlMsg]);
24
198
  node.status({ fill: 'green', shape: 'dot', text: 'success' });
199
+ done();
25
200
  }
26
201
  catch (error) {
27
- node.error('Failed to generate image: ' + error.message);
28
202
  node.status({ fill: 'red', shape: 'ring', text: 'error' });
203
+ done(new Error('Failed to generate image: ' + error.message));
29
204
  }
30
205
  });
31
206
  }