iobroker.luftdaten
Version:
ioBroker Luftdaten Adapter
538 lines (477 loc) • 25.9 kB
JavaScript
'use strict';
const utils = require('@iobroker/adapter-core');
const axios = require('axios').default;
const adapterName = require('./package.json').name.split('.').pop();
axios.interceptors.request.use((x) => {
x.meta = x.meta || {};
x.meta.requestStartedAt = new Date().getTime();
return x;
});
axios.interceptors.response.use((x) => {
x.responseTime = new Date().getTime() - x.config.meta.requestStartedAt;
return x;
});
class Luftdaten extends utils.Adapter {
constructor(options) {
super({
...options,
name: adapterName,
});
this.on('ready', this.onReady.bind(this));
this.on('unload', this.onUnload.bind(this));
}
async onReady() {
const sensorsAll = [];
const sensorsKeep = [];
try {
const devices = await this.getDevicesAsync();
if (devices && devices.length) {
for (let i = 0; i < devices.length; i++) {
const idNoNamespace = this.removeNamespace(devices[i]._id);
// Check if the state is a direct child (device)
if (idNoNamespace.indexOf('.') === -1) {
sensorsAll.push(idNoNamespace);
this.log.debug(`[onReady] sensor deviceId exists: "${idNoNamespace}"`);
}
}
}
const sensors = this.config.sensors;
let successfullyFilled = 0;
if (sensors && Array.isArray(sensors)) {
this.log.debug(`[onReady] found ${sensors.length} sensors in configuration, requesting data`);
for (const s in sensors) {
const sensorIndex = parseInt(s) + 1;
const sensor = sensors[s];
const sensorIdentifier = sensor.identifier;
const deviceId = sensor.type == 'local' ? sensorIdentifier.replace(/\./g, '_') : sensorIdentifier.replace(/\D/g, '');
if (deviceId) {
sensorsKeep.push(deviceId);
this.log.debug(`[onReady] sensor ${sensorIndex}/${sensors.length} with idenfitier "${sensorIdentifier}" will be saved as deviceId "${deviceId}"`);
try {
const responseTime = await this.fillSensorData(deviceId, sensor);
this.log.debug(`[onReady] sensor ${sensorIndex}/${sensors.length} - data of deviceId "${deviceId}" filled in ${responseTime / 1000}s`);
successfullyFilled++;
} catch (err) {
this.log.debug(`[onReady] sensor ${sensorIndex}/${sensors.length} - error of deviceId "${deviceId}": ${err}`);
}
} else {
this.log.error(`[onReady] sensor ${sensorIndex}/${sensors.length} identifier missing or invalid: "${sensorIdentifier}" - check instance configuration`);
}
}
this.log.debug(`[onReady] successfully filled ${successfullyFilled} of ${sensors.length} sensors`);
} else {
this.log.error('[onReady] no sensors configured - check instance configuration');
}
// Delete non existent sensors
for (let i = 0; i < sensorsAll.length; i++) {
const id = sensorsAll[i];
if (sensorsKeep.indexOf(id) === -1) {
await this.delObjectAsync(id, { recursive: true });
this.log.debug(`[onReady] deleted deviceId: "${id}"`);
}
}
} catch (err) {
this.log.error(`[onReady] error: ${err}`);
} finally {
this.log.debug(`[onReady] finished - stopping instance`);
this.stop();
}
}
async fillSensorData(deviceId, sensor) {
const sensorType = sensor.type;
const sensorName = sensor.name === '' ? sensor.identifier : sensor.name;
this.log.debug(`[fillSensorData] sensor "${sensorName}" with type: "${sensorType}", identifier: "${sensor.identifier}", deviceId: "${deviceId}"`);
const unitList = {
P1: 'µg/m³',
P2: 'µg/m³',
temperature: '°C',
humidity: '%',
pressure: 'Pa',
pressure_at_sealevel: 'Pa',
noise: 'dB(A)',
signal: 'dB(A)',
min_micro: 'µs',
max_micro: 'µs',
};
const roleList = {
P1: 'value.ppm',
P2: 'value.ppm',
temperature: 'value.temperature',
humidity: 'value.humidity',
pressure: 'value.pressure',
pressure_at_sealevel: 'value.pressure',
noise: 'value',
signal: 'value',
min_micro: 'value',
max_micro: 'value',
};
await this.setObjectNotExistsAsync(deviceId, {
type: 'device',
common: {
name: sensorName,
},
native: {},
});
await this.extendObjectAsync(deviceId, {
common: {
name: sensorName,
},
});
await this.setObjectNotExistsAsync(`${deviceId}.name`, {
type: 'state',
common: {
name: {
en: 'Sensor name',
de: 'Sensorname',
ru: 'Имя датчика',
pt: 'Nome do sensor',
nl: 'Sensornaam',
fr: 'Nom du capteur',
it: 'Nome del sensore',
es: 'Nombre del sensor',
pl: 'Nazwa czujnika',
uk: 'Назва датчика',
'zh-cn': '传感器名称',
},
type: 'string',
role: 'text',
read: true,
write: false,
},
native: {},
});
await this.setStateChangedAsync(`${deviceId}.name`, { val: sensorName, ack: true });
await this.setObjectNotExistsAsync(`${deviceId}.responseCode`, {
type: 'state',
common: {
name: {
en: 'Response Code',
de: 'Antwortcode',
ru: 'Код ответа',
pt: 'Código de resposta',
nl: 'Reactiecode',
fr: 'Code de réponse',
it: 'Codice di risposta',
es: 'Código de respuesta',
pl: 'Kod odpowiedzi',
uk: 'Код відгуку',
'zh-cn': '响应代码',
},
type: 'number',
role: 'value',
read: true,
write: false,
},
native: {},
});
return new Promise((resolve, reject) => {
if (sensorType == 'local') {
const sensorUrl = `http://${sensor.identifier}/data.json`;
this.log.debug(`[fillSensorData] local request started (timeout ${this.config.requestTimeout}s): ${sensorUrl}`);
const source = axios.CancelToken.source();
const abortTimeout = this.setTimeout(() => {
this.log.debug(`[fillSensorData] local request takes too much time - aborting ...`);
source.cancel();
}, this.config.requestTimeout * 1000);
axios({
method: 'get',
url: sensorUrl,
timeout: this.config.requestTimeout * 1000,
cancelToken: source.token,
responseType: 'json',
})
.then(async (response) => {
this.clearTimeout(abortTimeout);
const content = response.data;
this.log.debug(`[fillSensorData] local request done after ${response.responseTime / 1000}s - received data (${response.status}): ${JSON.stringify(content)}`);
await this.setStateAsync(`${deviceId}.responseCode`, { val: response.status, ack: true });
if (content && Object.prototype.hasOwnProperty.call(content, 'sensordatavalues')) {
for (const key in content.sensordatavalues) {
const obj = content.sensordatavalues[key];
let unit = null;
let role = 'value';
if (obj.value_type.indexOf('SDS_') == 0) {
unit = 'µg/m³';
role = 'value.ppm';
} else if (obj.value_type.indexOf('temperature') >= 0) {
unit = '°C';
role = 'value.temperature';
} else if (obj.value_type.indexOf('humidity') >= 0) {
unit = '%';
role = 'value.humidity';
} else if (obj.value_type.indexOf('pressure') >= 0) {
unit = 'Pa';
role = 'value.pressure';
} else if (obj.value_type.indexOf('noise') >= 0) {
unit = 'dB(A)';
role = 'value';
} else if (Object.prototype.hasOwnProperty.call(unitList, obj.value_type)) {
unit = unitList[obj.value_type];
role = roleList[obj.value_type];
}
await this.setObjectNotExistsAsync(`${deviceId}.${obj.value_type}`, {
type: 'state',
common: {
name: obj.value_type,
type: 'number',
role: role,
unit: unit,
read: true,
write: false,
},
native: {},
});
await this.setStateChangedAsync(`${deviceId}.${obj.value_type}`, { val: parseFloat(obj.value), ack: true });
}
}
resolve(response.responseTime);
})
.catch(async (error) => {
if (axios.isCancel(error)) {
await this.setStateAsync(`${deviceId}.responseCode`, { val: -2, ack: true });
} else if (error.response) {
// The request was made and the server responded with a status code
this.log.warn(
`[fillSensorData] received error ${error.response.status} response from local sensor ${sensor.identifier} with content: ${JSON.stringify(error.response.data)}`,
);
await this.setStateAsync(`${deviceId}.responseCode`, { val: error.response.status, ack: true });
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js<div></div>
this.log.info(`[fillSensorData] error: ${error.message}`);
await this.setStateAsync(`${deviceId}.responseCode`, { val: -1, ack: true });
} else {
// Something happened in setting up the request that triggered an Error
this.log.info(`[fillSensorData] error: ${error.message}`);
await this.setStateAsync(`${deviceId}.responseCode`, { val: -99, ack: true });
}
reject('http error');
});
} else if (sensorType == 'remote') {
const sensorUrl = `https://data.sensor.community/airrohr/v1/sensor/${sensor.identifier.replace(/\D/g, '')}/`;
this.log.debug(`[fillSensorData] remote request started (timeout ${this.config.requestTimeout}s): ${sensorUrl}`);
const source = axios.CancelToken.source();
const abortTimeout = this.setTimeout(() => {
this.log.debug(`[fillSensorData] remote request takes too much time - aborting ...`);
source.cancel();
}, this.config.requestTimeout * 1000);
axios({
method: 'get',
url: sensorUrl,
timeout: this.config.requestTimeout * 1000,
cancelToken: source.token,
responseType: 'json',
})
.then(async (response) => {
this.clearTimeout(abortTimeout);
const content = response.data;
this.log.debug(`[fillSensorData] remote request done after ${response.responseTime / 1000}s - received data (${response.status}): ${JSON.stringify(content)}`);
await this.setStateAsync(`${deviceId}.responseCode`, { val: response.status, ack: true });
if (content && Array.isArray(content) && content.length > 0) {
const sensorData = content[0];
if (sensorData && Object.prototype.hasOwnProperty.call(sensorData, 'sensordatavalues')) {
for (const key in sensorData.sensordatavalues) {
const obj = sensorData.sensordatavalues[key];
let unit = null;
let role = 'value';
if (obj.value_type.indexOf('noise') >= 0) {
unit = 'dB(A)';
role = 'value';
} else if (Object.prototype.hasOwnProperty.call(unitList, obj.value_type)) {
unit = unitList[obj.value_type];
role = roleList[obj.value_type];
}
await this.setObjectNotExistsAsync(`${deviceId}.SDS_${obj.value_type}`, {
type: 'state',
common: {
name: obj.value_type,
type: 'number',
role: role,
unit: unit,
read: true,
write: false,
},
native: {},
});
await this.setStateChangedAsync(`${deviceId}.SDS_${obj.value_type}`, { val: parseFloat(obj.value), ack: true });
}
}
if (Object.prototype.hasOwnProperty.call(sensorData, 'location')) {
await this.setObjectNotExistsAsync(`${deviceId}.location`, {
type: 'channel',
common: {
name: {
en: 'Location',
de: 'Standort',
ru: 'Место нахождения',
pt: 'Localização',
nl: 'Plaats',
fr: 'Emplacement',
it: 'Posizione',
es: 'Localización',
pl: 'Lokalizacja',
uk: 'Місцезнаходження',
'zh-cn': '地点',
},
role: 'value.gps',
},
native: {},
});
await this.setObjectNotExistsAsync(`${deviceId}.location.longitude`, {
type: 'state',
common: {
name: {
en: 'Longtitude',
de: 'Längengrad',
ru: 'Долгота',
pt: 'Longitude',
nl: 'lengtegraad',
fr: 'Longitude',
it: 'longitudine',
es: 'Longitud',
pl: 'Długość geograficzna',
uk: 'Довгота',
'zh-cn': '经度',
},
type: 'number',
role: 'value.gps.longitude',
unit: '°',
read: true,
write: false,
},
native: {},
});
await this.setStateChangedAsync(`${deviceId}.location.longitude`, { val: parseFloat(sensorData.location.longitude), ack: true });
await this.setObjectNotExistsAsync(`${deviceId}.location.latitude`, {
type: 'state',
common: {
name: {
en: 'Latitude',
de: 'Breite',
ru: 'Широта',
pt: 'Latitude',
nl: 'Breedtegraad',
fr: 'Latitude',
it: 'Latitudine',
es: 'Latitud',
pl: 'Szerokość',
uk: 'Прованс',
'zh-cn': '纬度',
},
type: 'number',
role: 'value.gps.latitude',
unit: '°',
read: true,
write: false,
},
native: {},
});
await this.setStateChangedAsync(`${deviceId}.location.latitude`, { val: parseFloat(sensorData.location.latitude), ack: true });
await this.setObjectNotExistsAsync(`${deviceId}.location.altitude`, {
type: 'state',
common: {
name: {
en: 'Altitude',
de: 'Höhe',
ru: 'Высота',
pt: 'Altitude',
nl: 'Hoogte',
fr: 'Altitude',
it: 'Altitudine',
es: 'Altitud',
pl: 'Wysokość',
uk: 'Сортування',
'zh-cn': '高度',
},
type: 'number',
role: 'value.gps.elevation',
unit: 'm',
read: true,
write: false,
},
native: {},
});
await this.setStateChangedAsync(`${deviceId}.location.altitude`, { val: parseFloat(sensorData.location.altitude), ack: true });
}
await this.setObjectNotExistsAsync(`${deviceId}.timestamp`, {
type: 'state',
common: {
name: {
en: 'Last Update',
de: 'Letztes Update',
ru: 'Последнее обновление',
pt: 'Última atualização',
nl: 'Laatste update',
fr: 'Dernière mise à jour',
it: 'Ultimo aggiornamento',
es: 'Última actualización',
pl: 'Ostatnia aktualizacja',
uk: 'Останнє оновлення',
'zh-cn': '最后更新',
},
type: 'number',
role: 'date',
read: true,
write: false,
},
native: {},
});
await this.setStateChangedAsync(`${deviceId}.timestamp`, { val: new Date(sensorData.timestamp).getTime(), ack: true });
}
resolve(response.responseTime);
})
.catch(async (error) => {
if (axios.isCancel(error)) {
await this.setStateAsync(`${deviceId}.responseCode`, { val: -2, ack: true });
} else if (error.response) {
// The request was made and the server responded with a status code
this.log.warn(
`[fillSensorData] received error ${error.response.status} response from remote sensor ${sensor.identifier} with content: ${JSON.stringify(error.response.data)}`,
);
await this.setStateAsync(`${deviceId}.responseCode`, { val: error.response.status, ack: true });
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
this.log.info(`[fillSensorData] error: ${error.message}`);
await this.setStateAsync(`${deviceId}.responseCode`, { val: -1, ack: true });
} else {
// Something happened in setting up the request that triggered an Error
this.log.info(`[fillSensorData] error: ${error.message}`);
await this.setStateAsync(`${deviceId}.responseCode`, { val: -99, ack: true });
}
reject('http error');
});
} else {
reject('unknown sensor type');
}
});
}
removeNamespace(id) {
const re = new RegExp(this.namespace + '*\\.', 'g');
return id.replace(re, '');
}
/**
* @param {() => void} callback
*/
onUnload(callback) {
try {
this.log.debug('cleaned everything up...');
callback();
} catch {
callback();
}
}
}
// @ts-ignore parent is a valid property on module
if (module.parent) {
// Export the constructor in compact mode
/**
* @param {Partial<ioBroker.AdapterOptions>} [options={}]
*/
module.exports = (options) => new Luftdaten(options);
} else {
// otherwise start the instance directly
new Luftdaten();
}