iobroker.absolutehumidity 0.0.1

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/main.js ADDED
@@ -0,0 +1,462 @@
1
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2
+ // @ts-nocheck
3
+ /* eslint-disable jsdoc/require-param-description, jsdoc/require-returns-description, jsdoc/reject-any-type */
4
+ 'use strict';
5
+
6
+ const utils = require('@iobroker/adapter-core');
7
+ const { calculateAbsoluteHumidity, calculateDewPointTemperature } = require('./lib/modules/calculation');
8
+ const {
9
+ DEVICE_ROOT,
10
+ STATE_ABSOLUTE_HUMIDITY,
11
+ STATE_DEW_POINT_TEMPERATURE,
12
+ STATE_RELATIVE_HUMIDITY,
13
+ STATE_TEMPERATURE,
14
+ } = require('./lib/modules/constants');
15
+ const { AbsoluteHumidityDeviceManagement } = require('./lib/modules/deviceManager');
16
+ const { createUniqueDeviceId, createUniqueIdFromBase, legacySanitizeId, sanitizeId } = require('./lib/modules/idUtils');
17
+ const { translate } = require('./lib/modules/i18n');
18
+
19
+ class Absolutehumidity extends utils.Adapter {
20
+ /**
21
+ * @param {Partial<utils.AdapterOptions>} [options] - Adapter options
22
+ */
23
+ constructor(options) {
24
+ super({
25
+ ...options,
26
+ name: 'absolutehumidity',
27
+ });
28
+
29
+ this.deviceManagement = null;
30
+ this.subscribedSourceIds = new Set();
31
+
32
+ this.on('ready', this.onReady.bind(this));
33
+ this.on('message', this.onMessage.bind(this));
34
+ this.on('stateChange', this.onStateChange.bind(this));
35
+ this.on('unload', this.onUnload.bind(this));
36
+ // this.on('objectChange', this.onObjectChange.bind(this));
37
+ }
38
+
39
+ /**
40
+ * Is called when databases are connected and adapter received configuration.
41
+ */
42
+ async onReady() {
43
+ this.deviceManagement = new AbsoluteHumidityDeviceManagement(this);
44
+ await this.ensureInfoStates();
45
+ await this.migrateLegacyDeviceIds();
46
+ await this.rebuildAllDevices();
47
+ await this.refreshSubscriptions();
48
+ await this.updateAllDevices();
49
+ }
50
+
51
+ /**
52
+ * @returns {Array<Record<string, any>>}
53
+ */
54
+ getConfiguredDevices() {
55
+ return Array.isArray(this.config.devices) ? this.config.devices : [];
56
+ }
57
+
58
+ /**
59
+ * @param {string} id
60
+ * @returns {Record<string, any> | undefined}
61
+ */
62
+ getDeviceById(id) {
63
+ return this.getConfiguredDevices().find(device => device.id === id);
64
+ }
65
+
66
+ /**
67
+ * @param {Record<string, any>} data
68
+ */
69
+ async addDevice(data) {
70
+ const devices = this.getConfiguredDevices();
71
+ const device = this.normalizeDeviceFormData(data, createUniqueDeviceId(data.name, devices));
72
+
73
+ await this.saveDevices([...devices, device]);
74
+ await this.ensureDeviceObjects(device);
75
+ await this.refreshSubscriptions();
76
+ await this.updateDeviceValues(device);
77
+ }
78
+
79
+ /**
80
+ * @param {string} id
81
+ * @param {Record<string, any>} data
82
+ */
83
+ async updateDevice(id, data) {
84
+ const oldDevice = this.getDeviceById(id);
85
+ const devices = this.getConfiguredDevices();
86
+ const device = this.normalizeDeviceFormData(data, id);
87
+
88
+ await this.saveDevices(devices.map(existingDevice => (existingDevice.id === id ? device : existingDevice)));
89
+
90
+ if (oldDevice) {
91
+ await this.deleteObsoleteDeviceStates(oldDevice, device);
92
+ }
93
+
94
+ await this.ensureDeviceObjects(device);
95
+ await this.refreshSubscriptions();
96
+ await this.updateDeviceValues(device);
97
+ }
98
+
99
+ /**
100
+ * @param {string} id
101
+ */
102
+ async deleteDevice(id) {
103
+ const devices = this.getConfiguredDevices();
104
+
105
+ await this.saveDevices(devices.filter(device => device.id !== id));
106
+ await this.deleteObjectIfExists(this.getDeviceObjectId(id), { recursive: true });
107
+ await this.refreshSubscriptions();
108
+ }
109
+
110
+ /**
111
+ * @param {Record<string, any>} data
112
+ * @param {string} id
113
+ */
114
+ normalizeDeviceFormData(data, id) {
115
+ return {
116
+ id,
117
+ name: String(data.name || id).trim(),
118
+ temperatureStateId: String(data.temperatureStateId || '').trim(),
119
+ relativeHumidityStateId: String(data.relativeHumidityStateId || '').trim(),
120
+ createTemperatureState: data.createTemperatureState !== false,
121
+ createRelativeHumidityState: data.createRelativeHumidityState !== false,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * @param {Record<string, any>[]} devices
127
+ */
128
+ async saveDevices(devices) {
129
+ const adapterObjectId = `system.adapter.${this.namespace}`;
130
+ const adapterObject = await this.getForeignObjectAsync(adapterObjectId);
131
+
132
+ if (!adapterObject) {
133
+ throw new Error(`Could not find adapter object ${adapterObjectId}`);
134
+ }
135
+
136
+ adapterObject.native = {
137
+ ...adapterObject.native,
138
+ devices,
139
+ };
140
+
141
+ await this.setForeignObjectAsync(adapterObjectId, adapterObject);
142
+ this.config.devices = devices;
143
+ }
144
+
145
+ async rebuildAllDevices() {
146
+ for (const device of this.getConfiguredDevices()) {
147
+ await this.ensureDeviceObjects(device);
148
+ }
149
+ }
150
+
151
+ async migrateLegacyDeviceIds() {
152
+ const devices = this.getConfiguredDevices();
153
+
154
+ if (!devices.length) {
155
+ return;
156
+ }
157
+
158
+ const usedLegacyIds = new Set();
159
+ const usedNewIds = new Set();
160
+ const migratedDevices = [];
161
+ const oldIdsToDelete = [];
162
+
163
+ for (const device of devices) {
164
+ const legacyId = createUniqueIdFromBase(legacySanitizeId(device.name || device.id), usedLegacyIds);
165
+ const newId = createUniqueIdFromBase(sanitizeId(device.name || device.id), usedNewIds);
166
+
167
+ usedLegacyIds.add(legacyId);
168
+ usedNewIds.add(newId);
169
+
170
+ if (device.id === legacyId && legacyId !== newId) {
171
+ migratedDevices.push({
172
+ ...device,
173
+ id: newId,
174
+ });
175
+ oldIdsToDelete.push(device.id);
176
+ } else {
177
+ migratedDevices.push(device);
178
+ }
179
+ }
180
+
181
+ if (!oldIdsToDelete.length) {
182
+ return;
183
+ }
184
+
185
+ await this.saveDevices(migratedDevices);
186
+
187
+ for (const oldId of oldIdsToDelete) {
188
+ await this.deleteObjectIfExists(this.getDeviceObjectId(oldId), { recursive: true });
189
+ }
190
+ }
191
+
192
+ async ensureInfoStates() {
193
+ await this.setObjectNotExistsAsync('info', {
194
+ type: 'channel',
195
+ common: {
196
+ name: 'Information',
197
+ },
198
+ native: {},
199
+ });
200
+ }
201
+
202
+ /**
203
+ * @param {Record<string, any>} device
204
+ */
205
+ async ensureDeviceObjects(device) {
206
+ const deviceObjectId = this.getDeviceObjectId(device.id);
207
+
208
+ await this.setObjectNotExistsAsync(deviceObjectId, {
209
+ type: 'device',
210
+ common: {
211
+ name: device.name,
212
+ },
213
+ native: {},
214
+ });
215
+
216
+ await this.extendObjectAsync(deviceObjectId, {
217
+ common: {
218
+ name: device.name,
219
+ },
220
+ native: {
221
+ temperatureStateId: device.temperatureStateId,
222
+ relativeHumidityStateId: device.relativeHumidityStateId,
223
+ },
224
+ });
225
+
226
+ await this.ensureStateObject(device, STATE_ABSOLUTE_HUMIDITY, {
227
+ name: translate('Absolute humidity'),
228
+ role: 'value.humidity.absolute',
229
+ unit: 'g/m³',
230
+ });
231
+ await this.ensureStateObject(device, STATE_DEW_POINT_TEMPERATURE, {
232
+ name: translate('Dew point temperature'),
233
+ role: 'value.temperature.dewpoint',
234
+ unit: '°C',
235
+ });
236
+
237
+ if (device.createTemperatureState) {
238
+ await this.ensureStateObject(device, STATE_TEMPERATURE, {
239
+ name: translate('Temperature'),
240
+ role: 'value.temperature',
241
+ unit: '°C',
242
+ });
243
+ } else {
244
+ await this.deleteObjectIfExists(`${this.getDeviceObjectId(device.id)}.${STATE_TEMPERATURE}`);
245
+ }
246
+
247
+ if (device.createRelativeHumidityState) {
248
+ await this.ensureStateObject(device, STATE_RELATIVE_HUMIDITY, {
249
+ name: translate('Relative humidity'),
250
+ role: 'value.humidity',
251
+ unit: '%',
252
+ });
253
+ } else {
254
+ await this.deleteObjectIfExists(`${this.getDeviceObjectId(device.id)}.${STATE_RELATIVE_HUMIDITY}`);
255
+ }
256
+ await this.deleteObjectIfExists(`${this.getDeviceObjectId(device.id)}.sources`, { recursive: true });
257
+ }
258
+
259
+ /**
260
+ * @param {Record<string, any>} device
261
+ * @param {string} stateName
262
+ * @param {{ name: ioBroker.StringOrTranslated; role: string; unit: string }} common
263
+ */
264
+ async ensureStateObject(device, stateName, common) {
265
+ const stateId = `${this.getDeviceObjectId(device.id)}.${stateName}`;
266
+
267
+ await this.setObjectNotExistsAsync(stateId, {
268
+ type: 'state',
269
+ common: {
270
+ name: common.name,
271
+ type: 'number',
272
+ role: common.role,
273
+ read: true,
274
+ write: false,
275
+ unit: common.unit,
276
+ },
277
+ native: {},
278
+ });
279
+ await this.extendObjectAsync(stateId, {
280
+ common: {
281
+ name: common.name,
282
+ type: 'number',
283
+ role: common.role,
284
+ read: true,
285
+ write: false,
286
+ unit: common.unit,
287
+ },
288
+ });
289
+ }
290
+
291
+ /**
292
+ * @param {Record<string, any>} oldDevice
293
+ * @param {Record<string, any>} newDevice
294
+ */
295
+ async deleteObsoleteDeviceStates(oldDevice, newDevice) {
296
+ if (oldDevice.createTemperatureState && !newDevice.createTemperatureState) {
297
+ await this.deleteObjectIfExists(`${this.getDeviceObjectId(newDevice.id)}.${STATE_TEMPERATURE}`);
298
+ }
299
+
300
+ if (oldDevice.createRelativeHumidityState && !newDevice.createRelativeHumidityState) {
301
+ await this.deleteObjectIfExists(`${this.getDeviceObjectId(newDevice.id)}.${STATE_RELATIVE_HUMIDITY}`);
302
+ }
303
+ }
304
+
305
+ /**
306
+ * @param {string} id
307
+ * @param {Record<string, unknown>} [options]
308
+ */
309
+ async deleteObjectIfExists(id, options) {
310
+ const object = await this.getObjectAsync(id);
311
+
312
+ if (object) {
313
+ await this.delObjectAsync(id, options);
314
+ }
315
+ }
316
+
317
+ async refreshSubscriptions() {
318
+ for (const stateId of this.subscribedSourceIds) {
319
+ this.unsubscribeForeignStates(stateId);
320
+ }
321
+
322
+ this.subscribedSourceIds.clear();
323
+
324
+ for (const device of this.getConfiguredDevices()) {
325
+ this.subscribedSourceIds.add(device.temperatureStateId);
326
+ this.subscribedSourceIds.add(device.relativeHumidityStateId);
327
+ }
328
+
329
+ for (const stateId of this.subscribedSourceIds) {
330
+ this.subscribeForeignStates(stateId);
331
+ }
332
+ }
333
+
334
+ async updateAllDevices() {
335
+ for (const device of this.getConfiguredDevices()) {
336
+ await this.updateDeviceValues(device);
337
+ }
338
+ }
339
+
340
+ // If object subscriptions are needed later, enable the constructor hook and call this.subscribeObjects(...).
341
+ // /**
342
+ // * Is called if a subscribed object changes
343
+ // *
344
+ // * @param {string} id - Object ID
345
+ // * @param {ioBroker.Object | null | undefined} obj - Object
346
+ // */
347
+ // onObjectChange(id, obj) {
348
+ // if (obj) {
349
+ // this.log.debug(`object ${id} changed: ${JSON.stringify(obj)}`);
350
+ // } else {
351
+ // this.log.debug(`object ${id} deleted`);
352
+ // }
353
+ // }
354
+
355
+ /**
356
+ * @param {string} id
357
+ * @param {ioBroker.State | null | undefined} state
358
+ */
359
+ async onStateChange(id, state) {
360
+ if (!state) {
361
+ return;
362
+ }
363
+
364
+ const affectedDevices = this.getConfiguredDevices().filter(
365
+ device => device.temperatureStateId === id || device.relativeHumidityStateId === id,
366
+ );
367
+
368
+ for (const device of affectedDevices) {
369
+ await this.updateDeviceValues(device);
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Is called if a message is sent to this adapter instance.
375
+ *
376
+ * @param {ioBroker.Message} obj - Message object
377
+ */
378
+ onMessage(obj) {
379
+ if (obj.command?.startsWith('dm:')) {
380
+ // Device Manager messages are handled by @iobroker/dm-utils.
381
+ return;
382
+ }
383
+
384
+ if (typeof obj === 'object' && obj.message) {
385
+ this.log.debug(`Unhandled message command: ${obj.command}`);
386
+ }
387
+ }
388
+
389
+ /**
390
+ * @param {Record<string, any>} device
391
+ */
392
+ async updateDeviceValues(device) {
393
+ const temperature = await this.readNumberState(device.temperatureStateId);
394
+ const relativeHumidity = await this.readNumberState(device.relativeHumidityStateId);
395
+ const absoluteHumidity = calculateAbsoluteHumidity(temperature, relativeHumidity);
396
+ const dewPointTemperature = calculateDewPointTemperature(temperature, relativeHumidity);
397
+ const deviceObjectId = this.getDeviceObjectId(device.id);
398
+
399
+ if (device.createTemperatureState) {
400
+ await this.setStateChangedAsync(`${deviceObjectId}.${STATE_TEMPERATURE}`, {
401
+ val: temperature,
402
+ ack: true,
403
+ });
404
+ }
405
+
406
+ if (device.createRelativeHumidityState) {
407
+ await this.setStateChangedAsync(`${deviceObjectId}.${STATE_RELATIVE_HUMIDITY}`, {
408
+ val: relativeHumidity,
409
+ ack: true,
410
+ });
411
+ }
412
+
413
+ await this.setStateChangedAsync(`${deviceObjectId}.${STATE_ABSOLUTE_HUMIDITY}`, {
414
+ val: absoluteHumidity,
415
+ ack: true,
416
+ });
417
+ await this.setStateChangedAsync(`${deviceObjectId}.${STATE_DEW_POINT_TEMPERATURE}`, {
418
+ val: dewPointTemperature,
419
+ ack: true,
420
+ });
421
+ }
422
+
423
+ /**
424
+ * @param {string} stateId
425
+ */
426
+ async readNumberState(stateId) {
427
+ const state = await this.getForeignStateAsync(stateId);
428
+ const value = Number(state?.val);
429
+
430
+ return Number.isFinite(value) ? value : null;
431
+ }
432
+
433
+ /**
434
+ * @param {string} id
435
+ */
436
+ getDeviceObjectId(id) {
437
+ return `${DEVICE_ROOT}.${id}`;
438
+ }
439
+
440
+ /**
441
+ * Is called when adapter shuts down - callback has to be called under any circumstances!
442
+ *
443
+ * @param {() => void} callback - Callback function
444
+ */
445
+ onUnload(callback) {
446
+ try {
447
+ callback();
448
+ } catch (error) {
449
+ this.log.error(`Error during unloading: ${error.message}`);
450
+ callback();
451
+ }
452
+ }
453
+ }
454
+
455
+ if (require.main !== module) {
456
+ /**
457
+ * @param {Partial<utils.AdapterOptions>} [options]
458
+ */
459
+ module.exports = options => new Absolutehumidity(options);
460
+ } else {
461
+ new Absolutehumidity();
462
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "iobroker.absolutehumidity",
3
+ "version": "0.0.1",
4
+ "description": "build absolute humidity from actual temperature and relative humidity",
5
+ "author": {
6
+ "name": "BenAhrdt",
7
+ "email": "github@ben-schmidt.net"
8
+ },
9
+ "contributors": [
10
+ {
11
+ "name": "J-Paul0815"
12
+ }
13
+ ],
14
+ "homepage": "https://github.com/BenAhrdt/ioBroker.absolutehumidity",
15
+ "license": "MIT",
16
+ "keywords": [
17
+ "absolute Humidity",
18
+ "dewpoint temperature"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git@github.com:BenAhrdt/ioBroker.absolutehumidity.git"
23
+ },
24
+ "engines": {
25
+ "node": ">= 20"
26
+ },
27
+ "dependencies": {
28
+ "@iobroker/adapter-core": "^3.4.1",
29
+ "@iobroker/dm-utils": "^3.1.1"
30
+ },
31
+ "devDependencies": {
32
+ "@alcalzone/release-script": "^5.2.1",
33
+ "@alcalzone/release-script-plugin-iobroker": "^5.2.0",
34
+ "@alcalzone/release-script-plugin-license": "^5.2.0",
35
+ "@alcalzone/release-script-plugin-manual-review": "^5.2.0",
36
+ "@iobroker/adapter-dev": "^1.5.0",
37
+ "@iobroker/dev-server": "^0.8.0",
38
+ "@iobroker/eslint-config": "^2.3.4",
39
+ "@iobroker/testing": "^5.2.2",
40
+ "@tsconfig/node20": "^20.1.9",
41
+ "@types/iobroker": "npm:@iobroker/types@^7.2.2",
42
+ "@types/node": "^20.19.43",
43
+ "typescript": "~5.9.3"
44
+ },
45
+ "main": "main.js",
46
+ "files": [
47
+ "admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
48
+ "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
49
+ "lib/",
50
+ "www/",
51
+ "io-package.json",
52
+ "LICENSE",
53
+ "main.js"
54
+ ],
55
+ "scripts": {
56
+ "test:js": "mocha --config test/mocharc.custom.json \"{!(node_modules|test)/**/*.test.js,*.test.js,test/**/test!(PackageFiles|Startup).js}\"",
57
+ "test:package": "mocha test/package --exit",
58
+ "test:integration": "mocha test/integration --exit",
59
+ "test": "npm run test:js && npm run test:package",
60
+ "check": "tsc --noEmit -p tsconfig.check.json",
61
+ "lint": "eslint -c eslint.config.mjs .",
62
+ "translate": "translate-adapter",
63
+ "release": "release-script",
64
+ "dev-server": "dev-server"
65
+ },
66
+ "bugs": {
67
+ "url": "https://github.com/BenAhrdt/ioBroker.absolutehumidity/issues"
68
+ },
69
+ "readmeFilename": "README.md"
70
+ }