iobroker.weathersense 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Daniel Luginbühl <webmaster@ltspiceusers.ch>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ ![Logo](admin/weathersense.png)
2
+ # ioBroker.weathersense
3
+
4
+ [![NPM version](https://img.shields.io/npm/v/iobroker.weathersense.svg)](https://www.npmjs.com/package/iobroker.weathersense)
5
+ [![Downloads](https://img.shields.io/npm/dm/iobroker.weathersense.svg)](https://www.npmjs.com/package/iobroker.weathersense)
6
+ ![Number of Installations](https://iobroker.live/badges/weathersense-installed.svg)
7
+ ![Current version in stable repository](https://iobroker.live/badges/weathersense-stable.svg)
8
+
9
+ [![NPM](https://nodei.co/npm/iobroker.weathersense.png?downloads=true)](https://nodei.co/npm/iobroker.weathersense/)
10
+
11
+ **Tests:** ![Test and Release](https://github.com/ltspicer/ioBroker.weathersense/workflows/Test%20and%20Release/badge.svg)
12
+
13
+ ## weathersense adapter for ioBroker
14
+
15
+ WeatherSense is a cloud for weather stations. This adapter reads data from the WeatherSense server.
16
+
17
+ See: https://play.google.com/store/apps/details?id=com.emax.weahter&hl=de_CH
18
+
19
+
20
+ ## Use:
21
+ Simply enter your WeatherSense account login details (email and password).
22
+ The weather station data is stored in the weathersense data point.
23
+ The data can also be sent via MQTT.
24
+
25
+
26
+ ## Changelog
27
+ ### 1.0.0 (2025-07-01)
28
+
29
+ - Initial release
30
+
31
+ ## License
32
+ MIT License
33
+
34
+ Copyright (c) 2025 Daniel Luginbühl <webmaster@ltspiceusers.ch>
35
+
36
+ Permission is hereby granted, free of charge, to any person obtaining a copy
37
+ of this software and associated documentation files (the "Software"), to deal
38
+ in the Software without restriction, including without limitation the rights
39
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
40
+ copies of the Software, and to permit persons to whom the Software is
41
+ furnished to do so, subject to the following conditions:
42
+
43
+ The above copyright notice and this permission notice shall be included in all
44
+ copies or substantial portions of the Software.
45
+
46
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
47
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
48
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
49
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
50
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
51
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
52
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ {
2
+ "heizoel24-mex adapter settings": "Adaptereinstellungen für WeatherSense",
3
+ "username": "WeatherSense Username (Email)",
4
+ "passwort": "WeatherSense Passwort",
5
+ "mqtt_active": "MQTT benutzen",
6
+ "broker_address": "MQTT Broker IP Adresse",
7
+ "mqtt_user": "MQTT User",
8
+ "mqtt_pass": "MQTT Passwort",
9
+ "mqtt_port": "MQTT Port",
10
+ "sensor_id": "Sensor ID (1-20)",
11
+ "storeJson": "Json speichern",
12
+ "storeDir": "Verz. für Json Datei"
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "heizoel24-mex adapter settings": "Adapter settings for WeatherSense",
3
+ "username": "WeatherSense username (email)",
4
+ "passwort": "WeatherSense password",
5
+ "mqtt_active": "Use MQTT",
6
+ "broker_address": "MQTT broker IP address",
7
+ "mqtt_user": "MQTT user",
8
+ "mqtt_pass": "MQTT password",
9
+ "mqtt_port": "MQTT port",
10
+ "sensor_id": "Sensor ID (1-20)",
11
+ "storeJson": "Save Json",
12
+ "storeDir": "Folder for Json file"
13
+ }
@@ -0,0 +1,118 @@
1
+ {
2
+ "i18n": true,
3
+ "type": "panel",
4
+ "items": {
5
+ "username": {
6
+ "type": "text",
7
+ "label": "WeatherSense username (email)",
8
+ "newLine": true,
9
+ "xs": 12,
10
+ "sm": 12,
11
+ "md": 6,
12
+ "lg": 4,
13
+ "xl": 4
14
+ },
15
+ "passwort": {
16
+ "type": "text",
17
+ "label": "WeatherSense password",
18
+ "xs": 12,
19
+ "sm": 12,
20
+ "md": 6,
21
+ "lg": 4,
22
+ "xl": 4
23
+ },
24
+ "mqtt_active": {
25
+ "type": "checkbox",
26
+ "label": "Use MQTT",
27
+ "default": false,
28
+ "newLine": true,
29
+ "xs": 12,
30
+ "sm": 12,
31
+ "md": 6,
32
+ "lg": 4,
33
+ "xl": 4
34
+ },
35
+ "celsius": {
36
+ "type": "checkbox",
37
+ "label": "°C (unchecked = °F)",
38
+ "default": true,
39
+ "newLine": true,
40
+ "xs": 12,
41
+ "sm": 12,
42
+ "md": 6,
43
+ "lg": 4,
44
+ "xl": 4
45
+ },
46
+ "broker_address": {
47
+ "type": "text",
48
+ "label": "MQTT broker IP address",
49
+ "newLine": true,
50
+ "xs": 12,
51
+ "sm": 12,
52
+ "md": 6,
53
+ "lg": 4,
54
+ "xl": 4
55
+ },
56
+ "mqtt_port": {
57
+ "type": "number",
58
+ "label": "MQTT port",
59
+ "default": 1883,
60
+ "xs": 12,
61
+ "sm": 12,
62
+ "md": 6,
63
+ "lg": 4,
64
+ "xl": 4
65
+ },
66
+ "mqtt_user": {
67
+ "type": "text",
68
+ "label": "MQTT user",
69
+ "newLine": true,
70
+ "xs": 12,
71
+ "sm": 12,
72
+ "md": 6,
73
+ "lg": 4,
74
+ "xl": 4
75
+ },
76
+ "mqtt_pass": {
77
+ "type": "text",
78
+ "label": "MQTT password",
79
+ "xs": 12,
80
+ "sm": 12,
81
+ "md": 6,
82
+ "lg": 4,
83
+ "xl": 4
84
+ },
85
+ "sensor_id": {
86
+ "type": "number",
87
+ "label": "Sensor ID (1-20)",
88
+ "default": 1,
89
+ "newLine": true,
90
+ "xs": 12,
91
+ "sm": 12,
92
+ "md": 6,
93
+ "lg": 4,
94
+ "xl": 4
95
+ },
96
+ "storeJson": {
97
+ "type": "checkbox",
98
+ "label": "Save Json file",
99
+ "default": false,
100
+ "newLine": true,
101
+ "xs": 12,
102
+ "sm": 12,
103
+ "md": 6,
104
+ "lg": 4,
105
+ "xl": 4
106
+ },
107
+ "storeDir": {
108
+ "type": "text",
109
+ "label": "Folder for JSON file",
110
+ "default": "/home/iobroker",
111
+ "xs": 12,
112
+ "sm": 12,
113
+ "md": 6,
114
+ "lg": 4,
115
+ "xl": 4
116
+ }
117
+ }
118
+ }
Binary file
@@ -0,0 +1,81 @@
1
+ {
2
+ "common": {
3
+ "name": "weathersense",
4
+ "version": "1.0.0",
5
+ "news": {
6
+ "1.0.0": {
7
+ "en": "First release",
8
+ "de": "Erstes Release"
9
+ }
10
+ },
11
+ "titleLang": {
12
+ "en": "WeatherSense",
13
+ "de": "WeatherSense"
14
+ },
15
+ "desc": {
16
+ "en": "Read in data from WeatherSense",
17
+ "de": "Daten von WeatherSense einlesen"
18
+ },
19
+ "authors": [
20
+ "Daniel Luginbühl <webmaster@ltspiceusers.ch>"
21
+ ],
22
+ "keywords": [
23
+ "weather",
24
+ "sense",
25
+ "weathersense",
26
+ "ioBroker.weathersense"
27
+ ],
28
+ "licenseInformation": {
29
+ "type": "free",
30
+ "license": "MIT"
31
+ },
32
+ "platform": "Javascript/Node.js",
33
+ "icon": "weathersense.png",
34
+ "enabled": false,
35
+ "extIcon": "https://raw.githubusercontent.com/ltspicer/ioBroker.weathersense/main/admin/weathersense.png",
36
+ "readme": "https://github.com/ltspicer/ioBroker.weathersense/blob/main/README.md",
37
+ "loglevel": "info",
38
+ "tier": 3,
39
+ "mode": "schedule",
40
+ "schedule": "*/5 * * * *",
41
+ "allowInit": true,
42
+ "type": "metering",
43
+ "compact": true,
44
+ "connectionType": "cloud",
45
+ "dataSource": "poll",
46
+ "adminUI": {
47
+ "config": "json"
48
+ },
49
+ "dependencies": [
50
+ {
51
+ "js-controller": ">=5.0.19"
52
+ }
53
+ ],
54
+ "globalDependencies": [
55
+ {
56
+ "admin": ">=7.4.10"
57
+ }
58
+ ],
59
+ "installedFrom": "file:/opt/iobroker/ioBroker.weathersense"
60
+ },
61
+ "encryptedNative": [
62
+ "passwort",
63
+ "mqtt_pass"
64
+ ],
65
+ "protectedNative": [
66
+ "passwort",
67
+ "mqtt_pass"
68
+ ],
69
+ "native": {
70
+ "username": "",
71
+ "passwort": "",
72
+ "mqtt_active": false,
73
+ "broker_address": "0.0.0.0",
74
+ "mqtt_user": "",
75
+ "mqtt_pass": "",
76
+ "mqtt_port": "1883",
77
+ "sensor_id": "1",
78
+ "storeJson": false,
79
+ "storeDir": "/home/iobroker"
80
+ }
81
+ }
@@ -0,0 +1,19 @@
1
+ // This file extends the AdapterConfig type from "@types/iobroker"
2
+ // using the actual properties present in io-package.json
3
+ // in order to provide typings for adapter.config properties
4
+
5
+ import { native } from "../io-package.json";
6
+
7
+ type _AdapterConfig = typeof native;
8
+
9
+ // Augment the globally declared type ioBroker.AdapterConfig
10
+ declare global {
11
+ namespace ioBroker {
12
+ interface AdapterConfig extends _AdapterConfig {
13
+ // Do not enter anything here!
14
+ }
15
+ }
16
+ }
17
+
18
+ // this is required so the above AdapterConfig is found by TypeScript / type checking
19
+ export {};
package/main.js ADDED
@@ -0,0 +1,617 @@
1
+ "use strict";
2
+
3
+ /*
4
+ * Created with @iobroker/create-adapter v2.6.2
5
+ */
6
+
7
+ const utils = require("@iobroker/adapter-core");
8
+ const axios = require("axios");
9
+ const mqtt = require("mqtt");
10
+ const fs = require("fs");
11
+ const crypto = require("crypto");
12
+ const https = require("https");
13
+ const path = require("path");
14
+
15
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
16
+
17
+ axios.defaults.timeout = 2000;
18
+
19
+ class WeatherSense extends utils.Adapter {
20
+
21
+ constructor(options) {
22
+ super({
23
+ ...options,
24
+ name: "weathersense",
25
+ });
26
+ this.on("ready", this.onReady.bind(this));
27
+ this.on("unload", this.onUnload.bind(this));
28
+ }
29
+
30
+ async onReady() {
31
+ const username = this.config.username;
32
+ const passwort = this.config.passwort;
33
+ const broker_address = this.config.broker_address;
34
+ const mqtt_active = this.config.mqtt_active;
35
+ const celsius = this.config.celsius;
36
+ const mqtt_user = this.config.mqtt_user;
37
+ const mqtt_pass = this.config.mqtt_pass;
38
+ const mqtt_port = this.config.mqtt_port;
39
+ const sensor_in = this.config.sensor_id;
40
+ let sensor_id = 1;
41
+ const storeJson = this.config.storeJson;
42
+ const storeDir = this.config.storeDir;
43
+
44
+ if (Number(sensor_in)) {
45
+ sensor_id = parseInt(sensor_in);
46
+ if (sensor_id < 1 || sensor_id > 20) {
47
+ this.log.error("Sensor ID has no value between 1 and 20");
48
+ this.terminate ? this.terminate("Sensor ID has no value between 1 and 20", 0) : process.exit(0);
49
+ return;
50
+ }
51
+ } else {
52
+ this.log.error("Sensor ID has no valid value");
53
+ this.terminate ? this.terminate("Sensor ID has no valid value", 0) : process.exit(0);
54
+ return;
55
+ }
56
+ this.log.debug("Sensor ID is " + sensor_id);
57
+
58
+ if (username.trim().length === 0 || passwort.trim().length === 0) {
59
+ this.log.error("User email and/or user password empty - please check instance configuration");
60
+ this.terminate ? this.terminate("User email and/or user password empty - please check instance configuration", 0) : process.exit(0);
61
+ return;
62
+ }
63
+
64
+ let client = null;
65
+ if (mqtt_active) {
66
+ if (broker_address.trim().length === 0 || broker_address == "0.0.0.0") {
67
+ this.log.error("MQTT IP address is empty - please check instance configuration");
68
+ this.terminate ? this.terminate("MQTT IP address is empty - please check instance configuration", 0) : process.exit(0);
69
+ return;
70
+ }
71
+ client = mqtt.connect(`mqtt://${broker_address}:${mqtt_port}`, {
72
+ connectTimeout: 4000,
73
+ username: mqtt_user,
74
+ password: mqtt_pass
75
+ });
76
+ }
77
+
78
+ try {
79
+ const instObj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);
80
+ if (instObj && instObj.common && instObj.common.schedule && instObj.common.schedule === "*/5 * * * *") {
81
+ instObj.common.schedule = `*/${Math.floor(Math.random() * 6)} * * * *`;
82
+ this.log.info(`Default schedule found and adjusted to spread calls better over the full hour!`);
83
+ await this.setForeignObjectAsync(`system.adapter.${this.namespace}`, instObj);
84
+ this.terminate ? this.terminate() : process.exit(0);
85
+ return;
86
+ }
87
+ } catch (err) {
88
+ this.log.error(`Could not check or adjust the schedule: ${err.message}`);
89
+ }
90
+
91
+ this.log.debug("MQTT active: " + mqtt_active);
92
+ this.log.debug("MQTT port: " + mqtt_port);
93
+
94
+ // Forecast Channel anlegen
95
+ const forecastChannelId = `${sensor_id}.forecast`;
96
+
97
+ try {
98
+ const dataReceived = await this.main(client, username, passwort, mqtt_active, sensor_id, storeJson, storeDir, celsius, forecastChannelId);
99
+
100
+ const systemStateId = `${sensor_id}.DataReceived`;
101
+ await this.setObjectNotExistsAsync(systemStateId, {
102
+ type: "state",
103
+ common: {
104
+ name: "Data successfully received",
105
+ type: "boolean",
106
+ role: "indicator",
107
+ read: true,
108
+ write: false
109
+ },
110
+ native: {},
111
+ });
112
+
113
+ if (dataReceived === true) {
114
+
115
+ await this.setStateAsync(systemStateId, { val: true, ack: true });
116
+
117
+ // DevData Channel anlegen
118
+ const devDataChannelId = `${sensor_id}.devdata`;
119
+ await this.setObjectNotExistsAsync(devDataChannelId, {
120
+ type: "channel",
121
+ common: { name: "DevData" },
122
+ native: {},
123
+ });
124
+
125
+ await this.setObjectNotExistsAsync(forecastChannelId, {
126
+ type: "channel",
127
+ common: { name: "Forecast" },
128
+ native: {},
129
+ });
130
+
131
+ const tempUnit = celsius ? "°C" : "°F";
132
+ this.log.debug(`Unit: ${tempUnit}`);
133
+
134
+ // Bekannte Items
135
+ const fixedItems = [
136
+ { id: "atmospheric_pressure", type: "number", role: "value.pressure", unit: "hPa" },
137
+ { id: "indoor_temp", type: "number", role: "value.temperature", unit: tempUnit },
138
+ { id: "indoor_humidity", type: "number", role: "value.humidity", unit: "%" },
139
+ { id: "outdoor_temp", type: "number", role: "value.temperature", unit: tempUnit },
140
+ { id: "outdoor_humidity", type: "number", role: "value.humidity", unit: "%" },
141
+ ];
142
+
143
+ // Zuerst die festen Items anlegen und setzen
144
+ for (const item of fixedItems) {
145
+ const id = `${devDataChannelId}.${item.id}`;
146
+ await this.setObjectNotExistsAsync(id, {
147
+ type: "state",
148
+ common: {
149
+ name: item.id,
150
+ type: item.type,
151
+ role: item.role,
152
+ unit: item.unit,
153
+ read: true,
154
+ write: false,
155
+ },
156
+ native: {},
157
+ });
158
+
159
+ const val = this.contentDevData ? this.contentDevData[item.id] : null;
160
+ if (val != null) {
161
+ await this.setStateAsync(id, { val: val, ack: true });
162
+ }
163
+ }
164
+
165
+ // Jetzt alle zusätzlichen dynamischen Keys aus this.contentDevData durchgehen
166
+ for (const key of Object.keys(this.contentDevData || {})) {
167
+ // Schon behandelt?
168
+ if (fixedItems.find(item => item.id === key)) continue;
169
+
170
+ const val = this.contentDevData[key];
171
+
172
+ // Typ und Rolle
173
+ const common = {
174
+ name: key,
175
+ type: typeof val === "number" ? "number" : "string",
176
+ role: "value",
177
+ unit: "",
178
+ read: true,
179
+ write: false,
180
+ };
181
+
182
+ if (key.includes("temp")) {
183
+ common.role = "value.temperature";
184
+ common.unit = tempUnit;
185
+ } else if (key.includes("humidity")) {
186
+ common.role = "value.humidity";
187
+ common.unit = "%";
188
+ } else if (key.includes("pressure")) {
189
+ common.role = "value.pressure";
190
+ common.unit = "hPa";
191
+ }
192
+
193
+ const id = `${devDataChannelId}.${key}`;
194
+
195
+ await this.setObjectNotExistsAsync(id, {
196
+ type: "state",
197
+ common,
198
+ native: {},
199
+ });
200
+
201
+ await this.setStateAsync(id, { val: val, ack: true });
202
+ }
203
+ } else {
204
+ this.log.error("Error loading data in main()");
205
+ await this.setStateAsync(systemStateId, { val: false, ack: true });
206
+ }
207
+ } catch (error) {
208
+ this.log.error("Unexpected error in onReady(): " + error.message);
209
+ } finally {
210
+ if (client) {
211
+ client.end();
212
+ }
213
+ this.terminate ? this.terminate("Everything done. Going to terminate till next schedule", 0) : process.exit(0);
214
+ }
215
+ }
216
+
217
+ async sendMqtt(sensor_id, mqtt_active, client, topic, wert) {
218
+ if (mqtt_active) {
219
+ // Wenn wert nicht String ist, in String umwandeln (auch null und undefined abfangen)
220
+ if (typeof wert !== "string") {
221
+ wert = wert !== null && wert !== undefined ? wert.toString() : "";
222
+ }
223
+ client.publish("WEATHERSENSE/" + sensor_id.toString() + "/" + topic, wert);
224
+ }
225
+ }
226
+
227
+ async createOrUpdateForecastDPs(forecastChannelId, forecasts, celsius) {
228
+ if (!forecasts || !forecastChannelId) return;
229
+
230
+ // Struktur der Forecast-Items pro Tag
231
+ const forecastItems = [
232
+ { id: "day", type: "string", role: "value", unit: "" },
233
+ { id: "date", type: "string", role: "value", unit: "" },
234
+ { id: "high", type: "number", role: "value.temperature", unit: celsius ? "°C" : "°F" },
235
+ { id: "low", type: "number", role: "value.temperature", unit: celsius ? "°C" : "°F" },
236
+ { id: "text", type: "string", role: "text", unit: "" },
237
+ ];
238
+
239
+ for (let i = 0; i < forecasts.length; i++) {
240
+ const forecast = forecasts[i];
241
+ if (!forecast) continue;
242
+
243
+ for (const item of forecastItems) {
244
+ const id = `${forecastChannelId}.${i}.${item.id}`;
245
+ await this.setObjectNotExistsAsync(id, {
246
+ type: "state",
247
+ common: {
248
+ name: item.id,
249
+ type: item.type,
250
+ role: item.role,
251
+ unit: item.unit,
252
+ read: true,
253
+ write: false,
254
+ },
255
+ native: {},
256
+ });
257
+
258
+ let val = forecast[item.id];
259
+ if ((item.id === "high" || item.id === "low") && typeof val === "number") {
260
+ if (celsius) {
261
+ val = Number(((val - 32) / 1.8).toFixed(1));
262
+ } else {
263
+ val = Number(val);
264
+ }
265
+ }
266
+
267
+ if (val != null) {
268
+ await this.setStateAsync(id, { val: val, ack: true });
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ async sendForecasts(client, forecasts, celsius, sensor_id) {
275
+ if (!client || !forecasts) return;
276
+
277
+ for (let i = 0; i < forecasts.length; i++) {
278
+ const forecast = forecasts[i];
279
+ if (!forecast) continue;
280
+
281
+ await this.sendMqtt(sensor_id, true, client, `forecast/${i}/day`, forecast.day || "");
282
+ await this.sendMqtt(sensor_id, true, client, `forecast/${i}/date`, forecast.date || "");
283
+
284
+ let tempHigh = forecast.high;
285
+ let tempLow = forecast.low;
286
+
287
+ if (celsius && typeof tempHigh === "number" && typeof tempLow === "number") {
288
+ tempHigh = Number(((tempHigh - 32) / 1.8).toFixed(1)); // Zahl, keine Zeichenkette
289
+ tempLow = Number(((tempLow - 32) / 1.8).toFixed(1));
290
+ } else {
291
+ // Wenn kein Celsius oder kein Zahlentyp, trotzdem als Zahl (sofern möglich), sonst 0 als Fallback
292
+ tempHigh = tempHigh != null ? Number(tempHigh) : 0;
293
+ tempLow = tempLow != null ? Number(tempLow) : 0;
294
+ }
295
+
296
+ await this.sendMqtt(sensor_id, true, client, `forecast/${i}/high`, tempHigh);
297
+ await this.sendMqtt(sensor_id, true, client, `forecast/${i}/low`, tempLow);
298
+ await this.sendMqtt(sensor_id, true, client, `forecast/${i}/text`, forecast.text || "");
299
+ }
300
+ }
301
+
302
+ async clearOldForecasts(sensor_id, client, maxDays = 6) {
303
+ for (let i = 0; i < maxDays; i++) {
304
+ for (const key of ["day", "date", "high", "low", "text"]) {
305
+ await this.sendMqtt(sensor_id, true, client, `forecast/${i}/${key}`, "");
306
+ }
307
+ }
308
+ }
309
+
310
+ printAllKeys(d, prefix = "") {
311
+ if (typeof d === "object" && d !== null && !Array.isArray(d)) {
312
+ for (const [k, v] of Object.entries(d)) {
313
+ this.printAllKeys(v, `${prefix}${k}/`);
314
+ }
315
+ } else if (Array.isArray(d)) {
316
+ d.forEach((item, i) => {
317
+ this.printAllKeys(item, `${prefix}${i}/`);
318
+ });
319
+ } else {
320
+ this.log.debug(`${prefix}: ${d}`);
321
+ }
322
+ }
323
+
324
+ findValue(sensor_data, type, channel) {
325
+ const entry = sensor_data.find(item => item.type === type && item.channel === channel);
326
+ return entry ? entry.curVal : null;
327
+ }
328
+
329
+
330
+ // Funktion zum Erzeugen des MD5-Hashes
331
+ hashPassword(pw, key) {
332
+ const combined = pw + key;
333
+ return crypto.createHash("md5").update(combined, "utf8").digest("hex").toUpperCase();
334
+ }
335
+
336
+ // Login-Funktion
337
+ async login(USERNAME, PASSWORD) {
338
+ const MD5_KEY = "emax@pwd123";
339
+ const LOGIN_URL = "https://47.52.149.125/V1.0/account/login";
340
+ const hashed_pw = this.hashPassword(PASSWORD, MD5_KEY);
341
+
342
+ const headers = {
343
+ "Content-Type": "application/json; charset=utf-8",
344
+ "User-Agent": "okhttp/3.14.9"
345
+ };
346
+
347
+ const payload = {
348
+ email: USERNAME,
349
+ pwd: hashed_pw
350
+ };
351
+
352
+ try {
353
+ const response = await axios.post(LOGIN_URL, payload, { headers });
354
+
355
+ this.log.debug("Status code:", response.status);
356
+ this.log.debug("Response:", response.data);
357
+
358
+ const data = response.data;
359
+
360
+ if (response.status === 200) {
361
+ if (data.status === 0 && data.content) {
362
+ const token = data.content.token;
363
+ this.log.debug("Login successful. Token: " + token.substring(0, 20) + "...");
364
+ return token;
365
+ } else {
366
+ this.log.error("Login failed:", data.message);
367
+ }
368
+ } else {
369
+ this.log.error("Server error");
370
+ }
371
+ } catch (error) {
372
+ this.log.error("Error during login:", error.message);
373
+ }
374
+
375
+ return null;
376
+ }
377
+
378
+ async devData(token) {
379
+ // Realtime Daten holen
380
+ this.log.debug("getRealtime data...");
381
+
382
+ const url = "https://emaxlife.net/V1.0/weather/devData/getRealtime";
383
+ const headers = {
384
+ "emaxtoken": token,
385
+ "Content-Type": "application/json"
386
+ };
387
+
388
+ try {
389
+ const response = await axios.get(url, {
390
+ headers,
391
+ timeout: 5000,
392
+ httpsAgent: new (require("https").Agent)({ rejectUnauthorized: false })
393
+ });
394
+
395
+ if (response.status === 200) {
396
+ this.log.debug("Data was received");
397
+ return response.data;
398
+ } else {
399
+ this.log.error(`devData > Status Code: ${response.status}`);
400
+ return "error";
401
+ }
402
+ } catch (error) {
403
+ if (error.response) {
404
+ this.log.error(`devData > Status Code: ${error.response.status}`);
405
+ } else {
406
+ this.log.error(`Error during request: ${error.message}`);
407
+ }
408
+ return "error";
409
+ }
410
+ }
411
+
412
+ async foreCast(token) {
413
+ // Forecast holen
414
+ this.log.debug("getForecast data...");
415
+
416
+ const url = "https://emaxlife.net/V1.0/weather/netData/getForecast";
417
+ const headers = {
418
+ "emaxtoken": token,
419
+ "Content-Type": "application/json"
420
+ };
421
+
422
+ try {
423
+ const response = await axios.get(url, {
424
+ headers,
425
+ timeout: 5000,
426
+ httpsAgent: new https.Agent({ rejectUnauthorized: false }) // entspricht verify=False
427
+ });
428
+
429
+ if (response.status === 200) {
430
+ this.log.debug("Data was received");
431
+ return response.data;
432
+ } else {
433
+ this.log.error(`foreCast > Status Code: ${response.status}`);
434
+ return "error";
435
+ }
436
+ } catch (error) {
437
+ if (error.response) {
438
+ this.log.error(`foreCast > Status Code: ${error.response.status}`);
439
+ } else {
440
+ this.log.error(`Error during request: ${error.message}`);
441
+ }
442
+ return "error";
443
+ }
444
+ }
445
+
446
+ async main(client, username, passwort, mqtt_active, sensor_id, storeJson, storeDir, celsius, forecastChannelId) {
447
+ const token = await this.login(username, passwort);
448
+ if (!token) {
449
+ this.log.error("No token received");
450
+ if (mqtt_active) {
451
+ await this.sendMqtt(sensor_id, mqtt_active, client, "dataReceived", "false");
452
+ client.end();
453
+ }
454
+ return false;
455
+ }
456
+ const devdata = await this.devData(token);
457
+ const forecast = await this.foreCast(token);
458
+ if (devdata === "error" || forecast === "error") {
459
+ this.log.error("No data received");
460
+ if (mqtt_active) {
461
+ await this.sendMqtt(sensor_id, mqtt_active, client, "dataReceived", "false");
462
+ client.end();
463
+ }
464
+ return false;
465
+ }
466
+
467
+ if (storeJson) {
468
+ this.log.debug("Save devData.json to " + storeDir);
469
+ const json_object = JSON.stringify(devdata, null, 4);
470
+ fs.writeFileSync(path.join(storeDir, "devData.json"), json_object, "utf-8");
471
+ }
472
+
473
+ this.log.debug("devData JSON:");
474
+ this.printAllKeys(devdata);
475
+
476
+ const content = devdata?.content || {};
477
+ const sensor_data = content.sensorDatas || [];
478
+
479
+ const luftdruck = content.atmos;
480
+ let temp_innen = this.findValue(sensor_data, 1, 0);
481
+ const feuchte_innen = this.findValue(sensor_data, 2, 0);
482
+ let temp_aussen = this.findValue(sensor_data, 1, 2);
483
+ const feuchte_aussen = this.findValue(sensor_data, 2, 2);
484
+
485
+ const skipCombinations = new Set(["1_0", "1_2", "2_0", "2_2"]);
486
+
487
+ if (celsius) {
488
+ if (temp_innen != null) temp_innen = ((temp_innen - 32) / 1.8).toFixed(1);
489
+ if (temp_aussen != null) temp_aussen = ((temp_aussen - 32) / 1.8).toFixed(1);
490
+ }
491
+
492
+ if (mqtt_active) {
493
+ const error_code = devdata.error;
494
+
495
+ if (error_code != null) this.sendMqtt(sensor_id, mqtt_active, client, "devData/error", error_code);
496
+ if (content.devTime) this.sendMqtt(sensor_id, mqtt_active, client, "devData/devtime", content.devTime);
497
+ if (content.updateTime) this.sendMqtt(sensor_id, mqtt_active, client, "devData/updateTime", content.updateTime);
498
+ if (content.deviceMac) this.sendMqtt(sensor_id, mqtt_active, client, "devData/deviceMac", content.deviceMac);
499
+ if (content.devTimezone != null) this.sendMqtt(sensor_id, mqtt_active, client, "devData/devTimezone", content.devTimezone);
500
+ if (content.wirelessStatus != null) this.sendMqtt(sensor_id, mqtt_active, client, "devData/wirelessStatus", content.wirelessStatus);
501
+ if (content.powerStatus != null) this.sendMqtt(sensor_id, mqtt_active, client, "devData/powerStatus", content.powerStatus);
502
+ if (content.weatherStatus != null) this.sendMqtt(sensor_id, mqtt_active, client, "devData/weatherStatus", content.weatherStatus);
503
+ if (luftdruck != null) this.sendMqtt(sensor_id, mqtt_active, client, "devData/atmospheric_pressure", luftdruck);
504
+ if (temp_innen != null) this.sendMqtt(sensor_id, mqtt_active, client, "devData/indoor_temp", temp_innen);
505
+ if (feuchte_innen != null) this.sendMqtt(sensor_id, mqtt_active, client, "devData/indoor_humidity", feuchte_innen);
506
+ if (temp_aussen != null) this.sendMqtt(sensor_id, mqtt_active, client, "devData/outdoor_temp", temp_aussen);
507
+ if (feuchte_aussen != null) this.sendMqtt(sensor_id, mqtt_active, client, "devData/outdoor_humidity", feuchte_aussen);
508
+
509
+ // >>> Zusätzliche dynamische Sensoren ausgeben:
510
+ for (const s of sensor_data) {
511
+ const { type, channel, curVal, hihgVal, lowVal, ...rest } = s;
512
+ const key = `${type}_${channel}`;
513
+ if (skipCombinations.has(key)) continue;
514
+ const base = `devData/sensor_${type}_${channel}`;
515
+
516
+ if (curVal != null && curVal !== 65535) {
517
+ await this.sendMqtt(sensor_id, mqtt_active, client, `${base}/current`, curVal);
518
+ }
519
+ if (hihgVal != null && hihgVal !== 65535) {
520
+ await this.sendMqtt(sensor_id, mqtt_active, client, `${base}/high`, hihgVal);
521
+ }
522
+ if (lowVal != null && lowVal !== 65535) {
523
+ await this.sendMqtt(sensor_id, mqtt_active, client, `${base}/low`, lowVal);
524
+ }
525
+
526
+ for (const [nestedKey, nestedVal] of Object.entries(rest)) {
527
+ if (nestedVal && typeof nestedVal === "object") {
528
+ const entries = Object.entries(nestedVal);
529
+
530
+ if (entries.length === 0) {
531
+ // Leeres Objekt → Platzhalter senden
532
+ const topic = `${base}/${nestedKey}`;
533
+ await this.sendMqtt(sensor_id, mqtt_active, client, topic, "n/a");
534
+ this.log.debug(`Send MQTT: ${topic}: n/a (empty object)`);
535
+ } else {
536
+ // Inhaltliches Objekt → Einzeldaten senden
537
+ for (const [k, v] of entries) {
538
+ if (v != null) {
539
+ const topic = `${base}/${nestedKey}/${k}`;
540
+ await this.sendMqtt(sensor_id, mqtt_active, client, topic, v);
541
+ this.log.debug(`Send MQTT: ${topic}: ${v}`);
542
+ }
543
+ }
544
+ }
545
+ }
546
+ }
547
+ }
548
+ }
549
+
550
+ // Basiswerte setzen
551
+ this.contentDevData = {
552
+ atmospheric_pressure: luftdruck,
553
+ indoor_temp: temp_innen,
554
+ indoor_humidity: feuchte_innen,
555
+ outdoor_temp: temp_aussen,
556
+ outdoor_humidity: feuchte_aussen,
557
+ };
558
+
559
+ // Zusätzliche Sensoren ergänzen (alle type/channel Kombinationen)
560
+ for (const s of sensor_data) {
561
+ const { type, channel, curVal, hihgVal, lowVal, ...rest } = s;
562
+ const key = `${type}_${channel}`;
563
+ if (skipCombinations.has(key)) continue; // Überspringen, wenn schon bekannt
564
+
565
+ const keyBase = `sensor_${type}_${channel}`;
566
+
567
+ if (curVal != null && curVal !== 65535) this.contentDevData[`${keyBase}_current`] = curVal;
568
+ if (hihgVal != null && hihgVal !== 65535) this.contentDevData[`${keyBase}_high`] = hihgVal;
569
+ if (lowVal != null && lowVal !== 65535) this.contentDevData[`${keyBase}_low`] = lowVal;
570
+
571
+ for (const [k, v] of Object.entries(rest)) {
572
+ if (v && typeof v === "object" && Object.keys(v).length === 0) {
573
+ this.contentDevData[`${keyBase}_${k}`] = "n/a";
574
+ } else if (v != null) {
575
+ this.contentDevData[`${keyBase}_${k}`] = v;
576
+ }
577
+ }
578
+ }
579
+
580
+ if (storeJson) {
581
+ this.log.debug("Save forecast.json to " + storeDir);
582
+ const json_object = JSON.stringify(forecast, null, 4);
583
+ fs.writeFileSync(path.join(storeDir, "forecast.json"), json_object, "utf-8");
584
+ }
585
+
586
+ this.printAllKeys(forecast);
587
+
588
+ const forecasts = forecast?.content?.forecast?.forecasts || [];
589
+
590
+ if (mqtt_active) {
591
+ await this.clearOldForecasts(sensor_id, client, 6);
592
+ await new Promise(r => setTimeout(r, 2000)); // sleep 2s
593
+
594
+ await this.sendForecasts(client, forecasts, celsius, sensor_id);
595
+
596
+ client.end(); // wie client.disconnect()
597
+ }
598
+
599
+ await this.createOrUpdateForecastDPs(forecastChannelId, forecasts, celsius);
600
+
601
+ return true;
602
+ }
603
+
604
+ onUnload(callback) {
605
+ try {
606
+ callback();
607
+ } catch (e) {
608
+ callback();
609
+ }
610
+ }
611
+ }
612
+
613
+ if (require.main !== module) {
614
+ module.exports = (options) => new WeatherSense(options);
615
+ } else {
616
+ new WeatherSense();
617
+ }
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "iobroker.weathersense",
3
+ "version": "1.0.0",
4
+ "description": "Read in data from WeatherSense",
5
+ "author": {
6
+ "name": "Daniel Luginbühl",
7
+ "email": "webmaster@ltspiceusers.ch"
8
+ },
9
+ "homepage": "https://github.com/ltspicer/ioBroker.weathersense",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "ioBroker",
13
+ "weathersense",
14
+ "weather",
15
+ "sense"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git@github.com:ltspicer/ioBroker.weathersense.git"
20
+ },
21
+ "engines": {
22
+ "node": ">= 20"
23
+ },
24
+ "dependencies": {
25
+ "@iobroker/adapter-core": "^3.2.3",
26
+ "axios": "^1.10.0",
27
+ "mqtt": "^5.13.0"
28
+ },
29
+ "devDependencies": {
30
+ "@alcalzone/release-script": "^3.8.0",
31
+ "@alcalzone/release-script-plugin-iobroker": "^3.7.2",
32
+ "@alcalzone/release-script-plugin-license": "^3.7.0",
33
+ "@alcalzone/release-script-plugin-manual-review": "^3.7.0",
34
+ "@iobroker/adapter-dev": "^1.4.0",
35
+ "@iobroker/dev-server": "^0.7.8",
36
+ "@iobroker/testing": "^5.0.4",
37
+ "@tsconfig/node18": "^18.2.4",
38
+ "@types/chai": "^4.3.12",
39
+ "@types/chai-as-promised": "^8.0.1",
40
+ "@types/mocha": "^10.0.10",
41
+ "@types/node": "^24.0.8",
42
+ "@types/proxyquire": "^1.3.31",
43
+ "@types/sinon": "^17.0.4",
44
+ "@types/sinon-chai": "^3.2.12",
45
+ "chai": "^4.5.0",
46
+ "chai-as-promised": "^8.0.1",
47
+ "eslint": "^9.28.0",
48
+ "mocha": "^11.5.0",
49
+ "proxyquire": "^2.1.3",
50
+ "sinon": "^21.0.0",
51
+ "sinon-chai": "^3.7.0",
52
+ "typescript": "~5.8.3"
53
+ },
54
+ "main": "main.js",
55
+ "files": [
56
+ "admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
57
+ "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
58
+ "lib/",
59
+ "www/",
60
+ "io-package.json",
61
+ "LICENSE",
62
+ "main.js"
63
+ ],
64
+ "scripts": {
65
+ "test:js": "mocha --config test/mocharc.custom.json \"{!(node_modules|test)/**/*.test.js,*.test.js,test/**/test!(PackageFiles|Startup).js}\"",
66
+ "test:package": "mocha test/package --exit",
67
+ "test:integration": "mocha test/integration --exit",
68
+ "test": "npm run test:js && npm run test:package",
69
+ "check": "tsc --noEmit -p tsconfig.check.json",
70
+ "lint": "eslint .",
71
+ "translate": "translate-adapter",
72
+ "release": "release-script",
73
+ "dev": "dev-server"
74
+ },
75
+ "bugs": {
76
+ "url": "https://github.com/ltspicer/ioBroker.weathersense/issues"
77
+ },
78
+ "readmeFilename": "README.md"
79
+ }