iobroker.absolutehumidity 0.0.2 → 0.0.3

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
@@ -19,6 +19,9 @@ build absolute humidity from actual temperature and relative humidity
19
19
  Placeholder for the next version (at the beginning of the line):
20
20
  ### **WORK IN PROGRESS**
21
21
  -->
22
+ ### 0.0.3 (2026-07-05)
23
+ * (BenAhrdt) Add new features and icons to deviceManager
24
+
22
25
  ### 0.0.2 (2026-07-05)
23
26
  * (BenAhrdt) initial release
24
27
 
@@ -11,6 +11,7 @@
11
11
  "Delete device": "Gerät löschen",
12
12
  "Delete this device?": "Dieses Gerät löschen?",
13
13
  "Device not found": "Gerät nicht gefunden",
14
+ "Invalid source state": "Ungültiger Quell-State",
14
15
  "Name": "Name",
15
16
  "Temperature": "Temperatur",
16
17
  "Temperature state": "Temperatur-State",
@@ -11,6 +11,7 @@
11
11
  "Delete device": "Delete device",
12
12
  "Delete this device?": "Delete this device?",
13
13
  "Device not found": "Device not found",
14
+ "Invalid source state": "Invalid source state",
14
15
  "Name": "Name",
15
16
  "Temperature": "Temperature",
16
17
  "Temperature state": "Temperature state",
@@ -11,6 +11,7 @@
11
11
  "Delete device": "Eliminar dispositivo",
12
12
  "Delete this device?": "¿Eliminar este dispositivo?",
13
13
  "Device not found": "Dispositivo no encontrado",
14
+ "Invalid source state": "Estado de origen no válido",
14
15
  "Name": "Nombre",
15
16
  "Temperature": "Temperatura",
16
17
  "Temperature state": "Estado de temperatura",
@@ -11,6 +11,7 @@
11
11
  "Delete device": "Supprimer l'appareil",
12
12
  "Delete this device?": "Supprimer cet appareil ?",
13
13
  "Device not found": "Appareil introuvable",
14
+ "Invalid source state": "État source invalide",
14
15
  "Name": "Nom",
15
16
  "Temperature": "Température",
16
17
  "Temperature state": "État de température",
@@ -11,6 +11,7 @@
11
11
  "Delete device": "Elimina dispositivo",
12
12
  "Delete this device?": "Eliminare questo dispositivo?",
13
13
  "Device not found": "Dispositivo non trovato",
14
+ "Invalid source state": "Stato sorgente non valido",
14
15
  "Name": "Nome",
15
16
  "Temperature": "Temperatura",
16
17
  "Temperature state": "Stato temperatura",
@@ -11,6 +11,7 @@
11
11
  "Delete device": "Apparaat verwijderen",
12
12
  "Delete this device?": "Dit apparaat verwijderen?",
13
13
  "Device not found": "Apparaat niet gevonden",
14
+ "Invalid source state": "Ongeldige bronstate",
14
15
  "Name": "Naam",
15
16
  "Temperature": "Temperatuur",
16
17
  "Temperature state": "Temperatuurstate",
@@ -11,6 +11,7 @@
11
11
  "Delete device": "Usuń urządzenie",
12
12
  "Delete this device?": "Usunąć to urządzenie?",
13
13
  "Device not found": "Nie znaleziono urządzenia",
14
+ "Invalid source state": "Nieprawidłowy stan źródłowy",
14
15
  "Name": "Nazwa",
15
16
  "Temperature": "Temperatura",
16
17
  "Temperature state": "Stan temperatury",
@@ -11,6 +11,7 @@
11
11
  "Delete device": "Excluir dispositivo",
12
12
  "Delete this device?": "Excluir este dispositivo?",
13
13
  "Device not found": "Dispositivo não encontrado",
14
+ "Invalid source state": "Estado de origem inválido",
14
15
  "Name": "Nome",
15
16
  "Temperature": "Temperatura",
16
17
  "Temperature state": "Estado de temperatura",
@@ -11,6 +11,7 @@
11
11
  "Delete device": "Удалить устройство",
12
12
  "Delete this device?": "Удалить это устройство?",
13
13
  "Device not found": "Устройство не найдено",
14
+ "Invalid source state": "Недопустимое исходное состояние",
14
15
  "Name": "Имя",
15
16
  "Temperature": "Температура",
16
17
  "Temperature state": "Состояние температуры",
@@ -11,6 +11,7 @@
11
11
  "Delete device": "Видалити пристрій",
12
12
  "Delete this device?": "Видалити цей пристрій?",
13
13
  "Device not found": "Пристрій не знайдено",
14
+ "Invalid source state": "Недійсний вихідний стан",
14
15
  "Name": "Ім'я",
15
16
  "Temperature": "Температура",
16
17
  "Temperature state": "Стан температури",
@@ -11,6 +11,7 @@
11
11
  "Delete device": "删除设备",
12
12
  "Delete this device?": "删除此设备?",
13
13
  "Device not found": "未找到设备",
14
+ "Invalid source state": "无效的源状态",
14
15
  "Name": "名称",
15
16
  "Temperature": "温度",
16
17
  "Temperature state": "温度状态",
