iobroker.nuasense 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/main.js CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";
1
+ 'use strict';
2
2
 
3
3
  /*
4
4
  * NuaSense for ioBroker.
@@ -18,512 +18,504 @@
18
18
  * wind, rain and light.
19
19
  */
20
20
 
21
- const utils = require("@iobroker/adapter-core");
22
- const { toEcowitt, encodeForm, uploadUrl } = require("./lib/ecowitt");
21
+ const utils = require('@iobroker/adapter-core');
22
+ const { toEcowitt, encodeForm, uploadUrl } = require('./lib/ecowitt');
23
+ const { version } = require('./package.json');
23
24
 
24
25
  const FORWARD_EVERY_MS = 60_000;
25
26
  const MIN_POLL_MINUTES = 5;
26
27
  // What the garden mostly is; the API returns watering for all three.
27
- const PLANTS = ["lawn", "vegetables", "beds"];
28
+ const PLANTS = ['lawn', 'vegetables', 'beds'];
28
29
 
29
30
  class Nuasense extends utils.Adapter {
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
- }
31
+ /** @param {Partial<utils.AdapterOptions>} [options] */
32
+ constructor(options) {
33
+ super({ ...options, name: 'nuasense' });
34
+ this.on('ready', this.onReady.bind(this));
35
+ this.on('unload', this.onUnload.bind(this));
36
+ this.pollTimer = null;
37
+ this.forwardTimer = null;
38
+ this.polling = false;
39
+ }
39
40
 
40
- async onReady() {
41
- await this.setStateAsync("info.connection", { val: false, ack: true });
42
- const cfg = this.config;
41
+ async onReady() {
42
+ await this.setStateAsync('info.connection', { val: false, ack: true });
43
+ const cfg = this.config;
43
44
 
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
- }
45
+ const canRead = Boolean(cfg.apiKey);
46
+ const canSend = Boolean(cfg.forwardEnabled && cfg.forwardToken);
47
+ if (!canRead && !canSend) {
48
+ this.log.warn(
49
+ 'Nothing configured: add an API key to read the station intelligence, or a station token to forward a station',
50
+ );
51
+ return;
52
+ }
52
53
 
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
- }
54
+ if (canRead) {
55
+ await this.poll();
56
+ const minutes = Math.max(MIN_POLL_MINUTES, Number(cfg.pollMinutes) || 15);
57
+ this.pollTimer = this.setInterval(() => this.poll(), minutes * 60_000);
58
+ this.log.info(`reading NuaSense every ${minutes} min`);
59
+ }
59
60
 
60
- if (canSend) {
61
- try {
62
- this.forwardUrl = uploadUrl(cfg.forwardServer, cfg.forwardToken);
63
- } catch (e) {
64
- this.log.error(
65
- `forwarding is on but the station token looks wrong: ${e.message}`,
66
- );
67
- return;
68
- }
69
- await this.forward();
70
- this.forwardTimer = this.setInterval(
71
- () => this.forward(),
72
- FORWARD_EVERY_MS,
73
- );
74
- this.log.info(
75
- `forwarding the mapped states to ${this.forwardUrl.replace(/\/station\/.*/, "/station/…")} every minute`,
76
- );
61
+ if (canSend) {
62
+ try {
63
+ this.forwardUrl = uploadUrl(cfg.forwardServer, cfg.forwardToken);
64
+ } catch (e) {
65
+ this.log.error(`forwarding is on but the station token looks wrong: ${e.message}`);
66
+ return;
67
+ }
68
+ await this.forward();
69
+ this.forwardTimer = this.setInterval(() => this.forward(), FORWARD_EVERY_MS);
70
+ this.log.info(
71
+ `forwarding the mapped states to ${this.forwardUrl.replace(/\/station\/.*/, '/station/…')} every minute`,
72
+ );
73
+ }
77
74
  }
78
- }
79
75
 
80
- // --- read ------------------------------------------------------------------
76
+ // --- read ------------------------------------------------------------------
81
77
 
