iobroker.nuasense 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/README.md +3 -0
- package/io-package.json +14 -14
- package/lib/ecowitt.js +85 -90
- package/main.js +430 -466
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -46,6 +46,9 @@ When you connect a station, NuaSense stores its readings and computes your forec
|
|
|
46
46
|
Placeholder for the next version (at the beginning of the line):
|
|
47
47
|
### **WORK IN PROGRESS**
|
|
48
48
|
-->
|
|
49
|
+
### 0.2.1 (2026-11-05)
|
|
50
|
+
* (NuaSense) checker fixes: workflow and tooling
|
|
51
|
+
|
|
49
52
|
### 0.2.0 (2026-11-05)
|
|
50
53
|
* (NuaSense) house & garden first: frost tonight, watering today in mm and minutes, rain next 24 h, plausibility; agronomy values behind Advanced
|
|
51
54
|
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "nuasense",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.2.1": {
|
|
7
|
+
"en": "Checker fixes: workflow and tooling",
|
|
8
|
+
"de": "Checker-Korrekturen: Workflow und Werkzeuge",
|
|
9
|
+
"ru": "Исправления по чекеру: рабочий процесс и инструменты",
|
|
10
|
+
"pt": "Correções do verificador: fluxo de trabalho e ferramentas",
|
|
11
|
+
"nl": "Checker-fixes: workflow en tooling",
|
|
12
|
+
"fr": "Corrections du vérificateur : flux de travail et outils",
|
|
13
|
+
"it": "Correzioni del checker: flusso di lavoro e strumenti",
|
|
14
|
+
"es": "Correcciones del verificador: flujo de trabajo y herramientas",
|
|
15
|
+
"pl": "Poprawki checkera: proces i narzędzia",
|
|
16
|
+
"uk": "Виправлення за чекером: робочий процес та інструменти",
|
|
17
|
+
"zh-cn": "检查器修复:工作流与工具"
|
|
18
|
+
},
|
|
6
19
|
"0.2.0": {
|
|
7
20
|
"en": "House & garden first: frost tonight, watering today in mm and minutes, rain next 24 h, plausibility; agronomy values behind Advanced",
|
|
8
21
|
"de": "Haus & Garten zuerst: Frost heute Nacht, Giessen heute in mm und Minuten, Regen der nächsten 24 h, Plausibilität; Agronomie-Werte unter «Erweitert»",
|
|
@@ -16,19 +29,6 @@
|
|
|
16
29
|
"uk": "Спочатку дім і сад: заморозки вночі, полив сьогодні в мм і хвилинах, дощ на 24 год, правдоподібність; агрономічні значення в розділі «Розширені»",
|
|
17
30
|
"zh-cn": "优先家居与花园:今夜霜冻、今日浇水量(毫米与分钟)、未来 24 小时降雨、合理性;农艺数值放在“高级”中"
|
|
18
31
|
},
|
|
19
|
-
"0.1.1": {
|
|
20
|
-
"en": "Checker fixes: Node.js 22, i18n for the admin UI, responsive sizes, trusted publishing workflow",
|
|
21
|
-
"de": "Checker-Korrekturen: Node.js 22, i18n für die Admin-Oberfläche, responsive Größen, Trusted-Publishing-Workflow",
|
|
22
|
-
"ru": "Исправления по чекеру: Node.js 22, i18n для панели администратора, адаптивные размеры, рабочий процесс доверенной публикации",
|
|
23
|
-
"pt": "Correções do verificador: Node.js 22, i18n para a interface de administração, tamanhos responsivos, fluxo de publicação confiável",
|
|
24
|
-
"nl": "Checker-fixes: Node.js 22, i18n voor de beheer-UI, responsieve groottes, trusted-publishing-workflow",
|
|
25
|
-
"fr": "Corrections du vérificateur : Node.js 22, i18n pour l'interface d'administration, tailles responsives, flux de publication de confiance",
|
|
26
|
-
"it": "Correzioni del checker: Node.js 22, i18n per l'interfaccia di amministrazione, dimensioni responsive, flusso di pubblicazione attendibile",
|
|
27
|
-
"es": "Correcciones del verificador: Node.js 22, i18n para la interfaz de administración, tamaños adaptables, flujo de publicación de confianza",
|
|
28
|
-
"pl": "Poprawki checkera: Node.js 22, i18n dla panelu administracyjnego, rozmiary responsywne, zaufany proces publikacji",
|
|
29
|
-
"uk": "Виправлення за чекером: Node.js 22, i18n для панелі адміністратора, адаптивні розміри, довірений процес публікації",
|
|
30
|
-
"zh-cn": "检查器修复:Node.js 22、管理界面 i18n、响应式尺寸、可信发布工作流"
|
|
31
|
-
},
|
|
32
32
|
"0.1.0": {
|
|
33
33
|
"en": "First release: first look and hourly agronomy as states; optional forwarding of a station from ioBroker to NuaSense",
|
|
34
34
|
"de": "Erste Version: Erster Blick und stündliche Agronomie als States; optional Weiterleitung einer Station von ioBroker zu NuaSense",
|
package/lib/ecowitt.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Metric readings in, an Ecowitt-format upload out.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* path to it. Everything here is pure, so it is tested without Node-RED.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
const crypto = require(
|
|
13
|
+
const crypto = require('node:crypto');
|
|
14
14
|
|
|
15
15
|
const INHG_PER_HPA = 1 / 33.8639;
|
|
16
16
|
const MPH_PER_MS = 1 / 0.44704;
|
|
@@ -19,31 +19,31 @@ const IN_PER_MM = 1 / 25.4;
|
|
|
19
19
|
// used the other way, so a flow that only has lux lands on the same number.
|
|
20
20
|
const LUX_PER_WM2 = 120;
|
|
21
21
|
|
|
22
|
-
const num =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const num = v => {
|
|
23
|
+
if (v === null || v === undefined || v === '') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const n = Number(v);
|
|
27
|
+
return Number.isFinite(n) ? n : null;
|
|
28
28
|
};
|
|
29
29
|
const fix = (n, digits) => (n === null ? null : Number(n.toFixed(digits)));
|
|
30
30
|
|
|
31
31
|
/** Ecowitt's `dateutc`: "YYYY-MM-DD HH:MM:SS", UTC. */
|
|
32
32
|
function dateUtc(when) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
const d = when instanceof Date ? when : when ? new Date(when) : new Date();
|
|
34
|
+
if (Number.isNaN(d.getTime())) {
|
|
35
|
+
return dateUtc(new Date());
|
|
36
|
+
}
|
|
37
|
+
return d.toISOString().slice(0, 19).replace('T', ' ');
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** A stable 32-hex PASSKEY from whatever names the device in the flow. */
|
|
41
41
|
function passkeyFor(deviceId) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
return crypto
|
|
43
|
+
.createHash('md5')
|
|
44
|
+
.update(String(deviceId || 'node-red'), 'utf8')
|
|
45
|
+
.digest('hex')
|
|
46
|
+
.toUpperCase();
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
@@ -53,86 +53,81 @@ function passkeyFor(deviceId) {
|
|
|
53
53
|
* solar_wm2 (W/m²) or light_lux, time (ISO or Date; now when absent)
|
|
54
54
|
* @returns the form fields, as strings, in the order a console sends them
|
|
55
55
|
*/
|
|
56
|
-
function toEcowitt(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
fields.uv = String(Math.round(uv));
|
|
104
|
-
}
|
|
105
|
-
return fields;
|
|
56
|
+
function toEcowitt(reading, { deviceId, stationType = 'node-red-contrib-nuasense' } = {}) {
|
|
57
|
+
const r = reading && typeof reading === 'object' ? reading : {};
|
|
58
|
+
const fields = {
|
|
59
|
+
PASSKEY: passkeyFor(deviceId),
|
|
60
|
+
stationtype: stationType,
|
|
61
|
+
dateutc: dateUtc(r.time),
|
|
62
|
+
};
|
|
63
|
+
const t = num(r.temperature);
|
|
64
|
+
if (t !== null) {
|
|
65
|
+
fields.tempf = String(fix((t * 9) / 5 + 32, 2));
|
|
66
|
+
}
|
|
67
|
+
const h = num(r.humidity);
|
|
68
|
+
if (h !== null) {
|
|
69
|
+
fields.humidity = String(Math.round(h));
|
|
70
|
+
}
|
|
71
|
+
const p = num(r.pressure);
|
|
72
|
+
if (p !== null) {
|
|
73
|
+
fields.baromabsin = String(fix(p * INHG_PER_HPA, 3));
|
|
74
|
+
}
|
|
75
|
+
const ws = num(r.wind_speed);
|
|
76
|
+
if (ws !== null) {
|
|
77
|
+
fields.windspeedmph = String(fix(ws * MPH_PER_MS, 2));
|
|
78
|
+
}
|
|
79
|
+
const wg = num(r.wind_gusts);
|
|
80
|
+
if (wg !== null) {
|
|
81
|
+
fields.windgustmph = String(fix(wg * MPH_PER_MS, 2));
|
|
82
|
+
}
|
|
83
|
+
const wd = num(r.wind_direction);
|
|
84
|
+
if (wd !== null) {
|
|
85
|
+
fields.winddir = String(Math.round(((wd % 360) + 360) % 360));
|
|
86
|
+
}
|
|
87
|
+
const rain = num(r.rain_total_mm);
|
|
88
|
+
if (rain !== null) {
|
|
89
|
+
fields.yearlyrainin = String(fix(rain * IN_PER_MM, 3));
|
|
90
|
+
}
|
|
91
|
+
const sol = num(r.solar_wm2);
|
|
92
|
+
const lux = num(r.light_lux);
|
|
93
|
+
if (sol !== null) {
|
|
94
|
+
fields.solarradiation = String(fix(sol, 2));
|
|
95
|
+
} else if (lux !== null) {
|
|
96
|
+
fields.solarradiation = String(fix(lux / LUX_PER_WM2, 2));
|
|
97
|
+
}
|
|
98
|
+
const uv = num(r.uv);
|
|
99
|
+
if (uv !== null) {
|
|
100
|
+
fields.uv = String(Math.round(uv));
|
|
101
|
+
}
|
|
102
|
+
return fields;
|
|
106
103
|
}
|
|
107
104
|
|
|
108
105
|
function encodeForm(fields) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
return Object.entries(fields)
|
|
107
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
108
|
+
.join('&');
|
|
112
109
|
}
|
|
113
110
|
|
|
114
111
|
/** The upload URL for a token as it is shown: kw7n-4hpd-x2vt (hyphens are fine on the wire). */
|
|
115
112
|
function uploadUrl(server, token) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
return `http://${host}/station/${tok}`;
|
|
113
|
+
const host = String(server || 'ingest.nuasense.com')
|
|
114
|
+
.trim()
|
|
115
|
+
.replace(/^https?:\/\//, '')
|
|
116
|
+
.replace(/\/+$/, '');
|
|
117
|
+
const tok = String(token || '')
|
|
118
|
+
.trim()
|
|
119
|
+
.toLowerCase();
|
|
120
|
+
if (!/^[a-z0-9-]{12,29}$/.test(tok)) {
|
|
121
|
+
throw new Error('the station path token looks wrong — copy it from the console');
|
|
122
|
+
}
|
|
123
|
+
return `http://${host}/station/${tok}`;
|
|
129
124
|
}
|
|
130
125
|
|
|
131
126
|
module.exports = {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
127
|
+
toEcowitt,
|
|
128
|
+
encodeForm,
|
|
129
|
+
uploadUrl,
|
|
130
|
+
passkeyFor,
|
|
131
|
+
dateUtc,
|
|
132
|
+
LUX_PER_WM2,
|
|
138
133
|
};
|
package/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
* NuaSense for ioBroker.
|
|
@@ -18,512 +18,476 @@
|
|
|
18
18
|
* wind, rain and light.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
const utils = require(
|
|
22
|
-
const { toEcowitt, encodeForm, uploadUrl } = require(
|
|
21
|
+
const utils = require('@iobroker/adapter-core');
|
|
22
|
+
const { toEcowitt, encodeForm, uploadUrl } = require('./lib/ecowitt');
|
|
23
23
|
|
|
24
24
|
const FORWARD_EVERY_MS = 60_000;
|
|
25
25
|
const MIN_POLL_MINUTES = 5;
|
|
26
26
|
// What the garden mostly is; the API returns watering for all three.
|
|
27
|
-
const PLANTS = [
|
|
27
|
+
const PLANTS = ['lawn', 'vegetables', 'beds'];
|
|
28
28
|
|
|
29
29
|
class Nuasense extends utils.Adapter {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
/** @param {Partial<utils.AdapterOptions>} [options] */
|
|
31
|
+
constructor(options) {
|
|
32
|
+
super({ ...options, name: 'nuasense' });
|
|
33
|
+
this.on('ready', this.onReady.bind(this));
|
|
34
|
+
this.on('unload', this.onUnload.bind(this));
|
|
35
|
+
this.pollTimer = null;
|
|
36
|
+
this.forwardTimer = null;
|
|
37
|
+
this.polling = false;
|
|
38
|
+
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
async onReady() {
|
|
41
|
+
await this.setStateAsync('info.connection', { val: false, ack: true });
|
|
42
|
+
const cfg = this.config;
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
44
|
+
const canRead = Boolean(cfg.apiKey);
|
|
45
|
+
const canSend = Boolean(cfg.forwardEnabled && cfg.forwardToken);
|
|
46
|
+
if (!canRead && !canSend) {
|
|
47
|
+
this.log.warn(
|
|
48
|
+
'Nothing configured: add an API key to read the station intelligence, or a station token to forward a station',
|
|
49
|
+
);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
if (canRead) {
|
|
54
|
+
await this.poll();
|
|
55
|
+
const minutes = Math.max(MIN_POLL_MINUTES, Number(cfg.pollMinutes) || 15);
|
|
56
|
+
this.pollTimer = this.setInterval(() => this.poll(), minutes * 60_000);
|
|
57
|
+
this.log.info(`reading NuaSense every ${minutes} min`);
|
|
58
|
+
}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
);
|
|
74
|
-
this.log.info(
|
|
75
|
-
`forwarding the mapped states to ${this.forwardUrl.replace(/\/station\/.*/, "/station/…")} every minute`,
|
|
76
|
-
);
|
|
60
|
+
if (canSend) {
|
|
61
|
+
try {
|
|
62
|
+
this.forwardUrl = uploadUrl(cfg.forwardServer, cfg.forwardToken);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
this.log.error(`forwarding is on but the station token looks wrong: ${e.message}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await this.forward();
|
|
68
|
+
this.forwardTimer = this.setInterval(() => this.forward(), FORWARD_EVERY_MS);
|
|
69
|
+
this.log.info(
|
|
70
|
+
`forwarding the mapped states to ${this.forwardUrl.replace(/\/station\/.*/, '/station/…')} every minute`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
77
73
|
}
|
|
78
|
-
}
|
|
79
74
|
|
|
80
|
-
|
|
75
|
+
// --- read ------------------------------------------------------------------
|
|
81
76
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
77
|
+
async api(path) {
|
|
78
|
+
const base = String(this.config.baseUrl || 'https://api.nuasense.com').replace(/\/+$/, '');
|
|
79
|
+
const res = await fetch(base + path, {
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
82
|
+
Accept: 'application/json',
|
|
83
|
+
},
|
|
84
|
+
signal: AbortSignal.timeout(30_000),
|
|
85
|
+
});
|
|
86
|
+
const text = await res.text();
|
|
87
|
+
let body;
|
|
88
|
+
try {
|
|
89
|
+
body = JSON.parse(text);
|
|
90
|
+
} catch {
|
|
91
|
+
body = { raw: text };
|
|
92
|
+
}
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
const err = new Error(body.error || body.message || `HTTP ${res.status}`);
|
|
95
|
+
err.status = res.status;
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
return body;
|
|
104
99
|
}
|
|
105
|
-
return body;
|
|
106
|
-
}
|
|
107
100
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
101
|
+
async poll() {
|
|
102
|
+
if (this.polling) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.polling = true;
|
|
106
|
+
try {
|
|
107
|
+
const list = await this.api('/api/nuasense/v1/ingest/stations');
|
|
108
|
+
await this.setStateAsync('info.connection', { val: true, ack: true });
|
|
109
|
+
const wanted = String(this.config.stationId || '').trim();
|
|
110
|
+
const stations = (list.stations || []).filter(s => !wanted || s.station_id === wanted);
|
|
111
|
+
if (stations.length === 0) {
|
|
112
|
+
this.log.warn(
|
|
113
|
+
wanted ? `station "${wanted}" is not on this account` : 'this account has no external stations yet',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
for (const station of stations) {
|
|
117
|
+
await this.updateStation(station);
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
await this.setStateAsync('info.connection', { val: false, ack: true });
|
|
121
|
+
this.log.warn(`NuaSense not read: ${e.message}`);
|
|
122
|
+
} finally {
|
|
123
|
+
this.polling = false;
|
|
124
|
+
}
|
|
111
125
|
}
|
|
112
|
-
this.polling = true;
|
|
113
|
-
try {
|
|
114
|
-
const list = await this.api("/api/nuasense/v1/ingest/stations");
|
|
115
|
-
await this.setStateAsync("info.connection", { val: true, ack: true });
|
|
116
|
-
const wanted = String(this.config.stationId || "").trim();
|
|
117
|
-
const stations = (list.stations || []).filter(
|
|
118
|
-
(s) => !wanted || s.station_id === wanted,
|
|
119
|
-
);
|
|
120
|
-
if (stations.length === 0) {
|
|
121
|
-
this.log.warn(
|
|
122
|
-
wanted
|
|
123
|
-
? `station "${wanted}" is not on this account`
|
|
124
|
-
: "this account has no external stations yet",
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
for (const station of stations) {
|
|
128
|
-
await this.updateStation(station);
|
|
129
|
-
}
|
|
130
|
-
} catch (e) {
|
|
131
|
-
await this.setStateAsync("info.connection", { val: false, ack: true });
|
|
132
|
-
this.log.warn(`NuaSense not read: ${e.message}`);
|
|
133
|
-
} finally {
|
|
134
|
-
this.polling = false;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
126
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
127
|
+
async updateStation(station) {
|
|
128
|
+
const id = `stations.${sanitize(station.station_id)}`;
|
|
129
|
+
await this.extendObjectAsync(id, {
|
|
130
|
+
type: 'channel',
|
|
131
|
+
common: { name: station.name || station.station_id },
|
|
132
|
+
native: { station_id: station.station_id },
|
|
133
|
+
});
|
|
145
134
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
135
|
+
const push = station.push || {};
|
|
136
|
+
await this.put(`${id}.lastUpload`, push.last_push_at || null, {
|
|
137
|
+
name: 'Last upload from the station',
|
|
138
|
+
type: 'string',
|
|
139
|
+
role: 'date',
|
|
140
|
+
});
|
|
141
|
+
await this.put(`${id}.device`, push.device || null, {
|
|
142
|
+
name: 'Device that uploads',
|
|
143
|
+
type: 'string',
|
|
144
|
+
role: 'text',
|
|
145
|
+
});
|
|
157
146
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
147
|
+
let look = null;
|
|
148
|
+
try {
|
|
149
|
+
look = await this.api(
|
|
150
|
+
`/api/nuasense/v1/ingest/first-look?station_id=${encodeURIComponent(station.station_id)}`,
|
|
151
|
+
);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
if (e.status !== 409) {
|
|
154
|
+
this.log.debug(`first look for ${station.station_id}: ${e.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const t = (look && look.tonight) || {};
|
|
158
|
+
const c = (look && (look.plausibility || look.station_check)) || {};
|
|
159
|
+
const w = (look && look.garden && look.garden.watering) || {};
|
|
171
160
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
161
|
+
// --- house & garden: every station ---------------------------------------
|
|
162
|
+
await this.put(`${id}.tonight.low`, numOrNull(t.low_c), {
|
|
163
|
+
name: "Tonight's low",
|
|
164
|
+
type: 'number',
|
|
165
|
+
role: 'value.temperature.min',
|
|
166
|
+
unit: '°C',
|
|
167
|
+
});
|
|
168
|
+
await this.put(`${id}.tonight.at`, t.at || null, {
|
|
169
|
+
name: 'Expected at (local)',
|
|
170
|
+
type: 'string',
|
|
171
|
+
role: 'text',
|
|
172
|
+
});
|
|
173
|
+
await this.put(`${id}.tonight.frost`, t.frost || null, {
|
|
174
|
+
name: 'Frost tonight: none, watch or frost',
|
|
175
|
+
type: 'string',
|
|
176
|
+
role: 'text',
|
|
177
|
+
});
|
|
178
|
+
await this.put(`${id}.tonight.grassLow`, numOrNull(t.grass_low_c), {
|
|
179
|
+
name: 'Estimated grass-level low',
|
|
180
|
+
type: 'number',
|
|
181
|
+
role: 'value.temperature',
|
|
182
|
+
unit: '°C',
|
|
183
|
+
});
|
|
195
184
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
await this.put(`${id}.garden.et0Source`, w.et0_source || null, {
|
|
239
|
-
name: "ET0 from: station or forecast",
|
|
240
|
-
type: "string",
|
|
241
|
-
role: "text",
|
|
242
|
-
});
|
|
185
|
+
const plant = PLANTS.includes(this.config.plant) ? this.config.plant : 'lawn';
|
|
186
|
+
const needMm = w.need_mm && typeof w.need_mm === 'object' ? numOrNull(w.need_mm[plant]) : null;
|
|
187
|
+
const rate = Number(this.config.sprinklerRate) || 0;
|
|
188
|
+
await this.put(`${id}.garden.wateringMm`, needMm, {
|
|
189
|
+
name: `Watering today (${plant})`,
|
|
190
|
+
type: 'number',
|
|
191
|
+
role: 'value',
|
|
192
|
+
unit: 'mm',
|
|
193
|
+
});
|
|
194
|
+
await this.put(
|
|
195
|
+
`${id}.garden.wateringMinutes`,
|
|
196
|
+
needMm !== null && rate > 0 ? Math.round((needMm / rate) * 60) : null,
|
|
197
|
+
{
|
|
198
|
+
name: 'Watering time today at the sprinkler rate',
|
|
199
|
+
type: 'number',
|
|
200
|
+
role: 'value',
|
|
201
|
+
unit: 'min',
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
await this.put(`${id}.garden.rainNext24h`, numOrNull(w.rain_next_24h_mm), {
|
|
205
|
+
name: 'Rain forecast, next 24 h',
|
|
206
|
+
type: 'number',
|
|
207
|
+
role: 'value.precipitation.forecast',
|
|
208
|
+
unit: 'mm',
|
|
209
|
+
});
|
|
210
|
+
await this.put(`${id}.garden.rainLast24h`, numOrNull(w.rain_last_24h_mm), {
|
|
211
|
+
name: 'Rain measured by the station, last 24 h',
|
|
212
|
+
type: 'number',
|
|
213
|
+
role: 'value.precipitation',
|
|
214
|
+
unit: 'mm',
|
|
215
|
+
});
|
|
216
|
+
await this.put(`${id}.garden.et0`, numOrNull(w.et0_mm), {
|
|
217
|
+
name: 'Evaporation today (ET0)',
|
|
218
|
+
type: 'number',
|
|
219
|
+
role: 'value',
|
|
220
|
+
unit: 'mm',
|
|
221
|
+
});
|
|
222
|
+
await this.put(`${id}.garden.et0Source`, w.et0_source || null, {
|
|
223
|
+
name: 'ET0 from: station or forecast',
|
|
224
|
+
type: 'string',
|
|
225
|
+
role: 'text',
|
|
226
|
+
});
|
|
243
227
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
228
|
+
await this.put(`${id}.plausibility.verdict`, c.verdict || null, {
|
|
229
|
+
name: 'Plausibility: plausible or implausible',
|
|
230
|
+
type: 'string',
|
|
231
|
+
role: 'text',
|
|
232
|
+
});
|
|
233
|
+
await this.put(`${id}.plausibility.reading`, numOrNull(c.reading_c), {
|
|
234
|
+
name: 'Latest reading',
|
|
235
|
+
type: 'number',
|
|
236
|
+
role: 'value.temperature',
|
|
237
|
+
unit: '°C',
|
|
238
|
+
});
|
|
239
|
+
await this.put(`${id}.plausibility.model`, numOrNull(c.model_c), {
|
|
240
|
+
name: 'Forecast model for that hour',
|
|
241
|
+
type: 'number',
|
|
242
|
+
role: 'value.temperature',
|
|
243
|
+
unit: '°C',
|
|
244
|
+
});
|
|
245
|
+
await this.put(`${id}.plausibility.difference`, numOrNull(c.diff_c), {
|
|
246
|
+
name: 'Reading minus model',
|
|
247
|
+
type: 'number',
|
|
248
|
+
role: 'value',
|
|
249
|
+
unit: 'K',
|
|
250
|
+
});
|
|
267
251
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
name: "Why there is no window",
|
|
295
|
-
type: "string",
|
|
296
|
-
role: "text",
|
|
297
|
-
});
|
|
252
|
+
// --- agronomy: only with "advanced" ----------------------------------------
|
|
253
|
+
if (!this.config.advanced) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const s = (look && look.spray) || {};
|
|
257
|
+
await this.put(`${id}.spray.window`, s.window ? `${s.window.from}–${s.window.to}` : s.date ? 'none' : null, {
|
|
258
|
+
name: 'Next spray window',
|
|
259
|
+
type: 'string',
|
|
260
|
+
role: 'text',
|
|
261
|
+
});
|
|
262
|
+
await this.put(`${id}.spray.date`, s.date || null, {
|
|
263
|
+
name: 'Day of the spray window',
|
|
264
|
+
type: 'string',
|
|
265
|
+
role: 'text',
|
|
266
|
+
});
|
|
267
|
+
await this.put(`${id}.spray.windMax`, numOrNull(s.wind_max_ms), {
|
|
268
|
+
name: 'Highest wind in the window',
|
|
269
|
+
type: 'number',
|
|
270
|
+
role: 'value.speed',
|
|
271
|
+
unit: 'm/s',
|
|
272
|
+
});
|
|
273
|
+
await this.put(`${id}.spray.reason`, s.reason || null, {
|
|
274
|
+
name: 'Why there is no window',
|
|
275
|
+
type: 'string',
|
|
276
|
+
role: 'text',
|
|
277
|
+
});
|
|
298
278
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
numOrNull(r.pressure),
|
|
388
|
-
{
|
|
389
|
-
name: "Pressure, hourly mean",
|
|
390
|
-
type: "number",
|
|
391
|
-
role: "value.pressure",
|
|
392
|
-
unit: "hPa",
|
|
393
|
-
},
|
|
394
|
-
],
|
|
395
|
-
];
|
|
396
|
-
for (const [key, value, common] of hourly) {
|
|
397
|
-
await this.put(`${id}.hourly.${key}`, value, common);
|
|
279
|
+
let row = null;
|
|
280
|
+
try {
|
|
281
|
+
const d = await this.api(
|
|
282
|
+
`/api/nuasense/v1/ingest/derived?station_id=${encodeURIComponent(station.station_id)}&start=-6h&limit=1`,
|
|
283
|
+
);
|
|
284
|
+
row = (d.data && d.data[0]) || null;
|
|
285
|
+
} catch (e) {
|
|
286
|
+
this.log.debug(`derived values for ${station.station_id}: ${e.message}`);
|
|
287
|
+
}
|
|
288
|
+
const r = row || {};
|
|
289
|
+
const hourly = [
|
|
290
|
+
['time', r.time || null, { name: 'Hour of these values', type: 'string', role: 'date' }],
|
|
291
|
+
['et0', numOrNull(r.et0_hour), { name: 'ET0 in the hour', type: 'number', role: 'value', unit: 'mm' }],
|
|
292
|
+
[
|
|
293
|
+
'leafWetnessHours',
|
|
294
|
+
numOrNull(r.lwd_consecutive_hours),
|
|
295
|
+
{
|
|
296
|
+
name: 'Consecutive wet-leaf hours',
|
|
297
|
+
type: 'number',
|
|
298
|
+
role: 'value',
|
|
299
|
+
unit: 'h',
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
['sprayQuality', r.spray_quality_label || null, { name: 'Spray quality', type: 'string', role: 'text' }],
|
|
303
|
+
[
|
|
304
|
+
'sprayIndex',
|
|
305
|
+
numOrNull(r.spray_quality_index),
|
|
306
|
+
{ name: 'Spray quality index', type: 'number', role: 'value' },
|
|
307
|
+
],
|
|
308
|
+
[
|
|
309
|
+
'sprayLimit',
|
|
310
|
+
r.spray_limiting_factor || null,
|
|
311
|
+
{ name: 'What limits spraying', type: 'string', role: 'text' },
|
|
312
|
+
],
|
|
313
|
+
[
|
|
314
|
+
'vpd',
|
|
315
|
+
numOrNull(r.vpd),
|
|
316
|
+
{
|
|
317
|
+
name: 'Vapour pressure deficit',
|
|
318
|
+
type: 'number',
|
|
319
|
+
role: 'value',
|
|
320
|
+
unit: 'kPa',
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
[
|
|
324
|
+
'deltaT',
|
|
325
|
+
numOrNull(r.delta_t),
|
|
326
|
+
{
|
|
327
|
+
name: 'Delta T',
|
|
328
|
+
type: 'number',
|
|
329
|
+
role: 'value.temperature',
|
|
330
|
+
unit: '°C',
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
[
|
|
334
|
+
'temperature',
|
|
335
|
+
numOrNull(r.temperature),
|
|
336
|
+
{
|
|
337
|
+
name: 'Temperature, hourly mean',
|
|
338
|
+
type: 'number',
|
|
339
|
+
role: 'value.temperature',
|
|
340
|
+
unit: '°C',
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
[
|
|
344
|
+
'humidity',
|
|
345
|
+
numOrNull(r.humidity),
|
|
346
|
+
{
|
|
347
|
+
name: 'Humidity, hourly mean',
|
|
348
|
+
type: 'number',
|
|
349
|
+
role: 'value.humidity',
|
|
350
|
+
unit: '%',
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
[
|
|
354
|
+
'pressure',
|
|
355
|
+
numOrNull(r.pressure),
|
|
356
|
+
{
|
|
357
|
+
name: 'Pressure, hourly mean',
|
|
358
|
+
type: 'number',
|
|
359
|
+
role: 'value.pressure',
|
|
360
|
+
unit: 'hPa',
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
];
|
|
364
|
+
for (const [key, value, common] of hourly) {
|
|
365
|
+
await this.put(`${id}.hourly.${key}`, value, common);
|
|
366
|
+
}
|
|
398
367
|
}
|
|
399
|
-
}
|
|
400
368
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
369
|
+
/** Create the state if needed, then write it acknowledged. */
|
|
370
|
+
async put(id, value, common) {
|
|
371
|
+
await this.setObjectNotExistsAsync(id, {
|
|
372
|
+
type: 'state',
|
|
373
|
+
common: { read: true, write: false, ...common },
|
|
374
|
+
native: {},
|
|
375
|
+
});
|
|
376
|
+
await this.setStateAsync(id, { val: value, ack: true });
|
|
377
|
+
}
|
|
410
378
|
|
|
411
|
-
|
|
379
|
+
// --- send ------------------------------------------------------------------
|
|
412
380
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
381
|
+
async readMapped(stateId) {
|
|
382
|
+
if (!stateId) {
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const s = await this.getForeignStateAsync(stateId);
|
|
387
|
+
if (!s || s.val === null || s.val === undefined || s.val === '') {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
const n = Number(s.val);
|
|
391
|
+
return Number.isFinite(n) ? n : undefined;
|
|
392
|
+
} catch (e) {
|
|
393
|
+
this.log.debug(`cannot read ${stateId}: ${e.message}`);
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
427
396
|
}
|
|
428
|
-
}
|
|
429
397
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
ack: true,
|
|
494
|
-
});
|
|
495
|
-
this.log.debug(`upload failed: ${e.message}`);
|
|
398
|
+
async forward() {
|
|
399
|
+
const cfg = this.config;
|
|
400
|
+
let wind = await this.readMapped(cfg.windSpeedState);
|
|
401
|
+
let gust = await this.readMapped(cfg.windGustState);
|
|
402
|
+
if (cfg.windUnit === 'kmh') {
|
|
403
|
+
if (wind !== undefined) {
|
|
404
|
+
wind /= 3.6;
|
|
405
|
+
}
|
|
406
|
+
if (gust !== undefined) {
|
|
407
|
+
gust /= 3.6;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const reading = {
|
|
411
|
+
temperature: await this.readMapped(cfg.temperatureState),
|
|
412
|
+
humidity: await this.readMapped(cfg.humidityState),
|
|
413
|
+
pressure: await this.readMapped(cfg.pressureState),
|
|
414
|
+
wind_speed: wind,
|
|
415
|
+
wind_gusts: gust,
|
|
416
|
+
wind_direction: await this.readMapped(cfg.windDirectionState),
|
|
417
|
+
rain_total_mm: await this.readMapped(cfg.rainTotalState),
|
|
418
|
+
solar_wm2: await this.readMapped(cfg.solarState),
|
|
419
|
+
light_lux: await this.readMapped(cfg.luxState),
|
|
420
|
+
};
|
|
421
|
+
const fields = toEcowitt(reading, {
|
|
422
|
+
deviceId: cfg.forwardDeviceId || `iobroker-${this.namespace}`,
|
|
423
|
+
stationType: 'iobroker.nuasense_V0.2.1',
|
|
424
|
+
});
|
|
425
|
+
if (Object.keys(fields).length <= 3) {
|
|
426
|
+
// PASSKEY, stationtype, dateutc and nothing else: no mapped state had a value.
|
|
427
|
+
await this.setStateAsync('forward.lastStatus', {
|
|
428
|
+
val: 'no values to send (check the mapped states)',
|
|
429
|
+
ack: true,
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
const res = await fetch(this.forwardUrl, {
|
|
435
|
+
method: 'POST',
|
|
436
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
437
|
+
body: encodeForm(fields),
|
|
438
|
+
signal: AbortSignal.timeout(10_000),
|
|
439
|
+
});
|
|
440
|
+
const text = (await res.text()).slice(0, 80);
|
|
441
|
+
await this.setStateAsync('forward.lastSentAt', {
|
|
442
|
+
val: new Date().toISOString(),
|
|
443
|
+
ack: true,
|
|
444
|
+
});
|
|
445
|
+
await this.setStateAsync('forward.lastStatus', {
|
|
446
|
+
val: `${res.status} ${text}`.trim(),
|
|
447
|
+
ack: true,
|
|
448
|
+
});
|
|
449
|
+
if (res.status === 404) {
|
|
450
|
+
this.log.warn('NuaSense does not know this station token (revoked, or mistyped)');
|
|
451
|
+
} else if (res.status === 409) {
|
|
452
|
+
this.log.warn('the station token is bound to another device; issue a new path in the console');
|
|
453
|
+
}
|
|
454
|
+
} catch (e) {
|
|
455
|
+
await this.setStateAsync('forward.lastStatus', {
|
|
456
|
+
val: `failed: ${e.message}`,
|
|
457
|
+
ack: true,
|
|
458
|
+
});
|
|
459
|
+
this.log.debug(`upload failed: ${e.message}`);
|
|
460
|
+
}
|
|
496
461
|
}
|
|
497
|
-
}
|
|
498
462
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
463
|
+
onUnload(callback) {
|
|
464
|
+
try {
|
|
465
|
+
if (this.pollTimer) {
|
|
466
|
+
this.clearInterval(this.pollTimer);
|
|
467
|
+
}
|
|
468
|
+
if (this.forwardTimer) {
|
|
469
|
+
this.clearInterval(this.forwardTimer);
|
|
470
|
+
}
|
|
471
|
+
} finally {
|
|
472
|
+
callback();
|
|
473
|
+
}
|
|
509
474
|
}
|
|
510
|
-
}
|
|
511
475
|
}
|
|
512
476
|
|
|
513
477
|
function sanitize(id) {
|
|
514
|
-
|
|
478
|
+
return String(id).replace(/[^A-Za-z0-9_-]/g, '_');
|
|
515
479
|
}
|
|
516
480
|
|
|
517
481
|
function numOrNull(v) {
|
|
518
|
-
|
|
519
|
-
|
|
482
|
+
const n = Number(v);
|
|
483
|
+
return v === null || v === undefined || !Number.isFinite(n) ? null : n;
|
|
520
484
|
}
|
|
521
485
|
|
|
522
486
|
if (require.main !== module) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
487
|
+
// Export the constructor in compact mode
|
|
488
|
+
/** @param {Partial<utils.AdapterOptions>} [options] */
|
|
489
|
+
module.exports = options => new Nuasense(options);
|
|
526
490
|
} else {
|
|
527
|
-
|
|
528
|
-
|
|
491
|
+
// otherwise start the instance directly
|
|
492
|
+
new Nuasense();
|
|
529
493
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.nuasense",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "NuaSense for ioBroker: what your weather station means for your house and garden (frost tonight, watering today, rain next 24 h, plausibility), and optionally forward a station from ioBroker to NuaSense",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NuaSense",
|
|
@@ -31,12 +31,15 @@
|
|
|
31
31
|
"@iobroker/adapter-core": "^3.4.3"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
+
"@alcalzone/release-script": "^5.2.1",
|
|
35
|
+
"@alcalzone/release-script-plugin-iobroker": "^5.2.0",
|
|
36
|
+
"@alcalzone/release-script-plugin-license": "^5.2.2",
|
|
37
|
+
"@alcalzone/release-script-plugin-manual-review": "^5.2.0",
|
|
38
|
+
"@iobroker/adapter-dev": "^1.5.0",
|
|
34
39
|
"@iobroker/eslint-config": "^2.3.4",
|
|
35
40
|
"@iobroker/testing": "^6.3.0",
|
|
36
41
|
"@iobroker/types": "^7.2.2",
|
|
37
|
-
"@types/node": "^22.20.2"
|
|
38
|
-
"chai": "^4.5.0",
|
|
39
|
-
"mocha": "^10.8.2"
|
|
42
|
+
"@types/node": "^22.20.2"
|
|
40
43
|
},
|
|
41
44
|
"main": "main.js",
|
|
42
45
|
"files": [
|
|
@@ -53,7 +56,8 @@
|
|
|
53
56
|
"test:integration": "mocha test/integration --exit",
|
|
54
57
|
"test": "npm run test:js && npm run test:package",
|
|
55
58
|
"lint": "eslint -c eslint.config.mjs .",
|
|
56
|
-
"release": "
|
|
59
|
+
"release": "release-script",
|
|
60
|
+
"translate": "translate-adapter"
|
|
57
61
|
},
|
|
58
62
|
"bugs": {
|
|
59
63
|
"url": "https://github.com/nuasense/ioBroker.nuasense/issues"
|