iobroker.absolutehumidity 0.0.3 → 0.0.5

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,17 +14,42 @@
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.5 (2026-07-05)
39
+ * (BenAhrdt) Id will change in case of renameing device
40
+ * (BenAhrdt) Add second manual calculation for compare
41
+
42
+ ### 0.0.4 (2026-07-05)
43
+ * (BenAhrdt) Build Config in adapter own device object
44
+
22
45
  ### 0.0.3 (2026-07-05)
23
46
  * (BenAhrdt) Add new features and icons to deviceManager
24
47
 
25
48
  ### 0.0.2 (2026-07-05)
26
49
  * (BenAhrdt) initial release
27
50
 
51
+ [Older changes can be found there](CHANGELOG_OLD.md)
52
+
28
53
  ## License
29
54
  MIT License
30
55
 
@@ -46,4 +71,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
46
71
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
47
72
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
48
73
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
49
- SOFTWARE.
74
+ SOFTWARE.
@@ -35,10 +35,10 @@
35
35
  "color": "#ffffff"
36
36
  }
37
37
  },
38
- "_previewCard": {
38
+ "_previewCards": {
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;' 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>",
41
+ "text": "<div style='display:flex;justify-content:center;gap:32px;flex-wrap:wrap;margin-top:24px;'><div class='ah-preview-card' style='width:300px;height:376px;background:#d3d3d3;border-radius:4px;overflow:hidden;box-shadow:0 2px 4px rgba(0,0,0,.25);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;font-size:15px;'><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\"))||{};document.querySelectorAll(\".ah-preview-card [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> 1</div><div style='height:316px;padding:18px 16px;box-sizing:border-box;display:flex;flex-direction:column;'><div style='height:24px;margin-bottom:22px;display:flex;align-items:center;'><i class='material-icons' aria-hidden='true' style='color:#00a51a;font-size:22px;line-height:22px;'>link</i></div><div style='display:grid;grid-template-columns:28px 1fr auto;gap:18px 8px;align-items:center;font-size:16px;'><svg viewBox='0 0 24 24' width='24' height='24' aria-hidden='true'><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'/><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'/></svg><label for='ah-preview-temp-1'><span data-ah-i18n='Temperature'>Temperature</span>:</label><span style='white-space:nowrap;'><input id='ah-preview-temp-1' 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;font-size:14px;' 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='font-size:14px;'>°C</span></span><svg viewBox='0 0 24 24' width='24' height='24' aria-hidden='true'><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'/><text x='12' y='16' text-anchor='middle' font-family='Arial, sans-serif' font-size='7' font-weight='700' fill='#fff'>%</text></svg><label for='ah-preview-rh-1'><span data-ah-i18n='Relative humidity'>Relative humidity</span>:</label><span style='white-space:nowrap;'><input id='ah-preview-rh-1' 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;font-size:14px;' 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='font-size:14px;'>%</span></span><svg viewBox='0 0 24 24' width='24' height='24' aria-hidden='true'><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'/><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'/></svg><span><span data-ah-i18n='Absolute humidity'>Absolute humidity</span>:</span><span style='text-align:right;white-space:nowrap;'><span data-ah-abs>9.16</span> <span style='font-size:14px;'>g/m³</span></span><svg viewBox='0 0 24 24' width='24' height='24' aria-hidden='true'><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'/><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'/><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'/></svg><span><span data-ah-i18n='Dew point temperature'>Dew point temperature</span>:</span><span style='text-align:right;white-space:nowrap;'><span data-ah-dew>10.18</span> <span style='font-size:14px;'>°C</span></span></div><div style='flex:1;'></div></div></div><div class='ah-preview-card' style='width:300px;height:376px;background:#d3d3d3;border-radius:4px;overflow:hidden;box-shadow:0 2px 4px rgba(0,0,0,.25);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;font-size:15px;'><img src='adapter/absolutehumidity/absolutehumidity.svg' alt='' style='width:34px;height:34px;object-fit:contain;'><span data-ah-i18n='Manual calculation'>Manual calculation</span> 2</div><div style='height:316px;padding:18px 16px;box-sizing:border-box;display:flex;flex-direction:column;'><div style='height:24px;margin-bottom:22px;display:flex;align-items:center;'><i class='material-icons' aria-hidden='true' style='color:#00a51a;font-size:22px;line-height:22px;'>link</i></div><div style='display:grid;grid-template-columns:28px 1fr auto;gap:18px 8px;align-items:center;font-size:16px;'><svg viewBox='0 0 24 24' width='24' height='24' aria-hidden='true'><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'/><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'/></svg><label for='ah-preview-temp-2'><span data-ah-i18n='Temperature'>Temperature</span>:</label><span style='white-space:nowrap;'><input id='ah-preview-temp-2' 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;font-size:14px;' 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='font-size:14px;'>°C</span></span><svg viewBox='0 0 24 24' width='24' height='24' aria-hidden='true'><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'/><text x='12' y='16' text-anchor='middle' font-family='Arial, sans-serif' font-size='7' font-weight='700' fill='#fff'>%</text></svg><label for='ah-preview-rh-2'><span data-ah-i18n='Relative humidity'>Relative humidity</span>:</label><span style='white-space:nowrap;'><input id='ah-preview-rh-2' 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;font-size:14px;' 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='font-size:14px;'>%</span></span><svg viewBox='0 0 24 24' width='24' height='24' aria-hidden='true'><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'/><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'/></svg><span><span data-ah-i18n='Absolute humidity'>Absolute humidity</span>:</span><span style='text-align:right;white-space:nowrap;'><span data-ah-abs>9.16</span> <span style='font-size:14px;'>g/m³</span></span><svg viewBox='0 0 24 24' width='24' height='24' aria-hidden='true'><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'/><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'/><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'/></svg><span><span data-ah-i18n='Dew point temperature'>Dew point temperature</span>:</span><span style='text-align:right;white-space:nowrap;'><span data-ah-dew>10.18</span> <span style='font-size:14px;'>°C</span></span></div><div style='flex:1;'></div></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.3",
4
+ "version": "0.0.5",
5
5
  "news": {
6
+ "0.0.5": {
7
+ "en": "Id will change in case of renameing device\nAdd second manual calculation for compare",
8
+ "de": "Die ID ändert sich, wenn das Gerät umbenannt wird.\nFüge eine zweite manuelle Berechnung zum Vergleich hinzu.",
9
+ "ru": "Идентификатор изменится в случае переименования устройства.\nДобавьте второй ручной расчет для сравнения.",
10
+ "pt": "O ID será alterado caso o dispositivo seja renomeado.\nAdicionar um segundo cálculo manual para comparação.",
11
+ "nl": "De ID verandert als het apparaat een andere naam krijgt.\nVoeg een tweede handmatige berekening toe ter vergelijking.",
12
+ "fr": "L'identifiant changera en cas de changement de nom de l'appareil.\nAjouter un deuxième calcul manuel pour la comparaison",
13
+ "it": "L'ID cambierà in caso di ridenominazione del dispositivo.\nAggiungi un secondo calcolo manuale per il confronto",
14
+ "es": "El ID cambiará en caso de que se cambie el nombre del dispositivo.\nAgregar un segundo cálculo manual para comparar",
15
+ "pl": "Identyfikator ulegnie zmianie w przypadku zmiany nazwy urządzenia\nDodaj drugie obliczenie ręczne w celu porównania",
16
+ "uk": "Ідентифікатор зміниться у разі перейменування пристрою\nДодати другий ручний розрахунок для порівняння",
17
+ "zh-cn": "设备重命名时,ID 将会更改。\n添加第二个手动计算以进行比较。"
18
+ },
19
+ "0.0.4": {
20
+ "en": "Build Config in adapter own device object",
21
+ "de": "Konfiguration im eigenen Geräteobjekt des Adapters erstellen",
22
+ "ru": "Создайте конфигурацию в собственном объекте устройства адаптера.",
23
+ "pt": "Configuração de construção no objeto de dispositivo próprio do adaptador",
24
+ "nl": "Bouw de configuratie in het eigen apparaatobject van de adapter.",
25
+ "fr": "Configurer l'objet périphérique propre à l'adaptateur",
26
+ "it": "Costruisci la configurazione nell'oggetto dispositivo dell'adattatore",
27
+ "es": "Construir la configuración en el objeto de dispositivo propio del adaptador",
28
+ "pl": "Konfiguracja kompilacji w obiekcie własnego urządzenia adaptera",
29
+ "uk": "Збірка конфігурації у власному об'єкті пристрою адаптера",
30
+ "zh-cn": "适配器自身设备对象中的构建配置"
31
+ },
6
32
  "0.0.3": {
7
33
  "en": "Add new features and icons to deviceManager",
8
34
  "de": "Füge dem Gerätemanager neue Funktionen und Symbole hinzu.",
@@ -127,9 +153,18 @@
127
153
  }
128
154
  ]
129
155
  },
130
- "native": {
131
- "devices": []
132
- },
156
+ "native": {},
133
157
  "objects": [],
134
- "instanceObjects": []
158
+ "instanceObjects": [
159
+ {
160
+ "_id": "devices",
161
+ "type": "folder",
162
+ "common": {
163
+ "name": "Devices"
164
+ },
165
+ "native": {
166
+ "devices": []
167
+ }
168
+ }
169
+ ]
135
170
  }