@@ -38,7 +38,7 @@
38
38
  "_previewCard": {
39
39
  "type": "staticText",
40
40
  "format": "html",
41
- "text": "<div class='ah-preview-card' style='width:300px;background:#d3d3d3;border-radius:4px;overflow:hidden;box-shadow:0 2px 4px rgba(0,0,0,.25);margin-top:24px;color:#000;font-family:Roboto,Arial,sans-serif;'><div style='height:60px;background:#0f4c81;color:#fff;display:flex;align-items:center;padding:0 18px;gap:18px;font-weight:700;'><img src='adapter/absolutehumidity/absolutehumidity.svg' alt='' style='width:34px;height:34px;object-fit:contain;'><span>TestGeraet</span></div><div style='padding:18px 16px 20px;'><div style='color:#00a51a;font-size:20px;margin-bottom:18px;'>↔</div><div style='display:grid;grid-template-columns:1fr auto;gap:14px 16px;align-items:center;'><label for='ah-preview-temp'>Temperatur:</label><span><input id='ah-preview-temp' data-ah-temp type='number' value='21' step='0.1' style='width:72px;background:#fff;border:1px solid #ccc;border-radius:3px;padding:4px 6px;text-align:right;color:#000;' onchange='event.stopPropagation();event.stopImmediatePropagation();' oninput='event.stopPropagation();event.stopImmediatePropagation();const r=this.closest(\".ah-preview-card\"),t=Number(r.querySelector(\"[data-ah-temp]\").value),h=Number(r.querySelector(\"[data-ah-rh]\").value),abs=r.querySelector(\"[data-ah-abs]\"),dew=r.querySelector(\"[data-ah-dew]\");if(Number.isFinite(t)&&Number.isFinite(h)&&h>=0&&h<=100){const es=6.112*Math.exp((17.67*t)/(t+243.5)),e=h/100*es;abs.textContent=(Math.round(((216.7*e)/(273.15+t))*100)/100).toFixed(2);}else abs.textContent=\"\";if(Number.isFinite(t)&&Number.isFinite(h)&&h>0&&h<=100){const a=Math.log(h/100)+(17.62*t)/(243.12+t);dew.textContent=(Math.round(((243.12*a)/(17.62-a))*100)/100).toFixed(2);}else dew.textContent=\"\";'> <span style='color:#555;font-size:12px;'>°C</span></span><label for='ah-preview-rh'>Relative Luftfeuchte:</label><span><input id='ah-preview-rh' data-ah-rh type='number' value='50' min='0' max='100' step='1' style='width:72px;background:#fff;border:1px solid #ccc;border-radius:3px;padding:4px 6px;text-align:right;color:#000;' onchange='event.stopPropagation();event.stopImmediatePropagation();' oninput='event.stopPropagation();event.stopImmediatePropagation();const r=this.closest(\".ah-preview-card\"),t=Number(r.querySelector(\"[data-ah-temp]\").value),h=Number(r.querySelector(\"[data-ah-rh]\").value),abs=r.querySelector(\"[data-ah-abs]\"),dew=r.querySelector(\"[data-ah-dew]\");if(Number.isFinite(t)&&Number.isFinite(h)&&h>=0&&h<=100){const es=6.112*Math.exp((17.67*t)/(t+243.5)),e=h/100*es;abs.textContent=(Math.round(((216.7*e)/(273.15+t))*100)/100).toFixed(2);}else abs.textContent=\"\";if(Number.isFinite(t)&&Number.isFinite(h)&&h>0&&h<=100){const a=Math.log(h/100)+(17.62*t)/(243.12+t);dew.textContent=(Math.round(((243.12*a)/(17.62-a))*100)/100).toFixed(2);}else dew.textContent=\"\";'> <span style='color:#555;font-size:12px;'>%</span></span><span>Absolute Luftfeuchte:</span><span style='text-align:right;'><span data-ah-abs>9.16</span> <span style='color:#555;font-size:12px;'>g/m³</span></span><span>Taupunkttemperatur:</span><span style='text-align:right;'><span data-ah-dew>10.18</span> <span style='color:#555;font-size:12px;'>°C</span></span></div><div style='height:34px;'></div></div></div>",
41
+ "text": "<div class='ah-preview-card' style='width:300px;background:#d3d3d3;border-radius:4px;overflow:hidden;box-shadow:0 2px 4px rgba(0,0,0,.25);margin-top:24px;color:#000;font-family:Roboto,Arial,sans-serif;'><div style='height:60px;background:#0f4c81;color:#fff;display:flex;align-items:center;padding:0 18px;gap:18px;font-weight:700;'><img src='adapter/absolutehumidity/absolutehumidity.svg' alt='' style='width:34px;height:34px;object-fit:contain;' onload='(async img=>{const c=img.closest(\".ah-preview-card\");if(!c||c.dataset.ahI18nDone)return;c.dataset.ahI18nDone=\"1\";const raw=String(window.systemLang||window.adminLanguage||navigator.language||\"en\").toLowerCase();const lang=raw.startsWith(\"zh\")?\"zh-cn\":raw.slice(0,2);const load=async l=>{const r=await fetch(\"adapter/absolutehumidity/i18n/\"+l+\".json\");return r.ok?r.json():null;};try{const d=(await load(lang))||(await load(\"en\"))||{};c.querySelectorAll(\"[data-ah-i18n]\").forEach(e=>{const k=e.dataset.ahI18n;e.textContent=d[k]||k;});}catch(e){}})(this)'><span data-ah-i18n='Manual calculation'>Manual calculation</span></div><div style='padding:18px 16px 20px;'><div style='color:#00a51a;font-size:20px;margin-bottom:18px;'>↔</div><div style='display:grid;grid-template-columns:1fr auto;gap:14px 16px;align-items:center;'><label for='ah-preview-temp'><span data-ah-i18n='Temperature'>Temperature</span>:</label><span><input id='ah-preview-temp' data-ah-temp type='number' value='21' step='0.1' style='width:72px;background:#fff;border:1px solid #ccc;border-radius:3px;padding:4px 6px;text-align:right;color:#000;' onchange='event.stopPropagation();event.stopImmediatePropagation();' oninput='event.stopPropagation();event.stopImmediatePropagation();const r=this.closest(\".ah-preview-card\"),t=Number(r.querySelector(\"[data-ah-temp]\").value),h=Number(r.querySelector(\"[data-ah-rh]\").value),abs=r.querySelector(\"[data-ah-abs]\"),dew=r.querySelector(\"[data-ah-dew]\");if(Number.isFinite(t)&&Number.isFinite(h)&&h>=0&&h<=100){const es=6.112*Math.exp((17.67*t)/(t+243.5)),e=h/100*es;abs.textContent=(Math.round(((216.7*e)/(273.15+t))*100)/100).toFixed(2);}else abs.textContent=\"\";if(Number.isFinite(t)&&Number.isFinite(h)&&h>0&&h<=100){const a=Math.log(h/100)+(17.62*t)/(243.12+t);dew.textContent=(Math.round(((243.12*a)/(17.62-a))*100)/100).toFixed(2);}else dew.textContent=\"\";'> <span style='color:#555;font-size:12px;'>°C</span></span><label for='ah-preview-rh'><span data-ah-i18n='Relative humidity'>Relative humidity</span>:</label><span><input id='ah-preview-rh' data-ah-rh type='number' value='50' min='0' max='100' step='1' style='width:72px;background:#fff;border:1px solid #ccc;border-radius:3px;padding:4px 6px;text-align:right;color:#000;' onchange='event.stopPropagation();event.stopImmediatePropagation();' oninput='event.stopPropagation();event.stopImmediatePropagation();const r=this.closest(\".ah-preview-card\"),t=Number(r.querySelector(\"[data-ah-temp]\").value),h=Number(r.querySelector(\"[data-ah-rh]\").value),abs=r.querySelector(\"[data-ah-abs]\"),dew=r.querySelector(\"[data-ah-dew]\");if(Number.isFinite(t)&&Number.isFinite(h)&&h>=0&&h<=100){const es=6.112*Math.exp((17.67*t)/(t+243.5)),e=h/100*es;abs.textContent=(Math.round(((216.7*e)/(273.15+t))*100)/100).toFixed(2);}else abs.textContent=\"\";if(Number.isFinite(t)&&Number.isFinite(h)&&h>0&&h<=100){const a=Math.log(h/100)+(17.62*t)/(243.12+t);dew.textContent=(Math.round(((243.12*a)/(17.62-a))*100)/100).toFixed(2);}else dew.textContent=\"\";'> <span style='color:#555;font-size:12px;'>%</span></span><span><span data-ah-i18n='Absolute humidity'>Absolute humidity</span>:</span><span style='text-align:right;'><span data-ah-abs>9.16</span> <span style='color:#555;font-size:12px;'>g/m³</span></span><span><span data-ah-i18n='Dew point temperature'>Dew point temperature</span>:</span><span style='text-align:right;'><span data-ah-dew>10.18</span> <span style='color:#555;font-size:12px;'>°C</span></span></div><div style='height:34px;'></div></div></div>",
42
42
  "newLine": true,
43
43
  "xs": 12,
44
44
  "sm": 12,
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "absolutehumidity",
4
- "version": "0.0.2",
4
+ "version": "0.0.3",
5
5
  "news": {
6
+ "0.0.3": {
7
+ "en": "Add new features and icons to deviceManager",
8
+ "de": "Füge dem Gerätemanager neue Funktionen und Symbole hinzu.",
9
+ "ru": "Добавьте новые функции и значки в deviceManager.",
10
+ "pt": "Adicionar novos recursos e ícones ao gerenciador de dispositivos",
11
+ "nl": "Voeg nieuwe functies en pictogrammen toe aan DeviceManager.",
12
+ "fr": "Ajouter de nouvelles fonctionnalités et icônes à deviceManager",
13
+ "it": "Aggiungi nuove funzionalità e icone a DeviceManager",
14
+ "es": "Agregar nuevas funciones e íconos al Administrador de dispositivos",
15
+ "pl": "Dodaj nowe funkcje i ikony do DeviceManager",
16
+ "uk": "Додавання нових функцій та піктограм до DeviceManager",
17
+ "zh-cn": "向设备管理器添加新功能和图标"
18
+ },
6
19
  "0.0.2": {
7
20
  "en": "initial release",
8
21
  "de": "Erstveröffentlichung",
@@ -12,6 +12,34 @@ const {
12
12
  } = require('./constants');
13
13
  const { translate } = require('./i18n');
14
14
 
15
+ const STATE_ICONS = {
16
+ temperature: createSvgIcon(`
17
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
18
+ <path fill="none" stroke="#1976d2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M14 14.76V5a4 4 0 0 0-8 0v9.76a6 6 0 1 0 8 0Z"/>
19
+ <path fill="#1976d2" d="M10 17.5a2.5 2.5 0 1 1-2-2.45V8a2 2 0 0 1 4 0v7.05a2.5 2.5 0 0 1-2 2.45Z"/>
20
+ </svg>
21
+ `),
22
+ relativeHumidity: createSvgIcon(`
23
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
24
+ <path fill="#03a9f4" d="M12 2.7 6.6 9.3A8.2 8.2 0 0 0 5 14a7 7 0 0 0 14 0 8.2 8.2 0 0 0-1.6-4.7L12 2.7Z"/>
25
+ <text x="12" y="16" text-anchor="middle" font-family="Arial, sans-serif" font-size="7" font-weight="700" fill="#fff">%</text>
26
+ </svg>
27
+ `),
28
+ absoluteHumidity: createSvgIcon(`
29
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
30
+ <path fill="#03a9f4" d="M12 2.7 6.6 9.3A8.2 8.2 0 0 0 5 14a7 7 0 0 0 14 0 8.2 8.2 0 0 0-1.6-4.7L12 2.7Z"/>
31
+ <path fill="rgba(255,255,255,.7)" d="M8.4 14.1c.3 2.1 1.6 3.3 3.4 3.7-2.8.3-5-1.2-5.4-3.6-.2-1.2.2-2.4 1.1-3.5-.1 1.2.2 2.4.9 3.4Z"/>
32
+ </svg>
33
+ `),
34
+ dewPointTemperature: createSvgIcon(`
35
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
36
+ <path fill="none" stroke="#1976d2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M10 13.9V5a3 3 0 0 0-6 0v8.9a5 5 0 1 0 6 0Z"/>
37
+ <path fill="#1976d2" d="M7 17.6a2.1 2.1 0 0 1-1-4V8a1 1 0 0 1 2 0v5.6a2.1 2.1 0 0 1-1 4Z"/>
38
+ <path fill="#03a9f4" d="M17 6.4 13.7 10a5 5 0 0 0-1.2 3.2 4.5 4.5 0 0 0 9 0 5 5 0 0 0-1.2-3.2L17 6.4Z"/>
39
+ </svg>
40
+ `),
41
+ };
42
+
15
43
  class AbsoluteHumidityDeviceManagement extends DeviceManagement {
16
44
  /**
17
45
  * @param {import('../../main')} adapter
@@ -67,13 +95,15 @@ class AbsoluteHumidityDeviceManagement extends DeviceManagement {
67
95
  /**
68
96
  * @param {import('@iobroker/dm-utils').DeviceLoadContext<string>} context
69
97
  */
70
- loadDevices(context) {
98
+ async loadDevices(context) {
71
99
  const devices = this.adapter.getConfiguredDevices();
72
100
 
73
101
  context.setTotalDevices(devices.length);
74
102
 
75
103
  for (const device of devices) {
76
- context.addDevice(createDeviceInfo(this.adapter, device));
104
+ const sourceValidation = await this.adapter.getDeviceSourceValidation(device);
105
+
106
+ context.addDevice(createDeviceInfo(this.adapter, device, sourceValidation));
77
107
  }
78
108
  }
79
109
  }
@@ -81,14 +111,20 @@ class AbsoluteHumidityDeviceManagement extends DeviceManagement {
81
111
  /**
82
112
  * @param {import('../../main')} adapter
83
113
  * @param {Record<string, any>} device
114
+ * @param {{ valid: boolean; temperatureStateValid: boolean; relativeHumidityStateValid: boolean }} sourceValidation
84
115
  */
85
- function createDeviceInfo(adapter, device) {
116
+ function createDeviceInfo(adapter, device, sourceValidation) {
86
117
  return {
87
118
  id: device.id,
88
119
  name: device.name,
89
120
  icon: ADAPTER_ICON,
90
121
  backgroundColor: CARD_HEADER_BACKGROUND,
91
- status: 'connected',
122
+ status: sourceValidation.valid
123
+ ? 'connected'
124
+ : {
125
+ connection: 'disconnected',
126
+ warning: translate('Invalid source state'),
127
+ },
92
128
  actions: [
93
129
  {
94
130
  id: 'editDevice',
@@ -124,15 +160,16 @@ function createDeviceInfo(adapter, device) {
124
160
  },
125
161
  },
126
162
  ],
127
- customInfo: createDeviceCustomInfo(adapter, device),
163
+ customInfo: createDeviceCustomInfo(adapter, device, sourceValidation),
128
164
  };
129
165
  }
130
166
 
131
167
  /**
132
168
  * @param {import('../../main')} adapter
133
169
  * @param {Record<string, any>} device
170
+ * @param {{ valid: boolean; temperatureStateValid: boolean; relativeHumidityStateValid: boolean }} sourceValidation
134
171
  */
135
- function createDeviceCustomInfo(adapter, device) {
172
+ function createDeviceCustomInfo(adapter, device, sourceValidation) {
136
173
  return {
137
174
  id: device.id,
138
175
  schema: {
@@ -144,31 +181,35 @@ function createDeviceCustomInfo(adapter, device) {
144
181
  items: {
145
182
  temperature: createStateInfoItem(
146
183
  translate('Temperature'),
147
- device.createTemperatureState
184
+ device.createTemperatureState || !sourceValidation.temperatureStateValid
148
185
  ? `${adapter.getDeviceObjectId(device.id)}.${STATE_TEMPERATURE}`
149
186
  : device.temperatureStateId,
150
187
  '°C',
151
- !device.createTemperatureState,
188
+ !device.createTemperatureState && sourceValidation.temperatureStateValid,
189
+ STATE_ICONS.temperature,
152
190
  ),
153
191
  relativeHumidity: createStateInfoItem(
154
192
  translate('Relative humidity'),
155
- device.createRelativeHumidityState
193
+ device.createRelativeHumidityState || !sourceValidation.relativeHumidityStateValid
156
194
  ? `${adapter.getDeviceObjectId(device.id)}.${STATE_RELATIVE_HUMIDITY}`
157
195
  : device.relativeHumidityStateId,
158
196
  '%',
159
- !device.createRelativeHumidityState,
197
+ !device.createRelativeHumidityState && sourceValidation.relativeHumidityStateValid,
198
+ STATE_ICONS.relativeHumidity,
160
199
  ),
161
200
  absoluteHumidity: createStateInfoItem(
162
201
  translate('Absolute humidity'),
163
202
  `${adapter.getDeviceObjectId(device.id)}.${STATE_ABSOLUTE_HUMIDITY}`,
164
203
  'g/m³',
165
204
  false,
205
+ STATE_ICONS.absoluteHumidity,
166
206
  ),
167
207
  dewPointTemperature: createStateInfoItem(
168
208
  translate('Dew point temperature'),
169
209
  `${adapter.getDeviceObjectId(device.id)}.${STATE_DEW_POINT_TEMPERATURE}`,
170
210
  '°C',
171
211
  false,
212
+ STATE_ICONS.dewPointTemperature,
172
213
  ),
173
214
  spacer: {
174
215
  type: 'staticText',
@@ -200,11 +241,13 @@ function createDeviceCustomInfo(adapter, device) {
200
241
  * @param {string} oid
201
242
  * @param {string} unit
202
243
  * @param {boolean} foreign
244
+ * @param {string} labelIcon
203
245
  */
204
- function createStateInfoItem(label, oid, unit, foreign) {
246
+ function createStateInfoItem(label, oid, unit, foreign, labelIcon) {
205
247
  return {
206
248
  type: 'state',
207
249
  label,
250
+ labelIcon,
208
251
  oid,
209
252
  foreign,
210
253
  control: 'text',
@@ -221,6 +264,13 @@ function createStateInfoItem(label, oid, unit, foreign) {
221
264
  };
222
265
  }
223
266
 
267
+ /**
268
+ * @param {string} svg
269
+ */
270
+ function createSvgIcon(svg) {
271
+ return `data:image/svg+xml;base64,${Buffer.from(svg.replace(/\s+/g, ' ').trim()).toString('base64')}`;
272
+ }
273
+
224
274
  /**
225
275
  * @param {import('@iobroker/dm-utils').ActionContext} context
226
276
  * @param {Record<string, any> | undefined} device
@@ -1,203 +1,36 @@
1
1
  /* eslint-disable jsdoc/require-jsdoc */
2
2
  'use strict';
3
3
 
4
- const I18N = {
5
- 'Add device': {
6
- en: 'Add device',
7
- de: 'Gerät hinzufügen',
8
- ru: 'Добавить устройство',
9
- pt: 'Adicionar dispositivo',
10
- nl: 'Apparaat toevoegen',
11
- fr: 'Ajouter un appareil',
12
- it: 'Aggiungi dispositivo',
13
- es: 'Añadir dispositivo',
14
- pl: 'Dodaj urządzenie',
15
- uk: 'Додати пристрій',
16
- 'zh-cn': '添加设备',
17
- },
18
- 'Add a temperature and humidity device': {
19
- en: 'Add a temperature and humidity device',
20
- de: 'Temperatur- und Luftfeuchte-Gerät hinzufügen',
21
- ru: 'Добавить устройство температуры и влажности',
22
- pt: 'Adicionar dispositivo de temperatura e umidade',
23
- nl: 'Temperatuur- en vochtigheidsapparaat toevoegen',
24
- fr: 'Ajouter un appareil de température et humidité',
25
- it: 'Aggiungi dispositivo di temperatura e umidità',
26
- es: 'Añadir dispositivo de temperatura y humedad',
27
- pl: 'Dodaj urządzenie temperatury i wilgotności',
28
- uk: 'Додати пристрій температури та вологості',
29
- 'zh-cn': '添加温湿度设备',
30
- },
31
- 'Edit device': {
32
- en: 'Edit device',
33
- de: 'Gerät bearbeiten',
34
- ru: 'Редактировать устройство',
35
- pt: 'Editar dispositivo',
36
- nl: 'Apparaat bewerken',
37
- fr: "Modifier l'appareil",
38
- it: 'Modifica dispositivo',
39
- es: 'Editar dispositivo',
40
- pl: 'Edytuj urządzenie',
41
- uk: 'Редагувати пристрій',
42
- 'zh-cn': '编辑设备',
43
- },
44
- 'Delete device': {
45
- en: 'Delete device',
46
- de: 'Gerät löschen',
47
- ru: 'Удалить устройство',
48
- pt: 'Excluir dispositivo',
49
- nl: 'Apparaat verwijderen',
50
- fr: "Supprimer l'appareil",
51
- it: 'Elimina dispositivo',
52
- es: 'Eliminar dispositivo',
53
- pl: 'Usuń urządzenie',
54
- uk: 'Видалити пристрій',
55
- 'zh-cn': '删除设备',
56
- },
57
- 'Delete this device?': {
58
- en: 'Delete this device?',
59
- de: 'Dieses Gerät löschen?',
60
- ru: 'Удалить это устройство?',
61
- pt: 'Excluir este dispositivo?',
62
- nl: 'Dit apparaat verwijderen?',
63
- fr: 'Supprimer cet appareil ?',
64
- it: 'Eliminare questo dispositivo?',
65
- es: '¿Eliminar este dispositivo?',
66
- pl: 'Usunąć to urządzenie?',
67
- uk: 'Видалити цей пристрій?',
68
- 'zh-cn': '删除此设备?',
69
- },
70
- 'Device not found': {
71
- en: 'Device not found',
72
- de: 'Gerät nicht gefunden',
73
- ru: 'Устройство не найдено',
74
- pt: 'Dispositivo não encontrado',
75
- nl: 'Apparaat niet gevonden',
76
- fr: 'Appareil introuvable',
77
- it: 'Dispositivo non trovato',
78
- es: 'Dispositivo no encontrado',
79
- pl: 'Nie znaleziono urządzenia',
80
- uk: 'Пристрій не знайдено',
81
- 'zh-cn': '未找到设备',
82
- },
83
- Name: {
84
- en: 'Name',
85
- de: 'Name',
86
- ru: 'Имя',
87
- pt: 'Nome',
88
- nl: 'Naam',
89
- fr: 'Nom',
90
- it: 'Nome',
91
- es: 'Nombre',
92
- pl: 'Nazwa',
93
- uk: "Ім'я",
94
- 'zh-cn': '名称',
95
- },
96
- Temperature: {
97
- en: 'Temperature',
98
- de: 'Temperatur',
99
- ru: 'Температура',
100
- pt: 'Temperatura',
101
- nl: 'Temperatuur',
102
- fr: 'Température',
103
- it: 'Temperatura',
104
- es: 'Temperatura',
105
- pl: 'Temperatura',
106
- uk: 'Температура',
107
- 'zh-cn': '温度',
108
- },
109
- 'Temperature state': {
110
- en: 'Temperature state',
111
- de: 'Temperatur-State',
112
- ru: 'Состояние температуры',
113
- pt: 'Estado de temperatura',
114
- nl: 'Temperatuurstate',
115
- fr: 'État de température',
116
- it: 'Stato temperatura',
117
- es: 'Estado de temperatura',
118
- pl: 'Stan temperatury',
119
- uk: 'Стан температури',
120
- 'zh-cn': '温度状态',
121
- },
122
- 'Create temperature state in adapter': {
123
- en: 'Create temperature state in adapter',
124
- de: 'Temperatur-State im Adapter anlegen',
125
- ru: 'Создать состояние температуры в адаптере',
126
- pt: 'Criar estado de temperatura no adaptador',
127
- nl: 'Temperatuurstate in adapter aanmaken',
128
- fr: "Créer l'état de température dans l'adaptateur",
129
- it: "Crea stato temperatura nell'adattatore",
130
- es: 'Crear estado de temperatura en el adaptador',
131
- pl: 'Utwórz stan temperatury w adapterze',
132
- uk: 'Створити стан температури в адаптері',
133
- 'zh-cn': '在适配器中创建温度状态',
134
- },
135
- 'Relative humidity': {
136
- en: 'Relative humidity',
137
- de: 'Relative Luftfeuchte',
138
- ru: 'Относительная влажность',
139
- pt: 'Umidade relativa',
140
- nl: 'Relatieve luchtvochtigheid',
141
- fr: 'Humidité relative',
142
- it: 'Umidità relativa',
143
- es: 'Humedad relativa',
144
- pl: 'Wilgotność względna',
145
- uk: 'Відносна вологість',
146
- 'zh-cn': '相对湿度',
147
- },
148
- 'Relative humidity state': {
149
- en: 'Relative humidity state',
150
- de: 'State für relative Luftfeuchte',
151
- ru: 'Состояние относительной влажности',
152
- pt: 'Estado de umidade relativa',
153
- nl: 'Relatieve-luchtvochtigheidstate',
154
- fr: "État d'humidité relative",
155
- it: 'Stato umidità relativa',
156
- es: 'Estado de humedad relativa',
157
- pl: 'Stan wilgotności względnej',
158
- uk: 'Стан відносної вологості',
159
- 'zh-cn': '相对湿度状态',
160
- },
161
- 'Create relative humidity state in adapter': {
162
- en: 'Create relative humidity state in adapter',
163
- de: 'State für relative Luftfeuchte im Adapter anlegen',
164
- ru: 'Создать состояние относительной влажности в адаптере',
165
- pt: 'Criar estado de umidade relativa no adaptador',
166
- nl: 'Relatieve-luchtvochtigheidstate in adapter aanmaken',
167
- fr: "Créer l'état d'humidité relative dans l'adaptateur",
168
- it: "Crea stato umidità relativa nell'adattatore",
169
- es: 'Crear estado de humedad relativa en el adaptador',
170
- pl: 'Utwórz stan wilgotności względnej w adapterze',
171
- uk: 'Створити стан відносної вологості в адаптері',
172
- 'zh-cn': '在适配器中创建相对湿度状态',
173
- },
174
- 'Absolute humidity': {
175
- en: 'Absolute humidity',
176
- de: 'Absolute Luftfeuchte',
177
- ru: 'Абсолютная влажность',
178
- pt: 'Umidade absoluta',
179
- nl: 'Absolute luchtvochtigheid',
180
- fr: 'Humidité absolue',
181
- it: 'Umidità assoluta',
182
- es: 'Humedad absoluta',
183
- pl: 'Wilgotność bezwzględna',
184
- uk: 'Абсолютна вологість',
185
- 'zh-cn': '绝对湿度',
186
- },
187
- 'Dew point temperature': {
188
- en: 'Dew point temperature',
189
- de: 'Taupunkttemperatur',
190
- ru: 'Температура точки росы',
191
- pt: 'Temperatura do ponto de orvalho',
192
- nl: 'Dauwpunttemperatuur',
193
- fr: 'Température du point de rosée',
194
- it: 'Temperatura del punto di rugiada',
195
- es: 'Temperatura del punto de rocío',
196
- pl: 'Temperatura punktu rosy',
197
- uk: 'Температура точки роси',
198
- 'zh-cn': '露点温度',
199
- },
200
- };
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const ADMIN_I18N_DIR = path.join(__dirname, '..', '..', 'admin', 'i18n');
8
+ const LANGUAGES = ['en', 'de', 'ru', 'pt', 'nl', 'fr', 'it', 'es', 'pl', 'uk', 'zh-cn'];
9
+ const I18N = loadAdminTranslations();
10
+
11
+ function loadAdminTranslations() {
12
+ const translations = {};
13
+
14
+ for (const language of LANGUAGES) {
15
+ const filePath = path.join(ADMIN_I18N_DIR, `${language}.json`);
16
+ const languageTranslations = readLanguageTranslations(filePath);
17
+
18
+ for (const [key, value] of Object.entries(languageTranslations)) {
19
+ translations[key] = translations[key] || {};
20
+ translations[key][language] = value;
21
+ }
22
+ }
23
+
24
+ return translations;
25
+ }
26
+
27
+ function readLanguageTranslations(filePath) {
28
+ try {
29
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
30
+ } catch {
31
+ return {};
32
+ }
33
+ }
201
34
 
202
35
  function translate(text) {
203
36
  return I18N[text] || text;
package/main.js CHANGED
@@ -69,11 +69,13 @@ class Absolutehumidity extends utils.Adapter {
69
69
  async addDevice(data) {
70
70
  const devices = this.getConfiguredDevices();
71
71
  const device = this.normalizeDeviceFormData(data, createUniqueDeviceId(data.name, devices));
72
+ const updatedDevices = [...devices, device];
72
73
 
73
- await this.saveDevices([...devices, device]);
74
+ await this.validateDeviceSourceIds(device);
74
75
  await this.ensureDeviceObjects(device);
75
- await this.refreshSubscriptions();
76
76
  await this.updateDeviceValues(device);
77
+ await this.refreshSubscriptions(updatedDevices);
78
+ await this.saveDevices(updatedDevices);
77
79
  }
78
80
 
79
81
  /**
@@ -84,16 +86,18 @@ class Absolutehumidity extends utils.Adapter {
84
86
  const oldDevice = this.getDeviceById(id);
85
87
  const devices = this.getConfiguredDevices();
86
88
  const device = this.normalizeDeviceFormData(data, id);
89
+ const updatedDevices = devices.map(existingDevice => (existingDevice.id === id ? device : existingDevice));
87
90
 
88
- await this.saveDevices(devices.map(existingDevice => (existingDevice.id === id ? device : existingDevice)));
91
+ await this.validateDeviceSourceIds(device);
89
92
 
90
93
  if (oldDevice) {
91
94
  await this.deleteObsoleteDeviceStates(oldDevice, device);
92
95
  }
93
96
 
94
97
  await this.ensureDeviceObjects(device);
95
- await this.refreshSubscriptions();
96
98
  await this.updateDeviceValues(device);
99
+ await this.refreshSubscriptions(updatedDevices);
100
+ await this.saveDevices(updatedDevices);
97
101
  }
98
102
 
99
103
  /**
@@ -101,10 +105,11 @@ class Absolutehumidity extends utils.Adapter {
101
105
  */
102
106
  async deleteDevice(id) {
103
107
  const devices = this.getConfiguredDevices();
108
+ const updatedDevices = devices.filter(device => device.id !== id);
104
109
 
105
- await this.saveDevices(devices.filter(device => device.id !== id));
106
110
  await this.deleteObjectIfExists(this.getDeviceObjectId(id), { recursive: true });
107
- await this.refreshSubscriptions();
111
+ await this.refreshSubscriptions(updatedDevices);
112
+ await this.saveDevices(updatedDevices);
108
113
  }
109
114
 
110
115
  /**
@@ -122,6 +127,79 @@ class Absolutehumidity extends utils.Adapter {
122
127
  };
123
128
  }
124
129
 
130
+ /**
131
+ * @param {Record<string, any>} device
132
+ */
133
+ async validateDeviceSourceIds(device) {
134
+ await this.validateForeignStateId(device.temperatureStateId, 'Temperature state');
135
+ await this.validateForeignStateId(device.relativeHumidityStateId, 'Relative humidity state');
136
+ }
137
+
138
+ /**
139
+ * @param {Record<string, any>} device
140
+ * @returns {Promise<boolean>}
141
+ */
142
+ async hasValidDeviceSourceIds(device) {
143
+ const sourceValidation = await this.getDeviceSourceValidation(device);
144
+
145
+ return sourceValidation.valid;
146
+ }
147
+
148
+ /**
149
+ * @param {unknown} stateId
150
+ * @param {string} label
151
+ */
152
+ async validateForeignStateId(stateId, label) {
153
+ if (await this.isUsableSourceStateId(stateId)) {
154
+ return;
155
+ }
156
+
157
+ throw new Error(`${label} "${stateId}" is not a valid number state ID`);
158
+ }
159
+
160
+ /**
161
+ * @param {Record<string, any>} device
162
+ */
163
+ async getDeviceSourceValidation(device) {
164
+ const temperatureStateValid = await this.isUsableSourceStateId(device.temperatureStateId);
165
+ const relativeHumidityStateValid = await this.isUsableSourceStateId(device.relativeHumidityStateId);
166
+
167
+ return {
168
+ valid: temperatureStateValid && relativeHumidityStateValid,
169
+ temperatureStateValid,
170
+ relativeHumidityStateValid,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * @param {unknown} stateId
176
+ * @returns {stateId is string}
177
+ */
178
+ isValidForeignStateId(stateId) {
179
+ return typeof stateId === 'string' && stateId.trim() !== '' && !stateId.endsWith('.');
180
+ }
181
+
182
+ /**
183
+ * @param {unknown} stateId
184
+ * @returns {Promise<boolean>}
185
+ */
186
+ async isUsableSourceStateId(stateId) {
187
+ if (!this.isValidForeignStateId(stateId)) {
188
+ return false;
189
+ }
190
+
191
+ let object;
192
+
193
+ try {
194
+ object = await this.getForeignObjectAsync(stateId);
195
+ } catch (error) {
196
+ this.log.warn(`Cannot validate state ID "${stateId}": ${error.message}`);
197
+ return false;
198
+ }
199
+
200
+ return object?.type === 'state' && object.common?.type === 'number';
201
+ }
202
+
125
203
  /**
126
204
  * @param {Record<string, any>[]} devices
127
205
  */
@@ -182,11 +260,15 @@ class Absolutehumidity extends utils.Adapter {
182
260
  return;
183
261
  }
184
262
 
185
- await this.saveDevices(migratedDevices);
263
+ for (const device of migratedDevices) {
264
+ await this.ensureDeviceObjects(device);
265
+ }
186
266
 
187
267
  for (const oldId of oldIdsToDelete) {
188
268
  await this.deleteObjectIfExists(this.getDeviceObjectId(oldId), { recursive: true });
189
269
  }
270
+
271
+ await this.saveDevices(migratedDevices);
190
272
  }
191
273
 
192
274
  async ensureInfoStates() {
@@ -314,20 +396,42 @@ class Absolutehumidity extends utils.Adapter {
314
396
  }
315
397
  }
316
398
 
317
- async refreshSubscriptions() {
399
+ /**
400
+ * @param {Array<Record<string, any>>} [devices]
401
+ */
402
+ async refreshSubscriptions(devices = this.getConfiguredDevices()) {
318
403
  for (const stateId of this.subscribedSourceIds) {
319
404
  this.unsubscribeForeignStates(stateId);
320
405
  }
321
406
 
322
407
  this.subscribedSourceIds.clear();
323
408
 
324
- for (const device of this.getConfiguredDevices()) {
325
- this.subscribedSourceIds.add(device.temperatureStateId);
326
- this.subscribedSourceIds.add(device.relativeHumidityStateId);
409
+ for (const device of devices) {
410
+ const sourceValidation = await this.getDeviceSourceValidation(device);
411
+
412
+ if (sourceValidation.temperatureStateValid) {
413
+ this.subscribedSourceIds.add(device.temperatureStateId);
414
+ } else {
415
+ this.log.warn(
416
+ `Ignoring unusable temperature state ID for device "${device.name}": ${device.temperatureStateId}`,
417
+ );
418
+ }
419
+
420
+ if (sourceValidation.relativeHumidityStateValid) {
421
+ this.subscribedSourceIds.add(device.relativeHumidityStateId);
422
+ } else {
423
+ this.log.warn(
424
+ `Ignoring unusable relative humidity state ID for device "${device.name}": ${device.relativeHumidityStateId}`,
425
+ );
426
+ }
327
427
  }
328
428
 
329
429
  for (const stateId of this.subscribedSourceIds) {
330
- this.subscribeForeignStates(stateId);
430
+ try {
431
+ this.subscribeForeignStates(stateId);
432
+ } catch (error) {
433
+ this.log.warn(`Cannot subscribe state ID "${stateId}": ${error.message}`);
434
+ }
331
435
  }
332
436
  }
333
437
 
@@ -390,6 +494,11 @@ class Absolutehumidity extends utils.Adapter {
390
494
  * @param {Record<string, any>} device
391
495
  */
392
496
  async updateDeviceValues(device) {
497
+ if (!(await this.hasValidDeviceSourceIds(device))) {
498
+ this.log.warn(`Skipping device "${device.name}" because it has unusable source state IDs`);
499
+ return;
500
+ }
501
+
393
502
  const temperature = await this.readNumberState(device.temperatureStateId);
394
503
  const relativeHumidity = await this.readNumberState(device.relativeHumidityStateId);
395
504
  const absoluteHumidity = calculateAbsoluteHumidity(temperature, relativeHumidity);
@@ -424,7 +533,20 @@ class Absolutehumidity extends utils.Adapter {
424
533
  * @param {string} stateId
425
534
  */
426
535
  async readNumberState(stateId) {
427
- const state = await this.getForeignStateAsync(stateId);
536
+ if (!(await this.isUsableSourceStateId(stateId))) {
537
+ this.log.warn(`Cannot read unusable state ID: ${stateId}`);
538
+ return null;
539
+ }
540
+
541
+ let state;
542
+
543
+ try {
544
+ state = await this.getForeignStateAsync(stateId);
545
+ } catch (error) {
546
+ this.log.warn(`Cannot read state ID "${stateId}": ${error.message}`);
547
+ return null;
548
+ }
549
+
428
550
  const value = Number(state?.val);
429
551
 
430
552
  return Number.isFinite(value) ? value : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.absolutehumidity",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "build absolute humidity from actual temperature and relative humidity",
5
5
  "author": {
6
6
  "name": "BenAhrdt",