iobroker.absolutehumidity 0.0.2 → 0.0.4

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
@@ -14,14 +14,38 @@
14
14
 
15
15
  build absolute humidity from actual temperature and relative humidity
16
16
 
17
+ ## Calculation method
18
+
19
+ The adapter uses empirical Magnus approximation formulas to calculate the
20
+ saturation vapor pressure from temperature and relative humidity.
21
+
22
+ * Absolute humidity is calculated from the saturation vapor pressure using the
23
+ Magnus/Bolton approximation and the ideal gas law. The result is returned in
24
+ g/m³.
25
+ * Dew point temperature is calculated using the Magnus formula with Sonntag
26
+ coefficients. The result is returned in °C.
27
+
28
+ Small deviations from online tables are expected because different tables often
29
+ use different Magnus, Tetens, Sonntag, Bolton or Buck coefficient sets.
30
+
31
+ ![Kachel](image.png)
32
+
17
33
  ## Changelog
18
34
  <!--
19
35
  Placeholder for the next version (at the beginning of the line):
20
36
  ### **WORK IN PROGRESS**
21
37
  -->
38
+ ### 0.0.4 (2026-07-05)
39
+ * (BenAhrdt) Build Config in adapter own device object
40
+
41
+ ### 0.0.3 (2026-07-05)
42
+ * (BenAhrdt) Add new features and icons to deviceManager
43
+
22
44
  ### 0.0.2 (2026-07-05)
23
45
  * (BenAhrdt) initial release
24
46
 
47
+ [Older changes can be found there](CHANGELOG_OLD.md)
48
+
25
49
  ## License
26
50
  MIT License
27
51
 
@@ -43,4 +67,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
67
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
68
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45
69
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
46
- SOFTWARE.
70
+ SOFTWARE.
@@ -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,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "absolutehumidity",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
5
  "news": {
6
+ "0.0.4": {
7
+ "en": "Build Config in adapter own device object",
8
+ "de": "Konfiguration im eigenen Geräteobjekt des Adapters erstellen",
9
+ "ru": "Создайте конфигурацию в собственном объекте устройства адаптера.",
10
+ "pt": "Configuração de construção no objeto de dispositivo próprio do adaptador",
11
+ "nl": "Bouw de configuratie in het eigen apparaatobject van de adapter.",
12
+ "fr": "Configurer l'objet périphérique propre à l'adaptateur",
13
+ "it": "Costruisci la configurazione nell'oggetto dispositivo dell'adattatore",
14
+ "es": "Construir la configuración en el objeto de dispositivo propio del adaptador",
15
+ "pl": "Konfiguracja kompilacji w obiekcie własnego urządzenia adaptera",
16
+ "uk": "Збірка конфігурації у власному об'єкті пристрою адаптера",
17
+ "zh-cn": "适配器自身设备对象中的构建配置"
18
+ },
19
+ "0.0.3": {
20
+ "en": "Add new features and icons to deviceManager",
21
+ "de": "Füge dem Gerätemanager neue Funktionen und Symbole hinzu.",
22
+ "ru": "Добавьте новые функции и значки в deviceManager.",
23
+ "pt": "Adicionar novos recursos e ícones ao gerenciador de dispositivos",
24
+ "nl": "Voeg nieuwe functies en pictogrammen toe aan DeviceManager.",
25
+ "fr": "Ajouter de nouvelles fonctionnalités et icônes à deviceManager",
26
+ "it": "Aggiungi nuove funzionalità e icone a DeviceManager",
27
+ "es": "Agregar nuevas funciones e íconos al Administrador de dispositivos",
28
+ "pl": "Dodaj nowe funkcje i ikony do DeviceManager",
29
+ "uk": "Додавання нових функцій та піктограм до DeviceManager",
30
+ "zh-cn": "向设备管理器添加新功能和图标"
31
+ },
6
32
  "0.0.2": {
7
33
  "en": "initial release",
8
34
  "de": "Erstveröffentlichung",
@@ -114,9 +140,18 @@
114
140
  }
115
141
  ]
116
142
  },
117
- "native": {
118
- "devices": []
119
- },
143
+ "native": {},
120
144
  "objects": [],