@@ -40,6 +40,11 @@ const STATE_ICONS = {
40
40
  `),
41
41
  };
42
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
+
43
48
  class AbsoluteHumidityDeviceManagement extends DeviceManagement {
44
49
  /**
45
50
  * @param {import('../../main')} adapter
@@ -322,6 +327,7 @@ function getDeviceFormSchema() {
322
327
  type: 'number',
323
328
  },
324
329
  },
330
+ filterFunc: TEMPERATURE_STATE_FILTER,
325
331
  },
326
332
  createTemperatureState: {
327
333
  type: 'checkbox',
@@ -342,6 +348,7 @@ function getDeviceFormSchema() {
342
348
  type: 'number',
343
349
  },
344
350
  },
351
+ filterFunc: RELATIVE_HUMIDITY_STATE_FILTER,
345
352
  },
346
353
  createRelativeHumidityState: {
347
354
  type: 'checkbox',
@@ -15,6 +15,18 @@ function createUniqueDeviceId(name, devices) {
15
15
  return id;
16
16
  }
17
17
 
18
+ /**
19
+ * @param {string} name
20
+ * @param {string} currentId
21
+ * @param {Record<string, any>[]} devices
22
+ */
23
+ function createUpdatedDeviceId(name, currentId, devices) {
24
+ return createUniqueDeviceId(
25
+ name,
26
+ devices.filter(device => device.id !== currentId),
27
+ );
28
+ }
29
+
18
30
  /**
19
31
  * @param {string} baseId
20
32
  * @param {Set<string>} usedIds
@@ -76,6 +88,7 @@ function replaceGermanUmlauts(value) {
76
88
  module.exports = {
77
89
  createUniqueDeviceId,
78
90
  createUniqueIdFromBase,
91
+ createUpdatedDeviceId,
79
92
  legacySanitizeId,
80
93
  replaceGermanUmlauts,
81
94
  sanitizeId,
package/main.js CHANGED
@@ -13,7 +13,13 @@ const {
13
13
  STATE_TEMPERATURE,
14
14
  } = require('./lib/modules/constants');
15
15
  const { AbsoluteHumidityDeviceManagement } = require('./lib/modules/deviceManager');
16
- const { createUniqueDeviceId, createUniqueIdFromBase, legacySanitizeId, sanitizeId } = require('./lib/modules/idUtils');
16
+ const {
17
+ createUniqueDeviceId,
18
+ createUniqueIdFromBase,
19
+ createUpdatedDeviceId,
20
+ legacySanitizeId,
21
+ sanitizeId,
22
+ } = require('./lib/modules/idUtils');
17
23
  const { translate } = require('./lib/modules/i18n');
18
24
 
19
25
  class Absolutehumidity extends utils.Adapter {
@@ -27,6 +33,7 @@ class Absolutehumidity extends utils.Adapter {
27
33
  });
28
34
 
29
35
  this.deviceManagement = null;
36
+ this.devices = [];
30
37
  this.subscribedSourceIds = new Set();
31
38
 
32
39
  this.on('ready', this.onReady.bind(this));
@@ -42,6 +49,8 @@ class Absolutehumidity extends utils.Adapter {
42
49
  async onReady() {
43
50
  this.deviceManagement = new AbsoluteHumidityDeviceManagement(this);
44
51
  await this.ensureInfoStates();
52
+ await this.ensureDeviceRootObject();
53
+ await this.loadDevices();
45
54
  await this.migrateLegacyDeviceIds();
46
55
  await this.rebuildAllDevices();
47
56
  await this.refreshSubscriptions();
@@ -52,7 +61,7 @@ class Absolutehumidity extends utils.Adapter {
52
61
  * @returns {Array<Record<string, any>>}
53
62
  */
54
63
  getConfiguredDevices() {
55
- return Array.isArray(this.config.devices) ? this.config.devices : [];
64
+ return this.devices;
56
65
  }
57
66
 
58
67
  /**
@@ -71,7 +80,6 @@ class Absolutehumidity extends utils.Adapter {
71
80
  const device = this.normalizeDeviceFormData(data, createUniqueDeviceId(data.name, devices));
72
81
  const updatedDevices = [...devices, device];
73
82
 
74
- await this.validateDeviceSourceIds(device);
75
83
  await this.ensureDeviceObjects(device);
76
84
  await this.updateDeviceValues(device);
77
85
  await this.refreshSubscriptions(updatedDevices);
@@ -85,12 +93,13 @@ class Absolutehumidity extends utils.Adapter {
85
93
  async updateDevice(id, data) {
86
94
  const oldDevice = this.getDeviceById(id);
87
95
  const devices = this.getConfiguredDevices();
88
- const device = this.normalizeDeviceFormData(data, id);
96
+ const updatedId = createUpdatedDeviceId(data.name, id, devices);
97
+ const device = this.normalizeDeviceFormData(data, updatedId);
89
98
  const updatedDevices = devices.map(existingDevice => (existingDevice.id === id ? device : existingDevice));
90
99
 
91
- await this.validateDeviceSourceIds(device);
92
-
93
- if (oldDevice) {
100
+ if (oldDevice && oldDevice.id !== device.id) {
101
+ await this.deleteObjectIfExists(this.getDeviceObjectId(oldDevice.id), { recursive: true });
102
+ } else if (oldDevice) {
94
103
  await this.deleteObsoleteDeviceStates(oldDevice, device);
95
104
  }
96
105
 
@@ -161,8 +170,8 @@ class Absolutehumidity extends utils.Adapter {
161
170
  * @param {Record<string, any>} device
162
171
  */
163
172
  async getDeviceSourceValidation(device) {
164
- const temperatureStateValid = await this.isUsableSourceStateId(device.temperatureStateId);
165
- const relativeHumidityStateValid = await this.isUsableSourceStateId(device.relativeHumidityStateId);
173
+ const temperatureStateValid = await this.isUsableTemperatureStateId(device.temperatureStateId);
174
+ const relativeHumidityStateValid = await this.isUsableRelativeHumidityStateId(device.relativeHumidityStateId);
166
175
 
167
176
  return {
168
177
  valid: temperatureStateValid && relativeHumidityStateValid,
@@ -184,26 +193,122 @@ class Absolutehumidity extends utils.Adapter {
184
193
  * @returns {Promise<boolean>}
185
194
  */
186
195
  async isUsableSourceStateId(stateId) {
196
+ const object = await this.getUsableSourceStateObject(stateId);
197
+
198
+ return object?.type === 'state' && object.common?.type === 'number';
199
+ }
200
+
201
+ /**
202
+ * @param {unknown} stateId
203
+ * @returns {Promise<boolean>}
204
+ */
205
+ async isUsableTemperatureStateId(stateId) {
206
+ const object = await this.getUsableSourceStateObject(stateId);
207
+
208
+ return (
209
+ object?.type === 'state' &&
210
+ object.common?.type === 'number' &&
211
+ this.isSupportedTemperatureUnit(object.common.unit)
212
+ );
213
+ }
214
+
215
+ /**
216
+ * @param {unknown} stateId
217
+ * @returns {Promise<boolean>}
218
+ */
219
+ async isUsableRelativeHumidityStateId(stateId) {
220
+ const object = await this.getUsableSourceStateObject(stateId);
221
+
222
+ return (
223
+ object?.type === 'state' &&
224
+ object.common?.type === 'number' &&
225
+ this.isSupportedRelativeHumidityUnit(object.common.unit)
226
+ );
227
+ }
228
+
229
+ /**
230
+ * @param {unknown} stateId
231
+ */
232
+ async getUsableSourceStateObject(stateId) {
187
233
  if (!this.isValidForeignStateId(stateId)) {
188
- return false;
234
+ return null;
189
235
  }
190
236
 
191
- let object;
192
-
193
237
  try {
194
- object = await this.getForeignObjectAsync(stateId);
238
+ return await this.getForeignObjectAsync(stateId);
195
239
  } catch (error) {
196
240
  this.log.warn(`Cannot validate state ID "${stateId}": ${error.message}`);
197
- return false;
241
+ return null;
198
242
  }
243
+ }
199
244
 
200
- return object?.type === 'state' && object.common?.type === 'number';
245
+ /**
246
+ * @param {unknown} unit
247
+ */
248
+ isSupportedTemperatureUnit(unit) {
249
+ const normalizedUnit = this.normalizeUnit(unit);
250
+
251
+ return (
252
+ normalizedUnit === '' || normalizedUnit === '°c' || normalizedUnit === 'c' || this.isFahrenheitUnit(unit)
253
+ );
254
+ }
255
+
256
+ /**
257
+ * @param {unknown} unit
258
+ */
259
+ isSupportedRelativeHumidityUnit(unit) {
260
+ const normalizedUnit = this.normalizeUnit(unit);
261
+
262
+ return normalizedUnit === '' || normalizedUnit === '%';
263
+ }
264
+
265
+ /**
266
+ * @param {unknown} unit
267
+ */
268
+ isFahrenheitUnit(unit) {
269
+ const normalizedUnit = this.normalizeUnit(unit);
270
+
271
+ return normalizedUnit === '°f' || normalizedUnit === 'f';
272
+ }
273
+
274
+ /**
275
+ * @param {unknown} unit
276
+ */
277
+ normalizeUnit(unit) {
278
+ return typeof unit === 'string' ? unit.trim().toLowerCase().replace(/\s+/g, '') : '';
201
279
  }
202
280
 
203
281
  /**
204
282
  * @param {Record<string, any>[]} devices
205
283
  */
206
284
  async saveDevices(devices) {
285
+ await this.extendObjectAsync(DEVICE_ROOT, {
286
+ native: {
287
+ devices,
288
+ },
289
+ });
290
+ this.devices = devices;
291
+ }
292
+
293
+ async loadDevices() {
294
+ const legacyDevices = Array.isArray(this.config.devices) ? this.config.devices : [];
295
+ const hasLegacyDevicesConfig = Object.prototype.hasOwnProperty.call(this.config, 'devices');
296
+
297
+ if (legacyDevices.length) {
298
+ await this.saveDevices(legacyDevices);
299
+ await this.deleteLegacyConfiguredDevices();
300
+ return;
301
+ }
302
+
303
+ const deviceRootObject = await this.getObjectAsync(DEVICE_ROOT);
304
+ this.devices = Array.isArray(deviceRootObject?.native?.devices) ? deviceRootObject.native.devices : [];
305
+
306
+ if (hasLegacyDevicesConfig) {
307
+ await this.deleteLegacyConfiguredDevices();
308
+ }
309
+ }
310
+
311
+ async deleteLegacyConfiguredDevices() {
207
312
  const adapterObjectId = `system.adapter.${this.namespace}`;
208
313
  const adapterObject = await this.getForeignObjectAsync(adapterObjectId);
209
314
 
@@ -211,13 +316,15 @@ class Absolutehumidity extends utils.Adapter {
211
316
  throw new Error(`Could not find adapter object ${adapterObjectId}`);
212
317
  }
213
318
 
214
- adapterObject.native = {
215
- ...adapterObject.native,
216
- devices,
217
- };
319
+ if (!Object.prototype.hasOwnProperty.call(adapterObject.native || {}, 'devices')) {
320
+ delete this.config.devices;
321
+ return;
322
+ }
323
+
324
+ delete adapterObject.native.devices;
218
325
 
219
326
  await this.setForeignObjectAsync(adapterObjectId, adapterObject);
220
- this.config.devices = devices;
327
+ delete this.config.devices;
221
328
  }
222
329
 
223
330
  async rebuildAllDevices() {
@@ -281,6 +388,18 @@ class Absolutehumidity extends utils.Adapter {
281
388
  });
282
389
  }
283
390
 
391
+ async ensureDeviceRootObject() {
392
+ await this.setObjectNotExistsAsync(DEVICE_ROOT, {
393
+ type: 'folder',
394
+ common: {
395
+ name: 'Devices',
396
+ },
397
+ native: {
398
+ devices: [],
399
+ },
400
+ });
401
+ }
402
+
284
403
  /**
285
404
  * @param {Record<string, any>} device
286
405
  */
@@ -499,7 +618,7 @@ class Absolutehumidity extends utils.Adapter {
499
618
  return;
500
619
  }
501
620
 
502
- const temperature = await this.readNumberState(device.temperatureStateId);
621
+ const temperature = await this.readTemperatureStateAsCelsius(device.temperatureStateId);
503
622
  const relativeHumidity = await this.readNumberState(device.relativeHumidityStateId);
504
623
  const absoluteHumidity = calculateAbsoluteHumidity(temperature, relativeHumidity);
505
624
  const dewPointTemperature = calculateDewPointTemperature(temperature, relativeHumidity);
@@ -529,6 +648,25 @@ class Absolutehumidity extends utils.Adapter {
529
648
  });
530
649
  }
531
650
 
651
+ /**
652
+ * @param {string} stateId
653
+ */
654
+ async readTemperatureStateAsCelsius(stateId) {
655
+ const temperature = await this.readNumberState(stateId);
656
+
657
+ if (temperature === null) {
658
+ return null;
659
+ }
660
+
661
+ const object = await this.getUsableSourceStateObject(stateId);
662
+
663
+ if (this.isFahrenheitUnit(object?.common?.unit)) {
664
+ return ((temperature - 32) * 5) / 9;
665
+ }
666
+
667
+ return temperature;
668
+ }
669
+
532
670
  /**
533
671
  * @param {string} stateId
534
672
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.absolutehumidity",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "build absolute humidity from actual temperature and relative humidity",
5
5
  "author": {
6
6
  "name": "BenAhrdt",