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 +25 -1
- package/admin/i18n/de.json +1 -0
- package/admin/i18n/en.json +1 -0
- package/admin/i18n/es.json +1 -0
- package/admin/i18n/fr.json +1 -0
- package/admin/i18n/it.json +1 -0
- package/admin/i18n/nl.json +1 -0
- package/admin/i18n/pl.json +1 -0
- package/admin/i18n/pt.json +1 -0
- package/admin/i18n/ru.json +1 -0
- package/admin/i18n/uk.json +1 -0
- package/admin/i18n/zh-cn.json +1 -0
- package/admin/jsonConfig.json +1 -1
- package/io-package.json +40 -5
- package/lib/modules/deviceManager.js +68 -11
- package/lib/modules/i18n.js +30 -197
- package/main.js +272 -21
- package/package.json +1 -1
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
|
+

|
|
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.
|
package/admin/i18n/de.json
CHANGED
|
@@ -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",
|
package/admin/i18n/en.json
CHANGED
|
@@ -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",
|
package/admin/i18n/es.json
CHANGED
|
@@ -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",
|
package/admin/i18n/fr.json
CHANGED
|
@@ -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",
|
package/admin/i18n/it.json
CHANGED
|
@@ -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",
|
package/admin/i18n/nl.json
CHANGED
|
@@ -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",
|
package/admin/i18n/pl.json
CHANGED
|
@@ -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",
|
package/admin/i18n/pt.json
CHANGED
|
@@ -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",
|
package/admin/i18n/ru.json
CHANGED
|
@@ -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": "Состояние температуры",
|
package/admin/i18n/uk.json
CHANGED
|
@@ -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": "Стан температури",
|
package/admin/i18n/zh-cn.json
CHANGED
package/admin/jsonConfig.json
CHANGED
|
@@ -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>
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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',
|
package/lib/modules/i18n.js
CHANGED
|
@@ -1,203 +1,36 @@
|
|
|
1
1
|
/* eslint-disable jsdoc/require-jsdoc */
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
138
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
325
|
-
this.
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|