121
- "instanceObjects": []
145
+ "instanceObjects": [
146
+ {
147
+ "_id": "devices",
148
+ "type": "folder",
149
+ "common": {
150
+ "name": "Devices"
151
+ },
152
+ "native": {
153
+ "devices": []
154
+ }
155
+ }
156
+ ]
122
157
  }
@@ -12,6 +12,39 @@ 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
+
43
+ const TEMPERATURE_STATE_FILTER =
44
+ "const unit = String(obj.common?.unit || '').trim().toLowerCase().replace(/\\s+/g, ''); return obj.type === 'state' && obj.common?.type === 'number' && (unit === '' || unit === '°c' || unit === 'c' || unit === '°f' || unit === 'f');";
45
+ const RELATIVE_HUMIDITY_STATE_FILTER =
46
+ "const unit = String(obj.common?.unit || '').trim().toLowerCase().replace(/\\s+/g, ''); return obj.type === 'state' && obj.common?.type === 'number' && (unit === '' || unit === '%');";
47
+
15
48
  class AbsoluteHumidityDeviceManagement extends DeviceManagement {
16
49
  /**
17
50
  * @param {import('../../main')} adapter
@@ -67,13 +100,15 @@ class AbsoluteHumidityDeviceManagement extends DeviceManagement {
67
100
  /**
68
101
  * @param {import('@iobroker/dm-utils').DeviceLoadContext<string>} context
69
102
  */
70
- loadDevices(context) {
103
+ async loadDevices(context) {
71
104
  const devices = this.adapter.getConfiguredDevices();
72
105
 
73
106
  context.setTotalDevices(devices.length);
74
107
 
75
108
  for (const device of devices) {
76
- context.addDevice(createDeviceInfo(this.adapter, device));
109
+ const sourceValidation = await this.adapter.getDeviceSourceValidation(device);
110
+
111
+ context.addDevice(createDeviceInfo(this.adapter, device, sourceValidation));
77
112
  }
78
113
  }
79
114
  }
@@ -81,14 +116,20 @@ class AbsoluteHumidityDeviceManagement extends DeviceManagement {
81
116
  /**
82
117
  * @param {import('../../main')} adapter
83
118
  * @param {Record<string, any>} device
119
+ * @param {{ valid: boolean; temperatureStateValid: boolean; relativeHumidityStateValid: boolean }} sourceValidation
84
120
  */
85
- function createDeviceInfo(adapter, device) {
121
+ function createDeviceInfo(adapter, device, sourceValidation) {
86
122
  return {
87
123
  id: device.id,
88
124
  name: device.name,
89
125
  icon: ADAPTER_ICON,
90
126
  backgroundColor: CARD_HEADER_BACKGROUND,
91
- status: 'connected',
127
+ status: sourceValidation.valid
128
+ ? 'connected'
129
+ : {
130
+ connection: 'disconnected',
131
+ warning: translate('Invalid source state'),
132
+ },
92
133
  actions: [
93
134
  {
94
135
  id: 'editDevice',
@@ -124,15 +165,16 @@ function createDeviceInfo(adapter, device) {
124
165
  },
125
166
  },
126
167
  ],
127
- customInfo: createDeviceCustomInfo(adapter, device),
168
+ customInfo: createDeviceCustomInfo(adapter, device, sourceValidation),
128
169
  };
129
170
  }
130
171
 
131
172
  /**
132
173
  * @param {import('../../main')} adapter
133
174
  * @param {Record<string, any>} device
175
+ * @param {{ valid: boolean; temperatureStateValid: boolean; relativeHumidityStateValid: boolean }} sourceValidation
134
176
  */
135
- function createDeviceCustomInfo(adapter, device) {
177
+ function createDeviceCustomInfo(adapter, device, sourceValidation) {
136
178
  return {
137
179
  id: device.id,
138
180
  schema: {
@@ -144,31 +186,35 @@ function createDeviceCustomInfo(adapter, device) {
144
186
  items: {
145
187
  temperature: createStateInfoItem(
146
188
  translate('Temperature'),
147
- device.createTemperatureState
189
+ device.createTemperatureState || !sourceValidation.temperatureStateValid
148
190
  ? `${adapter.getDeviceObjectId(device.id)}.${STATE_TEMPERATURE}`
149
191
  : device.temperatureStateId,
150
192
  '°C',
151
- !device.createTemperatureState,
193
+ !device.createTemperatureState && sourceValidation.temperatureStateValid,
194
+ STATE_ICONS.temperature,
152
195
  ),
153
196
  relativeHumidity: createStateInfoItem(
154
197
  translate('Relative humidity'),
155
- device.createRelativeHumidityState
198
+ device.createRelativeHumidityState || !sourceValidation.relativeHumidityStateValid
156
199
  ? `${adapter.getDeviceObjectId(device.id)}.${STATE_RELATIVE_HUMIDITY}`
157
200
  : device.relativeHumidityStateId,
158
201
  '%',
159
- !device.createRelativeHumidityState,
202
+ !device.createRelativeHumidityState && sourceValidation.relativeHumidityStateValid,
203
+ STATE_ICONS.relativeHumidity,
160
204
  ),
161
205
  absoluteHumidity: createStateInfoItem(
162
206
  translate('Absolute humidity'),
163
207
  `${adapter.getDeviceObjectId(device.id)}.${STATE_ABSOLUTE_HUMIDITY}`,
164
208
  'g/m³',
165
209
  false,
210
+ STATE_ICONS.absoluteHumidity,
166
211
  ),
167
212
  dewPointTemperature: createStateInfoItem(
168
213
  translate('Dew point temperature'),
169
214
  `${adapter.getDeviceObjectId(device.id)}.${STATE_DEW_POINT_TEMPERATURE}`,
170
215
  '°C',
171
216
  false,
217
+ STATE_ICONS.dewPointTemperature,
172
218
  ),
173
219
  spacer: {
174
220
  type: 'staticText',
@@ -200,11 +246,13 @@ function createDeviceCustomInfo(adapter, device) {
200
246
  * @param {string} oid
201
247
  * @param {string} unit
202
248
  * @param {boolean} foreign
249
+ * @param {string} labelIcon
203
250
  */
204
- function createStateInfoItem(label, oid, unit, foreign) {
251
+ function createStateInfoItem(label, oid, unit, foreign, labelIcon) {
205
252
  return {
206
253
  type: 'state',
207
254
  label,
255
+ labelIcon,
208
256
  oid,
209
257
  foreign,
210
258
  control: 'text',
@@ -221,6 +269,13 @@ function createStateInfoItem(label, oid, unit, foreign) {
221
269
  };
222
270
  }
223
271
 
272
+ /**
273
+ * @param {string} svg
274
+ */
275
+ function createSvgIcon(svg) {
276
+ return `data:image/svg+xml;base64,${Buffer.from(svg.replace(/\s+/g, ' ').trim()).toString('base64')}`;
277
+ }
278
+
224
279
  /**
225
280
  * @param {import('@iobroker/dm-utils').ActionContext} context
226
281
  * @param {Record<string, any> | undefined} device
@@ -272,6 +327,7 @@ function getDeviceFormSchema() {
272
327
  type: 'number',
273
328
  },
274
329
  },
330
+ filterFunc: TEMPERATURE_STATE_FILTER,
275
331
  },
276
332
  createTemperatureState: {
277
333
  type: 'checkbox',
@@ -292,6 +348,7 @@ function getDeviceFormSchema() {
292
348
  type: 'number',
293
349
  },
294
350
  },
351
+ filterFunc: RELATIVE_HUMIDITY_STATE_FILTER,
295
352
  },
296
353
  createRelativeHumidityState: {
297
354
  type: 'checkbox',
@@ -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
@@ -27,6 +27,7 @@ class Absolutehumidity extends utils.Adapter {
27
27
  });
28
28
 
29
29
  this.deviceManagement = null;
30
+ this.devices = [];
30
31
  this.subscribedSourceIds = new Set();
31
32
 
32
33
  this.on('ready', this.onReady.bind(this));
@@ -42,6 +43,8 @@ class Absolutehumidity extends utils.Adapter {
42
43
  async onReady() {
43
44
  this.deviceManagement = new AbsoluteHumidityDeviceManagement(this);
44
45
  await this.ensureInfoStates();
46
+ await this.ensureDeviceRootObject();
47
+ await this.loadDevices();
45
48
  await this.migrateLegacyDeviceIds();
46
49
  await this.rebuildAllDevices();
47
50
  await this.refreshSubscriptions();
@@ -52,7 +55,7 @@ class Absolutehumidity extends utils.Adapter {
52
55
  * @returns {Array<Record<string, any>>}
53
56
  */
54
57
  getConfiguredDevices() {
55
- return Array.isArray(this.config.devices) ? this.config.devices : [];
58
+ return this.devices;
56
59
  }
57
60
 
58
61
  /**
@@ -69,11 +72,12 @@ class Absolutehumidity extends utils.Adapter {
69
72
  async addDevice(data) {
70
73
  const devices = this.getConfiguredDevices();
71
74
  const device = this.normalizeDeviceFormData(data, createUniqueDeviceId(data.name, devices));
75
+ const updatedDevices = [...devices, device];
72
76
 
73
- await this.saveDevices([...devices, device]);
74
77
  await this.ensureDeviceObjects(device);
75
- await this.refreshSubscriptions();
76
78
  await this.updateDeviceValues(device);
79
+ await this.refreshSubscriptions(updatedDevices);
80
+ await this.saveDevices(updatedDevices);
77
81
  }
78
82
 
79
83
  /**
@@ -84,16 +88,16 @@ class Absolutehumidity extends utils.Adapter {
84
88
  const oldDevice = this.getDeviceById(id);
85
89
  const devices = this.getConfiguredDevices();
86
90
  const device = this.normalizeDeviceFormData(data, id);
87
-
88
- await this.saveDevices(devices.map(existingDevice => (existingDevice.id === id ? device : existingDevice)));
91
+ const updatedDevices = devices.map(existingDevice => (existingDevice.id === id ? device : existingDevice));
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,10 +127,179 @@ 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.isUsableTemperatureStateId(device.temperatureStateId);
165
+ const relativeHumidityStateValid = await this.isUsableRelativeHumidityStateId(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
+ const object = await this.getUsableSourceStateObject(stateId);
188
+
189
+ return object?.type === 'state' && object.common?.type === 'number';
190
+ }
191
+
192
+ /**
193
+ * @param {unknown} stateId
194
+ * @returns {Promise<boolean>}
195
+ */
196
+ async isUsableTemperatureStateId(stateId) {
197
+ const object = await this.getUsableSourceStateObject(stateId);
198
+
199
+ return (
200
+ object?.type === 'state' &&
201
+ object.common?.type === 'number' &&
202
+ this.isSupportedTemperatureUnit(object.common.unit)
203
+ );
204
+ }
205
+
206
+ /**
207
+ * @param {unknown} stateId
208
+ * @returns {Promise<boolean>}
209
+ */
210
+ async isUsableRelativeHumidityStateId(stateId) {
211
+ const object = await this.getUsableSourceStateObject(stateId);
212
+
213
+ return (
214
+ object?.type === 'state' &&
215
+ object.common?.type === 'number' &&
216
+ this.isSupportedRelativeHumidityUnit(object.common.unit)
217
+ );
218
+ }
219
+
220
+ /**
221
+ * @param {unknown} stateId
222
+ */
223
+ async getUsableSourceStateObject(stateId) {
224
+ if (!this.isValidForeignStateId(stateId)) {
225
+ return null;
226
+ }
227
+
228
+ try {
229
+ return await this.getForeignObjectAsync(stateId);
230
+ } catch (error) {
231
+ this.log.warn(`Cannot validate state ID "${stateId}": ${error.message}`);
232
+ return null;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * @param {unknown} unit
238
+ */
239
+ isSupportedTemperatureUnit(unit) {
240
+ const normalizedUnit = this.normalizeUnit(unit);
241
+
242
+ return (
243
+ normalizedUnit === '' || normalizedUnit === '°c' || normalizedUnit === 'c' || this.isFahrenheitUnit(unit)
244
+ );
245
+ }
246
+
247
+ /**
248
+ * @param {unknown} unit
249
+ */
250
+ isSupportedRelativeHumidityUnit(unit) {
251
+ const normalizedUnit = this.normalizeUnit(unit);
252
+
253
+ return normalizedUnit === '' || normalizedUnit === '%';
254
+ }
255
+
256
+ /**
257
+ * @param {unknown} unit
258
+ */
259
+ isFahrenheitUnit(unit) {
260
+ const normalizedUnit = this.normalizeUnit(unit);
261
+
262
+ return normalizedUnit === '°f' || normalizedUnit === 'f';
263
+ }
264
+
265
+ /**
266
+ * @param {unknown} unit
267
+ */
268
+ normalizeUnit(unit) {
269
+ return typeof unit === 'string' ? unit.trim().toLowerCase().replace(/\s+/g, '') : '';
270
+ }
271
+
125
272
  /**
126
273
  * @param {Record<string, any>[]} devices
127
274
  */
128
275
  async saveDevices(devices) {
276
+ await this.extendObjectAsync(DEVICE_ROOT, {
277
+ native: {
278
+ devices,
279
+ },
280
+ });
281
+ this.devices = devices;
282
+ }
283
+
284
+ async loadDevices() {
285
+ const legacyDevices = Array.isArray(this.config.devices) ? this.config.devices : [];
286
+ const hasLegacyDevicesConfig = Object.prototype.hasOwnProperty.call(this.config, 'devices');
287
+
288
+ if (legacyDevices.length) {
289
+ await this.saveDevices(legacyDevices);
290
+ await this.deleteLegacyConfiguredDevices();
291
+ return;
292
+ }
293
+
294
+ const deviceRootObject = await this.getObjectAsync(DEVICE_ROOT);
295
+ this.devices = Array.isArray(deviceRootObject?.native?.devices) ? deviceRootObject.native.devices : [];
296
+
297
+ if (hasLegacyDevicesConfig) {
298
+ await this.deleteLegacyConfiguredDevices();
299
+ }
300
+ }
301
+
302
+ async deleteLegacyConfiguredDevices() {
129
303
  const adapterObjectId = `system.adapter.${this.namespace}`;
130
304
  const adapterObject = await this.getForeignObjectAsync(adapterObjectId);
131
305
 
@@ -133,13 +307,15 @@ class Absolutehumidity extends utils.Adapter {
133
307
  throw new Error(`Could not find adapter object ${adapterObjectId}`);
134
308
  }
135
309
 
136
- adapterObject.native = {
137
- ...adapterObject.native,
138
- devices,
139
- };
310
+ if (!Object.prototype.hasOwnProperty.call(adapterObject.native || {}, 'devices')) {
311
+ delete this.config.devices;
312
+ return;
313
+ }
314
+
315
+ delete adapterObject.native.devices;
140
316
 
141
317
  await this.setForeignObjectAsync(adapterObjectId, adapterObject);
142
- this.config.devices = devices;
318
+ delete this.config.devices;
143
319
  }
144
320
 
145
321
  async rebuildAllDevices() {
@@ -182,11 +358,15 @@ class Absolutehumidity extends utils.Adapter {
182
358
  return;
183
359
  }
184
360
 
185
- await this.saveDevices(migratedDevices);
361
+ for (const device of migratedDevices) {
362
+ await this.ensureDeviceObjects(device);
363
+ }
186
364
 
187
365
  for (const oldId of oldIdsToDelete) {
188
366
  await this.deleteObjectIfExists(this.getDeviceObjectId(oldId), { recursive: true });
189
367
  }
368
+
369
+ await this.saveDevices(migratedDevices);
190
370
  }
191
371
 
192
372
  async ensureInfoStates() {
@@ -199,6 +379,18 @@ class Absolutehumidity extends utils.Adapter {
199
379
  });
200
380
  }
201
381
 
382
+ async ensureDeviceRootObject() {
383
+ await this.setObjectNotExistsAsync(DEVICE_ROOT, {
384
+ type: 'folder',
385
+ common: {
386
+ name: 'Devices',
387
+ },
388
+ native: {
389
+ devices: [],
390
+ },
391
+ });
392
+ }
393
+
202
394
  /**
203
395
  * @param {Record<string, any>} device
204
396
  */
@@ -314,20 +506,42 @@ class Absolutehumidity extends utils.Adapter {
314
506
  }
315
507
  }
316
508
 
317
- async refreshSubscriptions() {
509
+ /**
510
+ * @param {Array<Record<string, any>>} [devices]
511
+ */
512
+ async refreshSubscriptions(devices = this.getConfiguredDevices()) {
318
513
  for (const stateId of this.subscribedSourceIds) {
319
514
  this.unsubscribeForeignStates(stateId);
320
515
  }
321
516
 
322
517
  this.subscribedSourceIds.clear();
323
518
 
324
- for (const device of this.getConfiguredDevices()) {
325
- this.subscribedSourceIds.add(device.temperatureStateId);
326
- this.subscribedSourceIds.add(device.relativeHumidityStateId);
519
+ for (const device of devices) {
520
+ const sourceValidation = await this.getDeviceSourceValidation(device);
521
+
522
+ if (sourceValidation.temperatureStateValid) {
523
+ this.subscribedSourceIds.add(device.temperatureStateId);
524
+ } else {
525
+ this.log.warn(
526
+ `Ignoring unusable temperature state ID for device "${device.name}": ${device.temperatureStateId}`,
527
+ );
528
+ }
529
+
530
+ if (sourceValidation.relativeHumidityStateValid) {
531
+ this.subscribedSourceIds.add(device.relativeHumidityStateId);
532
+ } else {
533
+ this.log.warn(
534
+ `Ignoring unusable relative humidity state ID for device "${device.name}": ${device.relativeHumidityStateId}`,
535
+ );
536
+ }
327
537
  }
328
538
 
329
539
  for (const stateId of this.subscribedSourceIds) {
330
- this.subscribeForeignStates(stateId);
540
+ try {
541
+ this.subscribeForeignStates(stateId);
542
+ } catch (error) {
543
+ this.log.warn(`Cannot subscribe state ID "${stateId}": ${error.message}`);
544
+ }
331
545
  }
332
546
  }
333
547
 
@@ -390,7 +604,12 @@ class Absolutehumidity extends utils.Adapter {
390
604
  * @param {Record<string, any>} device
391
605
  */
392
606
  async updateDeviceValues(device) {
393
- const temperature = await this.readNumberState(device.temperatureStateId);
607
+ if (!(await this.hasValidDeviceSourceIds(device))) {
608
+ this.log.warn(`Skipping device "${device.name}" because it has unusable source state IDs`);
609
+ return;
610
+ }
611
+
612
+ const temperature = await this.readTemperatureStateAsCelsius(device.temperatureStateId);
394
613
  const relativeHumidity = await this.readNumberState(device.relativeHumidityStateId);
395
614
  const absoluteHumidity = calculateAbsoluteHumidity(temperature, relativeHumidity);
396
615
  const dewPointTemperature = calculateDewPointTemperature(temperature, relativeHumidity);
@@ -420,11 +639,43 @@ class Absolutehumidity extends utils.Adapter {
420
639
  });
421
640
  }
422
641
 
642
+ /**
643
+ * @param {string} stateId
644
+ */
645
+ async readTemperatureStateAsCelsius(stateId) {
646
+ const temperature = await this.readNumberState(stateId);
647
+
648
+ if (temperature === null) {
649
+ return null;
650
+ }
651
+
652
+ const object = await this.getUsableSourceStateObject(stateId);
653
+
654
+ if (this.isFahrenheitUnit(object?.common?.unit)) {
655
+ return ((temperature - 32) * 5) / 9;
656
+ }
657
+
658
+ return temperature;
659
+ }
660
+
423
661
  /**
424
662
  * @param {string} stateId
425
663
  */
426
664
  async readNumberState(stateId) {
427
- const state = await this.getForeignStateAsync(stateId);
665
+ if (!(await this.isUsableSourceStateId(stateId))) {
666
+ this.log.warn(`Cannot read unusable state ID: ${stateId}`);
667
+ return null;
668
+ }
669
+
670
+ let state;
671
+
672
+ try {
673
+ state = await this.getForeignStateAsync(stateId);
674
+ } catch (error) {
675
+ this.log.warn(`Cannot read state ID "${stateId}": ${error.message}`);
676
+ return null;
677
+ }
678
+
428
679
  const value = Number(state?.val);
429
680
 
430
681
  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.4",
4
4
  "description": "build absolute humidity from actual temperature and relative humidity",
5
5
  "author": {
6
6
  "name": "BenAhrdt",