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 +26 -1
- package/admin/jsonConfig.json +2 -2
- package/io-package.json +40 -5
- package/lib/modules/deviceManager.js +7 -0
- package/lib/modules/idUtils.js +13 -0
- package/main.js +159 -21
- package/package.json +1 -1
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
|
+

|
|
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.
|
package/admin/jsonConfig.json
CHANGED
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"color": "#ffffff"
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
-
"
|
|
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.
|
|
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',
|
package/lib/modules/idUtils.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
165
|
-
const relativeHumidityStateValid = await this.
|
|
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
|
|
234
|
+
return null;
|
|
189
235
|
}
|
|
190
236
|
|
|
191
|
-
let object;
|
|
192
|
-
|
|
193
237
|
try {
|
|
194
|
-
|
|
238
|
+
return await this.getForeignObjectAsync(stateId);
|
|
195
239
|
} catch (error) {
|
|
196
240
|
this.log.warn(`Cannot validate state ID "${stateId}": ${error.message}`);
|
|
197
|
-
return
|
|
241
|
+
return null;
|
|
198
242
|
}
|
|
243
|
+
}
|
|
199
244
|
|
|
200
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
|
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.
|
|
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
|
*/
|