82
- async api(path) {
83
- const base = String(
84
- this.config.baseUrl || "https://api.nuasense.com",
85
- ).replace(/\/+$/, "");
86
- const res = await fetch(base + path, {
87
- headers: {
88
- Authorization: `Bearer ${this.config.apiKey}`,
89
- Accept: "application/json",
90
- },
91
- signal: AbortSignal.timeout(30_000),
92
- });
93
- const text = await res.text();
94
- let body;
95
- try {
96
- body = JSON.parse(text);
97
- } catch {
98
- body = { raw: text };
99
- }
100
- if (!res.ok) {
101
- const err = new Error(body.error || body.message || `HTTP ${res.status}`);
102
- err.status = res.status;
103
- throw err;
78
+ async api(path) {
79
+ const base = String(this.config.baseUrl || 'https://api.nuasense.com').replace(/\/+$/, '');
80
+ const res = await fetch(base + path, {
81
+ headers: {
82
+ Authorization: `Bearer ${this.config.apiKey}`,
83
+ Accept: 'application/json',
84
+ },
85
+ signal: AbortSignal.timeout(30_000),
86
+ });
87
+ const text = await res.text();
88
+ let body;
89
+ try {
90
+ body = JSON.parse(text);
91
+ } catch {
92
+ body = { raw: text };
93
+ }
94
+ if (!res.ok) {
95
+ const err = new Error(body.error || body.message || `HTTP ${res.status}`);
96
+ err.status = res.status;
97
+ throw err;
98
+ }
99
+ return body;
104
100
  }
105
- return body;
106
- }
107
101
 
108
- async poll() {
109
- if (this.polling) {
110
- return;
102
+ async poll() {
103
+ if (this.polling) {
104
+ return;
105
+ }
106
+ this.polling = true;
107
+ try {
108
+ const list = await this.api('/api/nuasense/v1/ingest/stations');
109
+ await this.setStateAsync('info.connection', { val: true, ack: true });
110
+ const wanted = String(this.config.stationId || '').trim();
111
+ const stations = (list.stations || []).filter(s => !wanted || s.station_id === wanted);
112
+ if (stations.length === 0) {
113
+ this.log.warn(
114
+ wanted ? `station "${wanted}" is not on this account` : 'this account has no external stations yet',
115
+ );
116
+ }
117
+ for (const station of stations) {
118
+ await this.updateStation(station);
119
+ }
120
+ } catch (e) {
121
+ await this.setStateAsync('info.connection', { val: false, ack: true });
122
+ this.log.warn(`NuaSense not read: ${e.message}`);
123
+ } finally {
124
+ this.polling = false;
125
+ }
111
126
  }
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
127
 
138
- async updateStation(station) {
139
- const id = `stations.${sanitize(station.station_id)}`;
140
- await this.extendObjectAsync(id, {
141
- type: "channel",
142
- common: { name: station.name || station.station_id },
143
- native: { station_id: station.station_id },
144
- });
128
+ async updateStation(station) {
129
+ const id = `stations.${sanitize(station.station_id)}`;
130
+ await this.setObjectNotExistsAsync('stations', {
131
+ type: 'folder',
132
+ common: { name: 'Stations' },
133
+ native: {},
134
+ });
135
+ await this.extendObjectAsync(id, {
136
+ type: 'device',
137
+ common: { name: station.name || station.station_id },
138
+ native: { station_id: station.station_id },
139
+ });
140
+ await this.channel(`${id}.tonight`, 'Tonight');
141
+ await this.channel(`${id}.garden`, 'Garden');
142
+ await this.channel(`${id}.plausibility`, 'Plausibility');
145
143
 
146
- const push = station.push || {};
147
- await this.put(`${id}.lastUpload`, push.last_push_at || null, {
148
- name: "Last upload from the station",
149
- type: "string",
150
- role: "date",
151
- });
152
- await this.put(`${id}.device`, push.device || null, {
153
- name: "Device that uploads",
154
- type: "string",
155
- role: "text",
156
- });
144
+ const push = station.push || {};
145
+ await this.put(`${id}.lastUpload`, push.last_push_at || null, {
146
+ name: 'Last upload from the station',
147
+ type: 'string',
148
+ role: 'date',
149
+ });
150
+ await this.put(`${id}.device`, push.device || null, {
151
+ name: 'Device that uploads',
152
+ type: 'string',
153
+ role: 'text',
154
+ });
157
155
 
158
- let look = null;
159
- try {
160
- look = await this.api(
161
- `/api/nuasense/v1/ingest/first-look?station_id=${encodeURIComponent(station.station_id)}`,
162
- );
163
- } catch (e) {
164
- if (e.status !== 409) {
165
- this.log.debug(`first look for ${station.station_id}: ${e.message}`);
166
- }
167
- }
168
- const t = (look && look.tonight) || {};
169
- const c = (look && (look.plausibility || look.station_check)) || {};
170
- const w = (look && look.garden && look.garden.watering) || {};
156
+ let look = null;
157
+ try {
158
+ look = await this.api(
159
+ `/api/nuasense/v1/ingest/first-look?station_id=${encodeURIComponent(station.station_id)}`,
160
+ );
161
+ } catch (e) {
162
+ if (e.status !== 409) {
163
+ this.log.debug(`first look for ${station.station_id}: ${e.message}`);
164
+ }
165
+ }
166
+ const t = (look && look.tonight) || {};
167
+ const c = (look && (look.plausibility || look.station_check)) || {};
168
+ const w = (look && look.garden && look.garden.watering) || {};
171
169
 
172
- // --- house & garden: every station ---------------------------------------
173
- await this.put(`${id}.tonight.low`, numOrNull(t.low_c), {
174
- name: "Tonight's low",
175
- type: "number",
176
- role: "value.temperature.min",
177
- unit: "°C",
178
- });
179
- await this.put(`${id}.tonight.at`, t.at || null, {
180
- name: "Expected at (local)",
181
- type: "string",
182
- role: "text",
183
- });
184
- await this.put(`${id}.tonight.frost`, t.frost || null, {
185
- name: "Frost tonight: none, watch or frost",
186
- type: "string",
187
- role: "text",
188
- });
189
- await this.put(`${id}.tonight.grassLow`, numOrNull(t.grass_low_c), {
190
- name: "Estimated grass-level low",
191
- type: "number",
192
- role: "value.temperature",
193
- unit: "°C",
194
- });
170
+ // --- house & garden: every station ---------------------------------------
171
+ await this.put(`${id}.tonight.low`, numOrNull(t.low_c), {
172
+ name: "Tonight's low",
173
+ type: 'number',
174
+ role: 'value.temperature.min',
175
+ unit: '°C',
176
+ });
177
+ await this.put(`${id}.tonight.at`, t.at || null, {
178
+ name: 'Expected at (local)',
179
+ type: 'string',
180
+ role: 'text',
181
+ });
182
+ await this.put(`${id}.tonight.frost`, t.frost || null, {
183
+ name: 'Frost tonight: none, watch or frost',
184
+ type: 'string',
185
+ role: 'text',
186
+ });
187
+ await this.put(`${id}.tonight.grassLow`, numOrNull(t.grass_low_c), {
188
+ name: 'Estimated grass-level low',
189
+ type: 'number',
190
+ role: 'value.temperature',
191
+ unit: '°C',
192
+ });
195
193
 
196
- const plant = PLANTS.includes(this.config.plant)
197
- ? this.config.plant
198
- : "lawn";
199
- const needMm =
200
- w.need_mm && typeof w.need_mm === "object"
201
- ? numOrNull(w.need_mm[plant])
202
- : null;
203
- const rate = Number(this.config.sprinklerRate) || 0;
204
- await this.put(`${id}.garden.wateringMm`, needMm, {
205
- name: `Watering today (${plant})`,
206
- type: "number",
207
- role: "value",
208
- unit: "mm",
209
- });
210
- await this.put(
211
- `${id}.garden.wateringMinutes`,
212
- needMm !== null && rate > 0 ? Math.round((needMm / rate) * 60) : null,
213
- {
214
- name: "Watering time today at the sprinkler rate",
215
- type: "number",
216
- role: "value",
217
- unit: "min",
218
- },
219
- );
220
- await this.put(`${id}.garden.rainNext24h`, numOrNull(w.rain_next_24h_mm), {
221
- name: "Rain forecast, next 24 h",
222
- type: "number",
223
- role: "value.precipitation.forecast",
224
- unit: "mm",
225
- });
226
- await this.put(`${id}.garden.rainLast24h`, numOrNull(w.rain_last_24h_mm), {
227
- name: "Rain measured by the station, last 24 h",
228
- type: "number",
229
- role: "value.precipitation",
230
- unit: "mm",
231
- });
232
- await this.put(`${id}.garden.et0`, numOrNull(w.et0_mm), {
233
- name: "Evaporation today (ET0)",
234
- type: "number",
235
- role: "value",
236
- unit: "mm",
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
- });
194
+ const plant = PLANTS.includes(this.config.plant) ? this.config.plant : 'lawn';
195
+ const needMm = w.need_mm && typeof w.need_mm === 'object' ? numOrNull(w.need_mm[plant]) : null;
196
+ const rate = Number(this.config.sprinklerRate) || 0;
197
+ await this.put(`${id}.garden.wateringMm`, needMm, {
198
+ name: `Watering today (${plant})`,
199
+ type: 'number',
200
+ role: 'value',
201
+ unit: 'mm',
202
+ });
203
+ await this.put(
204
+ `${id}.garden.wateringMinutes`,
205
+ needMm !== null && rate > 0 ? Math.round((needMm / rate) * 60) : null,
206
+ {
207
+ name: 'Watering time today at the sprinkler rate',
208
+ type: 'number',
209
+ role: 'value',
210
+ unit: 'min',
211
+ },
212
+ );
213
+ await this.put(`${id}.garden.rainNext24h`, numOrNull(w.rain_next_24h_mm), {
214
+ name: 'Rain forecast, next 24 h',
215
+ type: 'number',
216
+ role: 'value.precipitation.forecast.0',
217
+ unit: 'mm',
218
+ });
219
+ await this.put(`${id}.garden.rainLast24h`, numOrNull(w.rain_last_24h_mm), {
220
+ name: 'Rain measured by the station, last 24 h',
221
+ type: 'number',
222
+ role: 'value.precipitation',
223
+ unit: 'mm',
224
+ });
225
+ await this.put(`${id}.garden.et0`, numOrNull(w.et0_mm), {
226
+ name: 'Evaporation today (ET0)',
227
+ type: 'number',
228
+ role: 'value',
229
+ unit: 'mm',
230
+ });
231
+ await this.put(`${id}.garden.et0Source`, w.et0_source || null, {
232
+ name: 'ET0 from: station or forecast',
233
+ type: 'string',
234
+ role: 'text',
235
+ });
243
236
 
244
- await this.put(`${id}.plausibility.verdict`, c.verdict || null, {
245
- name: "Plausibility: plausible or implausible",
246
- type: "string",
247
- role: "text",
248
- });
249
- await this.put(`${id}.plausibility.reading`, numOrNull(c.reading_c), {
250
- name: "Latest reading",
251
- type: "number",
252
- role: "value.temperature",
253
- unit: "°C",
254
- });
255
- await this.put(`${id}.plausibility.model`, numOrNull(c.model_c), {
256
- name: "Forecast model for that hour",
257
- type: "number",
258
- role: "value.temperature",
259
- unit: "°C",
260
- });
261
- await this.put(`${id}.plausibility.difference`, numOrNull(c.diff_c), {
262
- name: "Reading minus model",
263
- type: "number",
264
- role: "value",
265
- unit: "K",
266
- });
237
+ await this.put(`${id}.plausibility.verdict`, c.verdict || null, {
238
+ name: 'Plausibility: plausible or implausible',
239
+ type: 'string',
240
+ role: 'text',
241
+ });
242
+ await this.put(`${id}.plausibility.reading`, numOrNull(c.reading_c), {
243
+ name: 'Latest reading',
244
+ type: 'number',
245
+ role: 'value.temperature',
246
+ unit: '°C',
247
+ });
248
+ await this.put(`${id}.plausibility.model`, numOrNull(c.model_c), {
249
+ name: 'Forecast model for that hour',
250
+ type: 'number',
251
+ role: 'value.temperature',
252
+ unit: '°C',
253
+ });
254
+ await this.put(`${id}.plausibility.difference`, numOrNull(c.diff_c), {
255
+ name: 'Reading minus model',
256
+ type: 'number',
257
+ role: 'value',
258
+ unit: 'K',
259
+ });
267
260
 
268
- // --- agronomy: only with "advanced" ----------------------------------------
269
- if (!this.config.advanced) {
270
- return;
271
- }
272
- const s = (look && look.spray) || {};
273
- await this.put(
274
- `${id}.spray.window`,
275
- s.window ? `${s.window.from}–${s.window.to}` : s.date ? "none" : null,
276
- {
277
- name: "Next spray window",
278
- type: "string",
279
- role: "text",
280
- },
281
- );
282
- await this.put(`${id}.spray.date`, s.date || null, {
283
- name: "Day of the spray window",
284
- type: "string",
285
- role: "text",
286
- });
287
- await this.put(`${id}.spray.windMax`, numOrNull(s.wind_max_ms), {
288
- name: "Highest wind in the window",
289
- type: "number",
290
- role: "value.speed",
291
- unit: "m/s",
292
- });
293
- await this.put(`${id}.spray.reason`, s.reason || null, {
294
- name: "Why there is no window",
295
- type: "string",
296
- role: "text",
297
- });
261
+ // --- agronomy: only with "advanced" ----------------------------------------
262
+ if (!this.config.advanced) {
263
+ return;
264
+ }
265
+ await this.channel(`${id}.spray`, 'Spray window');
266
+ await this.channel(`${id}.hourly`, 'Hourly agronomy');
267
+ const s = (look && look.spray) || {};
268
+ await this.put(`${id}.spray.window`, s.window ? `${s.window.from}–${s.window.to}` : s.date ? 'none' : null, {
269
+ name: 'Next spray window',
270
+ type: 'string',
271
+ role: 'text',
272
+ });
273
+ await this.put(`${id}.spray.date`, s.date || null, {
274
+ name: 'Day of the spray window',
275
+ type: 'string',
276
+ role: 'text',
277
+ });
278
+ await this.put(`${id}.spray.windMax`, numOrNull(s.wind_max_ms), {
279
+ name: 'Highest wind in the window',
280
+ type: 'number',
281
+ role: 'value.speed.max.wind',
282
+ unit: 'm/s',
283
+ });
284
+ await this.put(`${id}.spray.reason`, s.reason || null, {
285
+ name: 'Why there is no window',
286
+ type: 'string',
287
+ role: 'text',
288
+ });
298
289
 
299
- let row = null;
300
- try {
301
- const d = await this.api(
302
- `/api/nuasense/v1/ingest/derived?station_id=${encodeURIComponent(station.station_id)}&start=-6h&limit=1`,
303
- );
304
- row = (d.data && d.data[0]) || null;
305
- } catch (e) {
306
- this.log.debug(`derived values for ${station.station_id}: ${e.message}`);
290
+ let row = null;
291
+ try {
292
+ const d = await this.api(
293
+ `/api/nuasense/v1/ingest/derived?station_id=${encodeURIComponent(station.station_id)}&start=-6h&limit=1`,
294
+ );
295
+ row = (d.data && d.data[0]) || null;
296
+ } catch (e) {
297
+ this.log.debug(`derived values for ${station.station_id}: ${e.message}`);
298
+ }
299
+ const r = row || {};
300
+ const hourly = [
301
+ ['time', r.time || null, { name: 'Hour of these values', type: 'string', role: 'date' }],
302
+ ['et0', numOrNull(r.et0_hour), { name: 'ET0 in the hour', type: 'number', role: 'value', unit: 'mm' }],
303
+ [
304
+ 'leafWetnessHours',
305
+ numOrNull(r.lwd_consecutive_hours),
306
+ {
307
+ name: 'Consecutive wet-leaf hours',
308
+ type: 'number',
309
+ role: 'value',
310
+ unit: 'h',
311
+ },
312
+ ],
313
+ ['sprayQuality', r.spray_quality_label || null, { name: 'Spray quality', type: 'string', role: 'text' }],
314
+ [
315
+ 'sprayIndex',
316
+ numOrNull(r.spray_quality_index),
317
+ { name: 'Spray quality index', type: 'number', role: 'value' },
318
+ ],
319
+ [
320
+ 'sprayLimit',
321
+ r.spray_limiting_factor || null,
322
+ { name: 'What limits spraying', type: 'string', role: 'text' },
323
+ ],
324
+ [
325
+ 'vpd',
326
+ numOrNull(r.vpd),
327
+ {
328
+ name: 'Vapour pressure deficit',
329
+ type: 'number',
330
+ role: 'value',
331
+ unit: 'kPa',
332
+ },
333
+ ],
334
+ [
335
+ 'deltaT',
336
+ numOrNull(r.delta_t),
337
+ {
338
+ name: 'Delta T',
339
+ type: 'number',
340
+ role: 'value.temperature',
341
+ unit: '°C',
342
+ },
343
+ ],
344
+ [
345
+ 'temperature',
346
+ numOrNull(r.temperature),
347
+ {
348
+ name: 'Temperature, hourly mean',
349
+ type: 'number',
350
+ role: 'value.temperature',
351
+ unit: '°C',
352
+ },
353
+ ],
354
+ [
355
+ 'humidity',
356
+ numOrNull(r.humidity),
357
+ {
358
+ name: 'Humidity, hourly mean',
359
+ type: 'number',
360
+ role: 'value.humidity',
361
+ unit: '%',
362
+ },
363
+ ],
364
+ [
365
+ 'pressure',
366
+ numOrNull(r.pressure),
367
+ {
368
+ name: 'Pressure, hourly mean',
369
+ type: 'number',
370
+ role: 'value.pressure',
371
+ unit: 'hPa',
372
+ },
373
+ ],
374
+ ];
375
+ for (const [key, value, common] of hourly) {
376
+ await this.put(`${id}.hourly.${key}`, value, common);
377
+ }
307
378
  }
308
- const r = row || {};
309
- const hourly = [
310
- [
311
- "time",
312
- r.time || null,
313
- { name: "Hour of these values", type: "string", role: "date" },
314
- ],
315
- [
316
- "et0",
317
- numOrNull(r.et0_hour),
318
- { name: "ET0 in the hour", type: "number", role: "value", unit: "mm" },
319
- ],
320
- [
321
- "leafWetnessHours",
322
- numOrNull(r.lwd_consecutive_hours),
323
- {
324
- name: "Consecutive wet-leaf hours",
325
- type: "number",
326
- role: "value",
327
- unit: "h",
328
- },
329
- ],
330
- [
331
- "sprayQuality",
332
- r.spray_quality_label || null,
333
- { name: "Spray quality", type: "string", role: "text" },
334
- ],
335
- [
336
- "sprayIndex",
337
- numOrNull(r.spray_quality_index),
338
- { name: "Spray quality index", type: "number", role: "value" },
339
- ],
340
- [
341
- "sprayLimit",
342
- r.spray_limiting_factor || null,
343
- { name: "What limits spraying", type: "string", role: "text" },
344
- ],
345
- [
346
- "vpd",
347
- numOrNull(r.vpd),
348
- {
349
- name: "Vapour pressure deficit",
350
- type: "number",
351
- role: "value",
352
- unit: "kPa",
353
- },
354
- ],
355
- [
356
- "deltaT",
357
- numOrNull(r.delta_t),
358
- {
359
- name: "Delta T",
360
- type: "number",
361
- role: "value.temperature",
362
- unit: "°C",
363
- },
364
- ],
365
- [
366
- "temperature",
367
- numOrNull(r.temperature),
368
- {
369
- name: "Temperature, hourly mean",
370
- type: "number",
371
- role: "value.temperature",
372
- unit: "°C",
373
- },
374
- ],
375
- [
376
- "humidity",
377
- numOrNull(r.humidity),
378
- {
379
- name: "Humidity, hourly mean",
380
- type: "number",
381
- role: "value.humidity",
382
- unit: "%",
383
- },
384
- ],
385
- [
386
- "pressure",
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);
379
+
380
+ /** A channel that groups the states below it. */
381
+ async channel(id, name) {
382
+ await this.setObjectNotExistsAsync(id, {
383
+ type: 'channel',
384
+ common: { name },
385
+ native: {},
386
+ });
398
387
  }
399
- }
400
388
 
401
- /** Create the state if needed, then write it acknowledged. */
402
- async put(id, value, common) {
403
- await this.setObjectNotExistsAsync(id, {
404
- type: "state",
405
- common: { read: true, write: false, ...common },
406
- native: {},
407
- });
408
- await this.setStateAsync(id, { val: value, ack: true });
409
- }
389
+ /**
390
+ * Create the state if needed, then write it acknowledged. With nothing to
391
+ * report the stored value goes away: null would break the declared type, and
392
+ * keeping the old value would show yesterday's answer as today's.
393
+ */
394
+ async put(id, value, common) {
395
+ await this.setObjectNotExistsAsync(id, {
396
+ type: 'state',
397
+ common: { read: true, write: false, ...common },
398
+ native: {},
399
+ });
400
+ if (value === null || value === undefined) {
401
+ await this.delStateAsync(id);
402
+ return;
403
+ }
404
+ await this.setStateAsync(id, { val: value, ack: true });
405
+ }
410
406
 
411
- // --- send ------------------------------------------------------------------
407
+ // --- send ------------------------------------------------------------------
412
408
 
413
- async readMapped(stateId) {
414
- if (!stateId) {
415
- return undefined;
416
- }
417
- try {
418
- const s = await this.getForeignStateAsync(stateId);
419
- if (!s || s.val === null || s.val === undefined || s.val === "") {
420
- return undefined;
421
- }
422
- const n = Number(s.val);
423
- return Number.isFinite(n) ? n : undefined;
424
- } catch (e) {
425
- this.log.debug(`cannot read ${stateId}: ${e.message}`);
426
- return undefined;
409
+ async readMapped(stateId) {
410
+ if (!stateId) {
411
+ return undefined;
412
+ }
413
+ try {
414
+ const s = await this.getForeignStateAsync(stateId);
415
+ if (!s || s.val === null || s.val === undefined || s.val === '') {
416
+ return undefined;
417
+ }
418
+ const n = Number(s.val);
419
+ return Number.isFinite(n) ? n : undefined;
420
+ } catch (e) {
421
+ this.log.debug(`cannot read ${stateId}: ${e.message}`);
422
+ return undefined;
423
+ }
427
424
  }
428
- }
429
425
 
430
- async forward() {
431
- const cfg = this.config;
432
- let wind = await this.readMapped(cfg.windSpeedState);
433
- let gust = await this.readMapped(cfg.windGustState);
434
- if (cfg.windUnit === "kmh") {
435
- if (wind !== undefined) {
436
- wind /= 3.6;
437
- }
438
- if (gust !== undefined) {
439
- gust /= 3.6;
440
- }
441
- }
442
- const reading = {
443
- temperature: await this.readMapped(cfg.temperatureState),
444
- humidity: await this.readMapped(cfg.humidityState),
445
- pressure: await this.readMapped(cfg.pressureState),
446
- wind_speed: wind,
447
- wind_gusts: gust,
448
- wind_direction: await this.readMapped(cfg.windDirectionState),
449
- rain_total_mm: await this.readMapped(cfg.rainTotalState),
450
- solar_wm2: await this.readMapped(cfg.solarState),
451
- light_lux: await this.readMapped(cfg.luxState),
452
- };
453
- const fields = toEcowitt(reading, {
454
- deviceId: cfg.forwardDeviceId || `iobroker-${this.namespace}`,
455
- stationType: "iobroker.nuasense_V0.2.0",
456
- });
457
- if (Object.keys(fields).length <= 3) {
458
- // PASSKEY, stationtype, dateutc and nothing else: no mapped state had a value.
459
- await this.setStateAsync("forward.lastStatus", {
460
- val: "no values to send (check the mapped states)",
461
- ack: true,
462
- });
463
- return;
464
- }
465
- try {
466
- const res = await fetch(this.forwardUrl, {
467
- method: "POST",
468
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
469
- body: encodeForm(fields),
470
- signal: AbortSignal.timeout(10_000),
471
- });
472
- const text = (await res.text()).slice(0, 80);
473
- await this.setStateAsync("forward.lastSentAt", {
474
- val: new Date().toISOString(),
475
- ack: true,
476
- });
477
- await this.setStateAsync("forward.lastStatus", {
478
- val: `${res.status} ${text}`.trim(),
479
- ack: true,
480
- });
481
- if (res.status === 404) {
482
- this.log.warn(
483
- "NuaSense does not know this station token (revoked, or mistyped)",
484
- );
485
- } else if (res.status === 409) {
486
- this.log.warn(
487
- "the station token is bound to another device; issue a new path in the console",
488
- );
489
- }
490
- } catch (e) {
491
- await this.setStateAsync("forward.lastStatus", {
492
- val: `failed: ${e.message}`,
493
- ack: true,
494
- });
495
- this.log.debug(`upload failed: ${e.message}`);
426
+ async forward() {
427
+ const cfg = this.config;
428
+ let wind = await this.readMapped(cfg.windSpeedState);
429
+ let gust = await this.readMapped(cfg.windGustState);
430
+ if (cfg.windUnit === 'kmh') {
431
+ if (wind !== undefined) {
432
+ wind /= 3.6;
433
+ }
434
+ if (gust !== undefined) {
435
+ gust /= 3.6;
436
+ }
437
+ }
438
+ const reading = {
439
+ temperature: await this.readMapped(cfg.temperatureState),
440
+ humidity: await this.readMapped(cfg.humidityState),
441
+ pressure: await this.readMapped(cfg.pressureState),
442
+ wind_speed: wind,
443
+ wind_gusts: gust,
444
+ wind_direction: await this.readMapped(cfg.windDirectionState),
445
+ rain_total_mm: await this.readMapped(cfg.rainTotalState),
446
+ solar_wm2: await this.readMapped(cfg.solarState),
447
+ light_lux: await this.readMapped(cfg.luxState),
448
+ };
449
+ const fields = toEcowitt(reading, {
450
+ deviceId: cfg.forwardDeviceId || `iobroker-${this.namespace}`,
451
+ stationType: `iobroker.nuasense_V${version}`,
452
+ });
453
+ if (Object.keys(fields).length <= 3) {
454
+ // PASSKEY, stationtype, dateutc and nothing else: no mapped state had a value.
455
+ await this.setStateAsync('forward.lastStatus', {
456
+ val: 'no values to send (check the mapped states)',
457
+ ack: true,
458
+ });
459
+ return;
460
+ }
461
+ try {
462
+ const res = await fetch(this.forwardUrl, {
463
+ method: 'POST',
464
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
465
+ body: encodeForm(fields),
466
+ signal: AbortSignal.timeout(10_000),
467
+ });
468
+ const text = (await res.text()).slice(0, 80);
469
+ await this.setStateAsync('forward.lastSentAt', {
470
+ val: new Date().toISOString(),
471
+ ack: true,
472
+ });
473
+ await this.setStateAsync('forward.lastStatus', {
474
+ val: `${res.status} ${text}`.trim(),
475
+ ack: true,
476
+ });
477
+ if (res.status === 404) {
478
+ this.log.warn('NuaSense does not know this station token (revoked, or mistyped)');
479
+ } else if (res.status === 409) {
480
+ this.log.warn('the station token is bound to another device; issue a new path in the console');
481
+ }
482
+ } catch (e) {
483
+ await this.setStateAsync('forward.lastStatus', {
484
+ val: `failed: ${e.message}`,
485
+ ack: true,
486
+ });
487
+ this.log.debug(`upload failed: ${e.message}`);
488
+ }
496
489
  }
497
- }
498
490
 
499
- onUnload(callback) {
500
- try {
501
- if (this.pollTimer) {
502
- this.clearInterval(this.pollTimer);
503
- }
504
- if (this.forwardTimer) {
505
- this.clearInterval(this.forwardTimer);
506
- }
507
- } finally {
508
- callback();
491
+ onUnload(callback) {
492
+ try {
493
+ if (this.pollTimer) {
494
+ this.clearInterval(this.pollTimer);
495
+ }
496
+ if (this.forwardTimer) {
497
+ this.clearInterval(this.forwardTimer);
498
+ }
499
+ } finally {
500
+ callback();
501
+ }
509
502
  }
510
- }
511
503
  }
512
504
 
513
505
  function sanitize(id) {
514
- return String(id).replace(/[^A-Za-z0-9_-]/g, "_");
506
+ return String(id).replace(/[^A-Za-z0-9_-]/g, '_');
515
507
  }
516
508
 
517
509
  function numOrNull(v) {
518
- const n = Number(v);
519
- return v === null || v === undefined || !Number.isFinite(n) ? null : n;
510
+ const n = Number(v);
511
+ return v === null || v === undefined || !Number.isFinite(n) ? null : n;
520
512
  }
521
513
 
522
514
  if (require.main !== module) {
523
- // Export the constructor in compact mode
524
- /** @param {Partial<utils.AdapterOptions>} [options] */
525
- module.exports = (options) => new Nuasense(options);
515
+ // Export the constructor in compact mode
516
+ /** @param {Partial<utils.AdapterOptions>} [options] */
517
+ module.exports = options => new Nuasense(options);
526
518
  } else {
527
- // otherwise start the instance directly
528
- new Nuasense();
519
+ // otherwise start the instance directly
520
+ new Nuasense();
529
521
  }