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.
- package/CHANGELOG.md +11 -0
- package/dist/assets/fonts/senec-sans.ttf +0 -0
- package/dist/lib/dashboard-html-renderer.d.ts +18 -0
- package/dist/lib/dashboard-html-renderer.d.ts.map +1 -0
- package/dist/lib/dashboard-html-renderer.js +604 -0
- package/dist/lib/dashboard-html-renderer.js.map +1 -0
- package/dist/lib/dashboard-layout.d.ts +152 -0
- package/dist/lib/dashboard-layout.d.ts.map +1 -0
- package/dist/lib/dashboard-layout.js +201 -0
- package/dist/lib/dashboard-layout.js.map +1 -0
- package/dist/lib/geocoding-client.d.ts +61 -0
- package/dist/lib/geocoding-client.d.ts.map +1 -0
- package/dist/lib/geocoding-client.js +77 -0
- package/dist/lib/geocoding-client.js.map +1 -0
- package/dist/lib/senec-image-renderer.d.ts +107 -0
- package/dist/lib/senec-image-renderer.d.ts.map +1 -0
- package/dist/lib/senec-image-renderer.js +872 -0
- package/dist/lib/senec-image-renderer.js.map +1 -0
- package/dist/lib/senec-layout.d.ts +212 -0
- package/dist/lib/senec-layout.d.ts.map +1 -0
- package/dist/lib/senec-layout.js +252 -0
- package/dist/lib/senec-layout.js.map +1 -0
- package/dist/lib/weather-client.d.ts +42 -0
- package/dist/lib/weather-client.d.ts.map +1 -0
- package/dist/lib/weather-client.js +193 -0
- package/dist/lib/weather-client.js.map +1 -0
- package/dist/nodes/senec-data.js +10 -2
- package/dist/nodes/senec-data.js.map +1 -1
- package/dist/nodes/senec-image.html +73 -53
- package/dist/nodes/senec-image.js +189 -14
- package/dist/nodes/senec-image.js.map +1 -1
- package/dist/nodes/weather.d.ts +2 -0
- package/dist/nodes/weather.d.ts.map +1 -0
- package/dist/nodes/weather.html +179 -0
- package/dist/nodes/weather.js +138 -0
- package/dist/nodes/weather.js.map +1 -0
- 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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"}
|
package/dist/nodes/senec-data.js
CHANGED
|
@@ -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:
|
|
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":";;AACA,8DAAyD;AAiBzD,MAAM,CAAC,OAAO,GAAG,UAAS,GAAQ;IAChC,SAAS,aAAa,CAAsB,MAAwB;QAClE,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAEnC,MAAM,IAAI,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QAEtB,kBAAkB;QAClB,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAQ,CAAC;QAE3D,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,IAAI,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;YAC1C,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;YAC/D,OAAO;QACT,CAAC;QAED,wBAAwB;QACxB,IAAI,CAAC;YACH,IAAI,CAAC,SAAS,GAAG,IAAI,iCAAc,CAAC;gBAClC,QAAQ,EAAE,UAAU,CAAC,QAAQ;gBAC7B,QAAQ,EAAE,UAAU,CAAC,WAAW,CAAC,QAAQ;aAC1C,CAAC,CAAC;YACH,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAC9D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,CAAC,mCAAmC,GAAG,KAAK,CAAC,CAAC;YACxD,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QAED,sBAAsB;QACtB,KAAK,UAAU,SAAS;YACtB,IAAI,CAAC,IAAI,CAAC,SAAS;gBAAE,OAAO;YAE5B,IAAI,CAAC;gBACH,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;gBAEjE,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;gBAElD,IAAI,CAAC,IAAI,CAAC;oBACR,OAAO,EAAE,UAAU;oBACnB,KAAK,EAAE,YAAY;
|
|
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:
|
|
8
|
-
height: { value:
|
|
9
|
-
layout: { value: '
|
|
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:
|
|
12
|
-
outputs:
|
|
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="
|
|
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="
|
|
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="
|
|
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>
|
|
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
|
-
<
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
<
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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>
|
|
87
|
-
|
|
88
|
-
|
|
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]
|
|
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 ||
|
|
8
|
-
node.height = config.height ||
|
|
9
|
-
node.layout = config.layout || '
|
|
10
|
-
node.
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
}
|