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/README.md CHANGED
@@ -9,11 +9,11 @@
9
9
 
10
10
  ## NuaSense adapter for ioBroker
11
11
 
12
- What your weather station means for your house and garden, as ioBroker states — and, if you like, your station forwarded from ioBroker to NuaSense. The first three stations on a NuaSense account connect free.
12
+ What your weather station means for your house and garden, as ioBroker states — and, if you like, your station forwarded from ioBroker to [NuaSense](https://nuasense.com). The first three stations on a NuaSense account connect free.
13
13
 
14
14
  ### Read
15
15
 
16
- Make an API key in the NuaSense console under **API Keys** (scope `ingest:write`; it is what reads your own stations), put it into the adapter, and tell it what the garden mostly is (lawn, vegetables, flower beds and shrubs) and your sprinkler's rate in mm per hour (pop-up sprinkler about 10, drip line 2–4; 0 for millimetres only). Every 15 minutes (configurable) the adapter writes, per station:
16
+ Make an API key in the [NuaSense console](https://platform.nuasense.com) under **API Keys** (scope `ingest:write`; it is what reads your own stations), put it into the adapter, and tell it what the garden mostly is (lawn, vegetables, flower beds and shrubs) and your sprinkler's rate in mm per hour (pop-up sprinkler about 10, drip line 2–4; 0 for millimetres only). Every 15 minutes (configurable) the adapter writes, per station:
17
17
 
18
18
  | state | meaning |
19
19
  |---|---|
@@ -46,6 +46,12 @@ 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.2 (2026-11-09)
50
+ * (NuaSense) object structure for the review: folders and channels for all states, valid roles, no null values
51
+
52
+ ### 0.2.1 (2026-11-05)
53
+ * (NuaSense) checker fixes: workflow and tooling
54
+
49
55
  ### 0.2.0 (2026-11-05)
50
56
  * (NuaSense) house & garden first: frost tonight, watering today in mm and minutes, rain next 24 h, plausibility; agronomy values behind Advanced
51
57
 
@@ -55,6 +61,8 @@ When you connect a station, NuaSense stores its readings and computes your forec
55
61
  ### 0.1.0 (2026-11-03)
56
62
  * (NuaSense) first release: first look and hourly agronomy as states; optional forwarding of a station from ioBroker to NuaSense
57
63
 
64
+ Older changes: [CHANGELOG_OLD.md](CHANGELOG_OLD.md)
65
+
58
66
  ## License
59
67
  MIT License
60
68
 
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "nuasense",
4
- "version": "0.2.0",
4
+ "version": "0.2.2",
5
5
  "news": {
6
+ "0.2.2": {
7
+ "en": "Object structure for the review: folders and channels for all states, valid roles, no null values",
8
+ "de": "Objektstruktur für das Review: Ordner und Kanäle für alle States, gültige Rollen, keine Nullwerte",
9
+ "ru": "Структура объектов для проверки: папки и каналы для всех состояний, допустимые роли, без значений null",
10
+ "pt": "Estrutura de objetos para a revisão: pastas e canais para todos os estados, funções válidas, sem valores nulos",
11
+ "nl": "Objectstructuur voor de review: mappen en kanalen voor alle states, geldige rollen, geen null-waarden",
12
+ "fr": "Structure des objets pour la revue : dossiers et canaux pour tous les états, rôles valides, aucune valeur nulle",
13
+ "it": "Struttura degli oggetti per la revisione: cartelle e canali per tutti gli stati, ruoli validi, nessun valore nullo",
14
+ "es": "Estructura de objetos para la revisión: carpetas y canales para todos los estados, roles válidos, sin valores nulos",
15
+ "pl": "Struktura obiektów do przeglądu: foldery i kanały dla wszystkich stanów, poprawne role, bez wartości null",
16
+ "uk": "Структура об'єктів для перевірки: папки та канали для всіх станів, допустимі ролі, без значень null",
17
+ "zh-cn": "用于审核的对象结构:所有状态都有文件夹和通道,角色有效,没有空值"
18
+ },
19
+ "0.2.1": {
20
+ "en": "Checker fixes: workflow and tooling",
21
+ "de": "Checker-Korrekturen: Workflow und Werkzeuge",
22
+ "ru": "Исправления по чекеру: рабочий процесс и инструменты",
23
+ "pt": "Correções do verificador: fluxo de trabalho e ferramentas",
24
+ "nl": "Checker-fixes: workflow en tooling",
25
+ "fr": "Corrections du vérificateur : flux de travail et outils",
26
+ "it": "Correzioni del checker: flusso di lavoro e strumenti",
27
+ "es": "Correcciones del verificador: flujo de trabajo y herramientas",
28
+ "pl": "Poprawki checkera: proces i narzędzia",
29
+ "uk": "Виправлення за чекером: робочий процес та інструменти",
30
+ "zh-cn": "检查器修复:工作流与工具"
31
+ },
6
32
  "0.2.0": {
7
33
  "en": "House & garden first: frost tonight, watering today in mm and minutes, rain next 24 h, plausibility; agronomy values behind Advanced",
8
34
  "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 +42,6 @@
16
42
  "uk": "Спочатку дім і сад: заморозки вночі, полив сьогодні в мм і хвилинах, дощ на 24 год, правдоподібність; агрономічні значення в розділі «Розширені»",
17
43
  "zh-cn": "优先家居与花园:今夜霜冻、今日浇水量(毫米与分钟)、未来 24 小时降雨、合理性;农艺数值放在“高级”中"
18
44
  },
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
45
  "0.1.0": {
33
46
  "en": "First release: first look and hourly agronomy as states; optional forwarding of a station from ioBroker to NuaSense",
34
47
  "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
- "use strict";
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("node:crypto");
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 = (v) => {
23
- if (v === null || v === undefined || v === "") {
24
- return null;
25
- }
26
- const n = Number(v);
27
- return Number.isFinite(n) ? n : null;
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
- 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", " ");
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
- return crypto
43
- .createHash("md5")
44
- .update(String(deviceId || "node-red"), "utf8")
45
- .digest("hex")
46
- .toUpperCase();
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
- reading,
58
- { deviceId, stationType = "node-red-contrib-nuasense" } = {},
59
- ) {
60
- const r = reading && typeof reading === "object" ? reading : {};
61
- const fields = {
62
- PASSKEY: passkeyFor(deviceId),
63
- stationtype: stationType,
64
- dateutc: dateUtc(r.time),
65
- };
66
- const t = num(r.temperature);
67
- if (t !== null) {
68
- fields.tempf = String(fix((t * 9) / 5 + 32, 2));
69
- }
70
- const h = num(r.humidity);
71
- if (h !== null) {
72
- fields.humidity = String(Math.round(h));
73
- }
74
- const p = num(r.pressure);
75
- if (p !== null) {
76
- fields.baromabsin = String(fix(p * INHG_PER_HPA, 3));
77
- }
78
- const ws = num(r.wind_speed);
79
- if (ws !== null) {
80
- fields.windspeedmph = String(fix(ws * MPH_PER_MS, 2));
81
- }
82
- const wg = num(r.wind_gusts);
83
- if (wg !== null) {
84
- fields.windgustmph = String(fix(wg * MPH_PER_MS, 2));
85
- }
86
- const wd = num(r.wind_direction);
87
- if (wd !== null) {
88
- fields.winddir = String(Math.round(((wd % 360) + 360) % 360));
89
- }
90
- const rain = num(r.rain_total_mm);
91
- if (rain !== null) {
92
- fields.yearlyrainin = String(fix(rain * IN_PER_MM, 3));
93
- }
94
- const sol = num(r.solar_wm2);
95
- const lux = num(r.light_lux);
96
- if (sol !== null) {
97
- fields.solarradiation = String(fix(sol, 2));
98
- } else if (lux !== null) {
99
- fields.solarradiation = String(fix(lux / LUX_PER_WM2, 2));
100
- }
101
- const uv = num(r.uv);
102
- if (uv !== null) {
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
- return Object.entries(fields)
110
- .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
111
- .join("&");
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
- const host = String(server || "ingest.nuasense.com")
117
- .trim()
118
- .replace(/^https?:\/\//, "")
119
- .replace(/\/+$/, "");
120
- const tok = String(token || "")
121
- .trim()
122
- .toLowerCase();
123
- if (!/^[a-z0-9-]{12,29}$/.test(tok)) {
124
- throw new Error(
125
- "the station path token looks wrong — copy it from the console",
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
- toEcowitt,
133
- encodeForm,
134
- uploadUrl,
135
- passkeyFor,
136
- dateUtc,
137
- LUX_PER_WM2,
127
+ toEcowitt,
128
+ encodeForm,
129
+ uploadUrl,
130
+ passkeyFor,
131
+ dateUtc,
132
+ LUX_PER_WM2,
138
133
  };