iobroker.dysonairpurifier
Version:
dyson air purifiers and fans
1,180 lines (1,153 loc) • 66.1 kB
JavaScript
// @ts-check
'use strict';
/**
* Data for the current device which are not provided by Web-API (IP-Address, MQTT-Password)
*
* Serial Serial number of the device
*
* ProductType Product type of the device
*
*/
// The adapter-core module gives you access to the core ioBroker functions
// you need to create an adapter
const utils = require('@iobroker/adapter-core');
const adapterName = require('./package.json').name.split('.').pop() || '';
// Load additional modules
const mqtt = require('mqtt');
// Load utils for this adapter
const dysonUtils = require('./dyson-utils.js');
const { getDatapoint, PRODUCTS, SPECIAL_PROPERTIES, getNameToDysoncodeTranslation } = require('./dysonConstants.js');
const { setInterval } = require('node:timers');
// Variable definitions
// let adapter = null;
let adapterIsSetUp = false;
let devices = [];
let VOC = 0; // Numeric representation of current VOCIndex
let PM25 = 0; // Numeric representation of current PM25Index
let PM10 = 0; // Numeric representation of current PM10Index
let Dust = 0; // Numeric representation of current DustIndex
/**
*
* @param number The number to be clamped
* @param min The minimum value
* @param max The maximum value
* @returns The clamped number
*/
function clamp(number, min, max) {
return Math.max(min, Math.min(number, max));
}
/**
* Main class of dyson AirPurifier adapter for ioBroker
*/
class dysonAirPurifier extends utils.Adapter {
/**
* @param options - Adapter configuration
*/
constructor(options = { temperatureUnit: 'C' }) {
super({ ...options, name: adapterName });
// this.on('objectChange', this.onObjectChange.bind(this));
this.on('message', this.onMessage.bind(this));
this.on('ready', this.onReady.bind(this));
this.on('stateChange', this.onStateChange.bind(this));
this.on('unload', this.onUnload.bind(this));
}
/**
* onMessage
*
* Some message was sent to this instance over message box. Used by email, pushover, text2speech, ...
* Using this method requires "common.messagebox" property to be set to true in io-package.json
* This function exchanges information between the admin frontend and the backend.
* In detail: it performs the 2 FA login at the dyson API. Therefore it receives messages from admin,
* sends them to dyson and reaches the received data back to admin.
*
* @param msg - Message object containing all necessary data to request the needed information
*/
async onMessage(msg) {
if (!msg?.callback || !msg?.from?.startsWith('system.adapter.admin')) {
return;
}
switch (msg.command) {
case 'getDyson2faMail':
this.log.debug('OnMessage: Received getDyson2faMail request.');
msg.message.locale = dysonUtils.getDyson2faLocale(msg.message.country);
try {
const response = await dysonUtils.getDyson2faMail(
this,
msg.message.email,
msg.message.password,
msg.message.country,
msg.message.locale,
);
this.sendTo(msg.from, msg.command, response, msg.callback);
} catch (error) {
this.log.warn(`Couldn't handle getDyson2faMail message: ${error}`);
this.sendTo(msg.from, msg.command, { error: error || 'No data' }, msg.callback);
}
break;
case 'getDysonToken':
this.log.debug('OnMessage: getting Dyson-Token');
try {
const response = await dysonUtils.getDysonToken(
this,
msg.message.email,
msg.message.password,
msg.message.country,
msg.message.challengeId,
msg.message.PIN,
);
this.sendTo(msg.from, msg.command, response, msg.callback);
} catch (error) {
this.log.warn(`Couldn't handle getDysonToken message: ${error}`);
this.sendTo(msg.from, msg.command, { error: error || 'No data' }, msg.callback);
}
break;
default:
this.log.warn(`Unknown message: ${msg.command}`);
break;
}
}
/**
* @param dysonAction - The action to be performed
* @param messageValue - The value to be set
* @param id - The id of the datapoint that was changed
* @param state - The new state-object of the datapoint after change
* @returns The message data
*/
async #getMessageData(dysonAction, messageValue, id, state) {
switch (dysonAction) {
case 'fnsp': {
// protect upper and lower speed limit of fan
const value = parseInt(typeof messageValue === 'string' ? messageValue : messageValue.toString(), 10);
const clamped = clamp(value, 1, 10);
return { [dysonAction]: clamped.toString().padStart(4, '0') };
}
case 'hmax': {
// Target temperature for heating in KELVIN!
// convert temperature to configured unit
let value = parseInt(typeof messageValue === 'string' ? messageValue : messageValue.toString(), 10);
// @ts-expect-error - temperatureUnit is defined in the adapter configuration
switch (this.config.temperatureUnit) {
case 'K':
value *= 10;
break;
case 'C':
value = Number((value + 273.15) * 10);
break;
case 'F':
value = Number((value - 32) * (9 / 5) + 273.15);
break;
}
return { [dysonAction]: value.toFixed(0).padStart(4, '0') };
}
case 'ancp':
case 'osal':
case 'osau':
try {
const result = await dysonUtils.getAngles(this, dysonAction, id, state);
this.log.debug(`Result of getAngles: ${JSON.stringify(result)}`);
result.osal = parseInt(result.osal.val);
result.osau = parseInt(result.osau.val);
switch (result.ancp.val) {
case 'CUST':
result.ancp = result.osau - result.osal;
break;
case 'BRZE':
result.ancp = 'BRZE';
break;
default:
result.ancp = parseInt(result.ancp.val);
}
if (result.ancp === 'BRZE') {
return {
['ancp']: 'BRZE',
['oson']: 'ON',
};
}
this.log.debug(
`Result of parseInt(result.ancp.val): ${result.ancp}, typeof: ${typeof result.ancp}`,
);
if (result.osal + result.ancp > 355) {
result.osau = 355;
result.osal = 355 - result.ancp;
} else if (result.osau - result.ancp < 5) {
result.osal = 5;
result.osau = 5 + result.ancp;
} else {
result.osau = result.osal + result.ancp;
}
return {
['osal']: dysonUtils.zeroFill(result.osal, 4),
['osau']: dysonUtils.zeroFill(result.osau, 4),
// ['ancp']: 'CUST',
['ancp']: await this.getOscillationAngle(result.ancp),
['oson']: 'ON',
};
} catch (error) {
this.log.error('An error occurred while trying to retrieve the oscillation angles.');
throw error;
}
default:
return {
[dysonAction]:
typeof messageValue === 'number' ? messageValue.toString().padStart(4, '0') : messageValue,
};
}
}
async getOscillationAngle(angle) {
if (angle === 0) {
return '0000';
} else if (angle <= 45) {
return '0045';
} else if (angle > 45 && angle <= 90) {
return '0090';
} else if (angle > 180 && angle <= 270) {
return '0180';
} else if (angle > 270 && angle <= 355) {
return '0350';
}
}
/**
* onStateChange
*
* Sends the control mqtt message to your device in case you changed a value
*
* @param id - id of the datapoint that was changed
* @param state - new state-object of the datapoint after change
*/
async onStateChange(id, state) {
const thisDevice = id.split('.')[2];
const action = id.split('.').pop();
// Warning, state can be null if it was deleted
if (!state || !action) {
return;
}
// state changes by hardware or adapter depending on hardware values
// check if it is an Index calculation
if (state.ack) {
if (!action.includes('Index')) {
return;
}
// if some index has changed recalculate overall AirQuality
this.createOrExtendObject(
`${thisDevice}.AirQuality`,
{
type: 'state',
common: {
name: 'Overall AirQuality (worst value of all indexes except NO2)',
read: true,
write: false,
role: 'value',
type: 'number',
states: {
0: 'Good',
1: 'Medium',
2: 'Bad',
3: 'very Bad',
4: 'extremely Bad',
5: 'worrying',
},
},
native: {},
},
Math.max(VOC, Dust, PM25, PM10),
);
return;
}
// if dysonAction is undefined it's an adapter internal action and has to be handled with the given Name
// pick the dyson internal Action from the result row
const dysonAction = getNameToDysoncodeTranslation(action);
if (!dysonAction) {
this.log.warn(`Unknown Dyson Action ${action}`);
return;
}
// you can use the ack flag to detect if it is status (true) or command (false)
// get the whole data field array
const ActionData = getDatapoint(dysonAction);
const value = state.val;
let messageData = await this.#getMessageData(dysonAction, value, id, state);
// TODO: refactor
// switches defined as boolean must get the proper value to be send
// this is to translate between the needed states for ioBroker and the device
// boolean switches are better for visualizations and other adapters like text2command
if (typeof ActionData !== 'undefined') {
if (ActionData.type === 'boolean' && ActionData.role.startsWith('switch')) {
// current state is TRUE!
if (state.val) {
// handle special action "humidification" where ON is not ON but HUME
if (dysonAction === 'hume') {
messageData = { [dysonAction]: 'HUMD' };
// handle special action "HeatingMode" where ON is not ON but HEAT
} else if (dysonAction === 'hmod') {
messageData = { [dysonAction]: 'HEAT' };
} else {
messageData = { [dysonAction]: 'ON' };
}
} else {
messageData = { [dysonAction]: 'OFF' };
}
}
}
// check whether fanspeed has been set to Auto
if ('fnsp' === dysonAction && 11 === value) {
messageData = { auto: 'ON' };
}
// only send to device if change should set a device value
if (action === 'Hostaddress') {
return;
}
// build the message to be sent to the device
const message = {
msg: 'STATE-SET',
time: new Date().toISOString(),
'mode-reason': 'LAPP',
'state-reason': 'MODE',
data: messageData,
};
for (const mqttDevice of devices) {
if (mqttDevice.Serial === thisDevice) {
this.log.debug(`MANUAL CHANGE: device [${thisDevice}] -> [${action}] -> [${state.val}], id: [${id}]`);
this.log.debug(`SENDING this data to device (${thisDevice}): ${JSON.stringify(message)}`);
await this.setState(id, state.val, true);
mqttDevice.mqttClient.publish(
`${mqttDevice.ProductType}/${thisDevice}/command`,
JSON.stringify(message),
);
// refresh data with a delay of 250 ms to avoid 30 Sec gap
setTimeout(() => {
this.log.debug(`requesting new state of device (${thisDevice}).`);
mqttDevice.mqttClient.publish(
`${mqttDevice.ProductType}/${thisDevice}/command`,
JSON.stringify({
msg: 'REQUEST-CURRENT-STATE',
time: new Date().toISOString(),
}),
);
}, 100);
}
}
}
/**
* CreateOrUpdateDevice
*
* Creates the base device information
*
* @param device - Data for the current device which are not provided by Web-API (IP-Address, MQTT-Password)
*/
async CreateOrUpdateDevice(device) {
try {
// create device folder
//this.log.debug('Creating device folder.');
this.createOrExtendObject(
device.Serial,
{
type: 'device',
common: {
name: PRODUCTS[device.ProductType].name,
icon: PRODUCTS[device.ProductType].icon,
type: 'string',
},
native: {},
},
null,
);
this.createOrExtendObject(
`${device.Serial}.Firmware`,
{
type: 'channel',
common: {
name: 'Information on devices firmware',
read: true,
write: false,
type: 'string',
role: 'value',
},
native: {},
},
null,
);
this.createOrExtendObject(
`${device.Serial}.SystemState`,
{
type: 'folder',
common: {
name: 'Information on devices system state (Filter, Water tank, ...)',
read: true,
write: false,
type: 'string',
role: 'value',
},
native: {},
},
null,
);
this.createOrExtendObject(
`${device.Serial}.SystemState.product-errors`,
{
type: 'channel',
common: {
name: 'Information on devices product errors - false=No error, true=Failure',
read: true,
write: false,
type: 'string',
role: 'value',
},
native: {},
},
null,
);
this.createOrExtendObject(
`${device.Serial}.SystemState.product-warnings`,
{
type: 'channel',
common: {
name: 'Information on devices product-warnings - false=No error, true=Failure',
read: true,
write: false,
type: 'string',
role: 'value',
},
native: {},
},
null,
);
this.createOrExtendObject(
`${device.Serial}.SystemState.module-errors`,
{
type: 'channel',
common: {
name: 'Information on devices module-errors - false=No error, true=Failure',
read: true,
write: false,
type: 'string',
role: 'value',
},
native: {},
},
null,
);
this.createOrExtendObject(
`${device.Serial}.SystemState.module-warnings`,
{
type: 'channel',
common: {
name: 'Information on devices module-warnings - false=No error, true=Failure',
read: true,
write: false,
type: 'string',
role: 'value',
},
native: {},
},
null,
);
this.createOrExtendObject(
`${device.Serial}.Firmware.Version`,
{
type: 'state',
common: {
name: 'Current firmware version',
read: true,
write: false,
role: 'value',
type: 'string',
},
native: {},
},
device.Version,
);
this.createOrExtendObject(
`${device.Serial}.Firmware.Autoupdate`,
{
type: 'state',
common: {
name: "Shows whether the device updates it's firmware automatically if update is available.",
read: true,
write: true,
role: 'indicator',
type: 'boolean',
},
native: {},
},
device.AutoUpdate,
);
this.createOrExtendObject(
`${device.Serial}.Firmware.NewVersionAvailable`,
{
type: 'state',
common: {
name: 'Shows whether a firmware update for this device is available online.',
read: true,
write: false,
role: 'indicator',
type: 'boolean',
},
native: {},
},
device.NewVersionAvailable,
);
this.createOrExtendObject(
`${device.Serial}.ProductType`,
{
type: 'state',
common: {
name: 'dyson internal productType.',
read: true,
write: false,
role: 'value',
type: 'string',
},
native: {},
},
device.ProductType,
);
this.createOrExtendObject(
`${device.Serial}.ConnectionType`,
{
type: 'state',
common: {
name: 'Type of connection.',
read: true,
write: false,
role: 'value',
type: 'string',
},
native: {},
},
device.ConnectionType,
);
this.createOrExtendObject(
`${device.Serial}.Name`,
{
type: 'state',
common: {
name: 'Name of device.',
read: true,
write: true,
role: 'value',
type: 'string',
},
native: {},
},
device.Name,
);
this.log.debug(`Querying Host-Address of device: ${device.Serial}`);
const hostAddress = await this.getStateAsync(`${device.Serial}.Hostaddress`);
this.log.debug(`Got Host-Address-object [${JSON.stringify(hostAddress)}] for device: ${device.Serial}`);
if (hostAddress?.val && typeof hostAddress.val === 'string') {
this.log.debug(
`Found predefined Host-Address [${hostAddress.val}] for device: ${device.Serial} in object tree.`,
);
device.hostAddress = hostAddress.val;
this.createOrExtendObject(
`${device.Serial}.Hostaddress`,
{
type: 'state',
common: {
name: 'Local host address (or IP) of device.',
read: true,
write: true,
role: 'value',
type: 'string',
},
native: {},
},
hostAddress.val,
);
} else {
// No valid IP address of device found. Without we can't proceed. So terminate adapter.
this.createOrExtendObject(
`${device.Serial}.Hostaddress`,
{
type: 'state',
common: {
name: 'Local host address (IP) of device.',
read: true,
write: true,
role: 'value',
type: 'string',
},
native: {},
},
'',
);
}
} catch (error) {
this.log.error(`[CreateOrUpdateDevice] Error: ${error}, Callstack: ${error.stack}`);
}
}
/**
* processMsg
*
* Processes the current received message and updates relevant data fields
*
* @param device additional data for the current device which are not provided by Web-API (IP-Address, MQTT-Password)
* @param path Additional subfolders can be given here if needed with a leading dot (eg. .Sensor)!
* @param message Current State of the device. Message is send by device via mqtt due to request or state change.
*/
async processMsg(device, path, message) {
for (const dysonCode in message) {
// Is this a "product-state" message?
if (dysonCode === 'product-state') {
await this.processMsg(device, '', message[dysonCode]);
return;
}
if (
['product-errors', 'product-warnings', 'module-errors', 'module-warnings'].includes(dysonCode)
//row === 'product-errors' ||
//row === 'product-warnings' ||
//row === 'module-errors' ||
//row === 'module-warnings'
) {
await this.processMsg(device, `${path}.${dysonCode}`, message[dysonCode]);
}
// Is this a "data" message?
if (dysonCode === 'data') {
await this.processMsg(device, '.Sensor', message[dysonCode]);
if (Object.prototype.hasOwnProperty.call(message[dysonCode], 'p25r')) {
this.createPM25(message, dysonCode, device);
}
if (Object.prototype.hasOwnProperty.call(message[dysonCode], 'p10r')) {
this.createPM10(message, dysonCode, device);
}
if (Object.prototype.hasOwnProperty.call(message[dysonCode], 'pact')) {
this.createDust(message, dysonCode, device);
}
if (Object.prototype.hasOwnProperty.call(message[dysonCode], 'vact')) {
this.createVOC(message, dysonCode, device);
}
if (Object.prototype.hasOwnProperty.call(message[dysonCode], 'va10')) {
this.createVOC(message, dysonCode, device);
}
if (Object.prototype.hasOwnProperty.call(message[dysonCode], 'noxl')) {
this.createNO2(message, dysonCode, device);
}
if (Object.prototype.hasOwnProperty.call(message[dysonCode], 'hchr')) {
this.createHCHO(message, dysonCode, device);
}
return;
}
// Handle all other message types
//this.log.debug(`Processing item [${JSON.stringify(row)}] of Message: ${((typeof message === 'object')? JSON.stringify(message) : message)}` );
const deviceConfig = getDatapoint(dysonCode);
if (deviceConfig === undefined) {
this.log.silly(
`Skipped creating unknown data field for Device:[${device.Serial}], Field: [${dysonCode}] Value:[${typeof message[dysonCode] === 'object' ? JSON.stringify(message[dysonCode]) : message[dysonCode]}]`,
);
continue;
}
if (deviceConfig.name === 'skip') {
this.log.silly(
`Skipped creating known but unused data field for Device:[${device.Serial}], Field: [${dysonCode}] Value:[${typeof message[dysonCode] === 'object' ? JSON.stringify(message[dysonCode]) : message[dysonCode]}]`,
);
continue;
}
// this.setDysonCode(deviceConfig, dysonUtils.getFieldRewrite(deviceConfig[0]));
// strip leading zeros from numbers
let value;
if (deviceConfig.type === 'number') {
value = parseInt(message[dysonCode], 10);
// TP02: When continuous monitoring is off and the fan is switched off - temperature and humidity loose their values.
// test whether the values are invalid and config.keepValues is true to prevent the old values from being destroyed
if (
dysonCode === 'rhtm' &&
message[dysonCode] === 'OFF' &&
// @ts-expect-error - keepValues is defined in the adapter configuration
this.config.keepValues
) {
continue;
}
// if field is sleep timer test for value OFF and remap it to a number
if (dysonCode === 'sltm' && message[dysonCode] === 'OFF') {
value = 0;
}
// if field is fan speed test for value AUTO and remap it to a number
if (dysonCode === 'fnsp' && message[dysonCode] === 'AUTO') {
value = 11;
}
if (dysonCode === 'filf') {
// create additional data field filterlifePercent converting value from hours to percent; 4300 is the estimated lifetime in hours by dyson
this.createOrExtendObject(
`${device.Serial + path}.FilterLifePercent`,
{
type: 'state',
common: {
name: deviceConfig.description,
read: true,
write: deviceConfig.writeable,
role: deviceConfig.role,
type: deviceConfig.type,
unit: '%',
states: deviceConfig.displayValues,
},
native: {},
},
Number((value * 100) / 4300),
);
}
if (dysonCode === 'vact' || dysonCode === 'va10' || dysonCode === 'noxl') {
value = Math.floor(value / 10);
}
if (dysonCode === 'hchr') {
value = value / 1000;
}
} else if (deviceConfig.role === 'value.temperature') {
// TP02: When continuous monitoring is off and the fan ist switched off - temperature and humidity loose their values.
// test whether the values are invalid and config.keepValues is true to prevent the old values from being destroyed
if (
message[dysonCode] === 'OFF' &&
// @ts-expect-error - keepValues is defined in the adapter configuration
this.config.keepValues
) {
continue;
}
value = parseInt(message[dysonCode], 10);
// convert temperature to configured unit
// @ts-expect-error - temperatureUnit is defined in the adapter configuration
switch (this.config.temperatureUnit) {
case 'K':
deviceConfig.unit = 'K';
value /= 10;
break;
case 'C':
deviceConfig.unit = '°C';
// OLD: deviceConfig[6] = '°' + this.config.temperatureUnit;
value = Number(value / 10 - 273.15).toFixed(2);
break;
case 'F':
deviceConfig.unit = '°F';
// OLD: deviceConfig[6] = '°' + this.config.temperatureUnit;
value = Number((value / 10 - 273.15) * (9 / 5) + 32).toFixed(2);
break;
}
} else if (deviceConfig.type === 'boolean' && deviceConfig.role.startsWith('switch')) {
// testValue should be the 2nd value in an array or if it's no array, the value itself
const testValue = typeof message[dysonCode] === 'object' ? message[dysonCode][1] : message[dysonCode];
//this.log.debug(`${getDataPointName(deviceConfig)} is a bool switch. Current state: [${testValue}]`);
value = ['ON', 'HUMD', 'HEAT'].includes(testValue); // testValue === 'ON' || testValue === 'HUMD' || testValue === 'HEAT';
} else if (deviceConfig.type === 'boolean' && deviceConfig.role.startsWith('indicator')) {
// testValue should be the 2nd value in an array or if it's no array, the value itself
const testValue = typeof message[dysonCode] === 'object' ? message[dysonCode][1] : message[dysonCode];
this.log.silly(
`${deviceConfig.name} is a bool switch. Current state: [${testValue}] --> returnvalue for further processing: ${testValue === 'FAIL'}`,
);
value = testValue === 'FAIL';
} else {
// It's no bool switch
value = message[dysonCode];
}
// during state-change message only changed values are being updated
if (typeof value === 'object') {
if (value[0] === value[1]) {
this.log.debug(`Values for [${deviceConfig.name}] are equal. No update required. Skipping.`);
continue;
} else {
value = value[1].valueOf();
}
this.log.debug(
`Value is an object. Converting to value: [${JSON.stringify(value)}] --> [${value.valueOf()}]`,
);
value = value.valueOf();
}
this.createOrExtendObject(
`${device.Serial + path}.${deviceConfig.name}`,
{
type: 'state',
common: {
name: deviceConfig.description,
read: true,
write: deviceConfig.writeable,
role: deviceConfig.role,
type: deviceConfig.type,
unit: deviceConfig.unit,
states: !deviceConfig.displayValues
? undefined
: SPECIAL_PROPERTIES.has(dysonCode)
? PRODUCTS[device.ProductType][dysonCode]
: deviceConfig.displayValues,
},
native: {},
},
value,
);
// getWriteable(deviceConfig)=true -> data field is editable, so subscribe for state changes
if (deviceConfig.writeable) {
//this.log.debug('Subscribing for state changes on datapoint: ' + device.Serial + path + '.'+ deviceConfig.name );
this.subscribeStates(`${device.Serial + path}.${deviceConfig.name}`);
}
}
}
/**
* createNO2
*
* creates the data fields for the values itself and the index if the device has a NO2 sensor
*
* @param message the received mqtt message
* @param message[].noxl the NO2 value
* @param row the current data row
* @param device the device object the data is valid for
*/
createNO2(message, row, device) {
// NO2 QualityIndex
// 0-3: Good, 4-6: Medium, 7-8, Bad, >9: very Bad
let NO2Index = 0;
const value = Math.floor(message[row].noxl / 10);
if (value < 4) {
NO2Index = 0;
} else if (value >= 4 && value <= 6) {
NO2Index = 1;
} else if (value >= 7 && value <= 8) {
NO2Index = 2;
} else if (value >= 9) {
NO2Index = 3;
}
this.createOrExtendObject(
`${device.Serial}.Sensor.NO2Index`,
{
type: 'state',
common: {
name: 'NO2 QualityIndex. 0-3: Good, 4-6: Medium, 7-8, Bad, >9: very Bad',
read: true,
write: false,
role: 'value',
type: 'number',
states: {
0: 'Good',
1: 'Medium',
2: 'Bad',
3: 'very Bad',
4: 'extremely Bad',
5: 'worrying',
},
},
native: {},
},
NO2Index,
);
this.subscribeStates(`${device.Serial}.Sensor.NO2Index`);
}
/**
* createHCHO
*
* creates the data fields for the values itself and the index if the device has a HCHO sensor
*
* @param message the received mqtt message
* @param message[].noxl the NO2 value
* @param row the current data row
* @param device the device object the data is valid for
*/
createHCHO(message, row, device) {
// HCHO QualityIndex
// 0-3: Good, 4-6: Medium, 7-8, Bad, >9: very Bad
let HCHOIndex = 0;
const value = message[row].hchr / 1000;
if (value >= 0 && value < 0.1) {
HCHOIndex = 0;
} else if (value >= 0.1 && value < 0.3) {
HCHOIndex = 1;
} else if (value >= 0.3 && value < 0.5) {
HCHOIndex = 2;
} else if (value >= 0.5) {
HCHOIndex = 3;
}
this.createOrExtendObject(
`${device.Serial}.Sensor.HCHOIndex`,
{
type: 'state',
common: {
name: 'HCHO QualityIndex. 0 - 0.099: Good, 0.100 - 0.299: Medium, 0.300 - 0.499, Bad, >0.500: very Bad',
read: true,
write: false,
role: 'value',
type: 'number',
states: {
0: 'Good',
1: 'Medium',
2: 'Bad',
3: 'very Bad',
4: 'extremely Bad',
5: 'worrying',
},
},
native: {},
},
HCHOIndex,
);
this.subscribeStates(`${device.Serial}.Sensor.HCHOIndex`);
}
/**
* createVOC
*
* creates the data fields for the values itself and the index if the device has a VOC sensor
*
* @param message the received mqtt message
* @param message[].va10 the VOC value
* @param row the current data row
* @param device the device object the data is valid for
*/
createVOC(message, row, device) {
// VOC QualityIndex
// 0-3: Good, 4-6: Medium, 7-8, Bad, >9: very Bad
let VOCIndex = 0;
if (message[row].va10 < 40) {
VOCIndex = 0;
} else if (message[row].va10 >= 40 && message[row].va10 < 70) {
VOCIndex = 1;
} else if (message[row].va10 >= 70 && message[row].va10 < 90) {
VOCIndex = 2;
} else if (message[row].va10 >= 90) {
VOCIndex = 3;
}
this.createOrExtendObject(
`${device.Serial}.Sensor.VOCIndex`,
{
type: 'state',
common: {
name: 'VOC QualityIndex. 0-3: Good, 4-6: Medium, 7-9: Bad, >9: very Bad',
read: true,
write: false,
role: 'value',
type: 'number',
states: {
0: 'Good',
1: 'Medium',
2: 'Bad',
3: 'very Bad',
4: 'extremely Bad',
5: 'worrying',
},
},
native: {},
},
VOCIndex,
);
VOC = VOCIndex;
this.subscribeStates(`${device.Serial}.Sensor.VOCIndex`);
}
/**
* createPM10
*
* creates the data fields for the values itself and the index if the device has a PM 10 sensor
*
* @param message the received mqtt message
* @param message[].pm10 the PM10 value
* @param row the current data row
* @param device the device object the data is valid for
*/
createPM10(message, row, device) {
// PM10 QualityIndex
// 0-50: Good, 51-75: Medium, 76-100, Bad, 101-350: very Bad, 351-420: extremely Bad, >421 worrying
let PM10Index = 0;
if (message[row].p10r < 51) {
PM10Index = 0;
} else if (message[row].p10r >= 51 && message[row].p10r <= 75) {
PM10Index = 1;
} else if (message[row].p10r >= 76 && message[row].p10r <= 100) {
PM10Index = 2;
} else if (message[row].p10r >= 101 && message[row].p10r <= 350) {
PM10Index = 3;
} else if (message[row].p10r >= 351 && message[row].p10r <= 420) {
PM10Index = 4;
} else if (message[row].p10r >= 421) {
PM10Index = 5;
}
this.createOrExtendObject(
`${device.Serial}.Sensor.PM10Index`,
{
type: 'state',
common: {
name: 'PM10 QualityIndex. 0-50: Good, 51-75: Medium, 76-100, Bad, 101-350: very Bad, 351-420: extremely Bad, >421 worrying',
read: true,
write: false,
role: 'value',
type: 'number',
states: {
0: 'Good',
1: 'Medium',
2: 'Bad',
3: 'very Bad',
4: 'extremely Bad',
5: 'worrying',
},
},
native: {},
},
PM10Index,
);
PM10 = PM10Index;
this.subscribeStates(`${device.Serial}.Sensor.PM10Index`);
}
/**
* createDust
*
* creates the data fields for the values itself and the index if the device has a simple dust sensor
*
* @param message the received mqtt message
* @param message[].pact the dust value
* @param row the current data row
* @param device the device object the data is valid for
*/
createDust(message, row, device) {
// PM10 QualityIndex
// 0-50: Good, 51-75: Medium, 76-100, Bad, 101-350: very Bad, 351-420: extremely Bad, >421 worrying
let dustIndex = 0;
if (message[row].pact < 51) {
dustIndex = 0;
} else if (message[row].pact >= 51 && message[row].pact <= 75) {
dustIndex = 1;
} else if (message[row].pact >= 76 && message[row].pact <= 100) {
dustIndex = 2;
} else if (message[row].pact >= 101 && message[row].pact <= 350) {
dustIndex = 3;
} else if (message[row].pact >= 351 && message[row].pact <= 420) {
dustIndex = 4;
} else if (message[row].pact >= 421) {
dustIndex = 5;
}
this.createOrExtendObject(
`${device.Serial}.Sensor.DustIndex`,
{
type: 'state',
common: {
name: 'Dust QualityIndex. 0-50: Good, 51-75: Medium, 76-100, Bad, 101-350: very Bad, 351-420: extremely Bad, >421 worrying',
read: true,
write: false,
role: 'value',
type: 'number',
states: {
0: 'Good',
1: 'Medium',
2: 'Bad',
3: 'very Bad',
4: 'extremely Bad',
5: 'worrying',
},
},
native: {},
},
dustIndex,
);
Dust = dustIndex;
this.subscribeStates(`${device.Serial}.Sensor.DustIndex`);
}
/**
* createPM25
*
* creates the data fields for the values itself and the index if the device has a PM 2,5 sensor
*
* @param message the received mqtt message
* @param message[].p25r the PM2.5 value
* @param row the current data row
* @param device the device object the data is valid for
*/
createPM25(message, row, device) {
// PM2.5 QualityIndex
// 0-35: Good, 36-53: Medium, 54-70: Bad, 71-150: very Bad, 151-250: extremely Bad, >251 worrying
let PM25Index = 0;
if (message[row].p25r < 36) {
PM25Index = 0;
} else if (message[row].p25r >= 36 && message[row].p25r <= 53) {
PM25Index = 1;
} else if (message[row].p25r >= 54 && message[row].p25r <= 70) {
PM25Index = 2;
} else if (message[row].p25r >= 71 && message[row].p25r <= 150) {
PM25Index = 3;
} else if (message[row].p25r >= 151 && message[row].p25r <= 250) {
PM25Index = 4;
} else if (message[row].p25r >= 251) {
PM25Index = 5;
}
this.createOrExtendObject(
`${device.Serial}.Sensor.PM25Index`,
{
type: 'state',
common: {
name: 'PM2.5 QualityIndex. 0-35: Good, 36-53: Medium, 54-70: Bad, 71-150: very Bad, 151-250: extremely Bad, >251 worrying',
read: true,
write: false,
role: 'value',
type: 'number',
states: {
0: 'Good',
1: 'Medium',
2: 'Bad',
3: 'very Bad',
4: 'extremely Bad',
5: 'worrying',
},
},
native: {},
},
PM25Index,
);
PM25 = PM25Index;
this.subscribeStates(`${device.Serial}.Sensor.PM25Index`);
}
/**
*
* @param thisDevice {Object} link to the current Device object
* @param adapterLog {Object} link to the adapters log output
* @param reason {string} the reason why this function is called (for logging purposes)
*/
async pollDeviceInfo(thisDevice, adapterLog, reason) {
adapterLog.debug(`Updating device [${thisDevice.Serial}] (polling API ${reason}).`);
try {
// possible messages:
// msg: 'REQUEST-CURRENT-STATE'
// msg: 'REQUEST-PRODUCT-ENVIRONMENT-CURRENT-SENSOR-DATA'
// msg: 'REQUEST-CURRENT-FAULTS'
thisDevice.mqttClient.publish(
`${thisDevice.ProductType}/${thisDevice.Serial}/command`,
JSON.stringify({
msg: 'REQUEST-CURRENT-STATE',
time: new Date().toISOString(),
}),
);
thisDevice.mqttClient.publish(
`${thisDevice.ProductType}/${thisDevice.Serial}/command`,
JSON.stringify({
msg: 'REQUEST-CURRENT-FAULTS',
time: new Date().toISOString(),
}),
);
thisDevice.mqttClient.publish(
`${thisDevice.ProductType}/${thisDevice.Serial}/command`,
JSON.stringify({
msg: 'REQUEST-PRODUCT-ENVIRONMENT-CURRENT-SENSOR-DATA',
time: new Date().toISOString(),
}),
);
} catch (error) {
adapterLog.error(`${thisDevice.Serial} - MQTT interval error: ${error}`);
}
}
/**
* main
*
* It's the main routine of the adapter
*/
async main() {
const adapterLog = this.log;
// @ts-expect-error - devices is defined in the global scope
if (!this.config.token) {
return;
}
try {
adapterLog.info('Querying devices from dyson API.');
// @ts-expect-error - devices is defined in the global scope
devices = await dysonUtils.getDevices(this.config.token, this);
if (typeof devices != 'undefined') {
for (const thisDevice of devices) {
// delete deprecated fields from device tree
await this.CreateOrUpdateDevice(thisDevice);
await dysonUtils.deleteUnusedFields(this, `${this.name}.${this.instance}.${thisDevice.Serial}`);
// Initializes the MQTT client for local communication with the thisDevice
this.log.debug(`Result of CreateOrUpdateDevice: [${JSON.stringify(thisDevice)}]`);
await this.getIPofDevice(thisDevice);
// subscribe to changes on host address to re-init adapter on changes
this.log.debug(`Subscribing for state changes on datapoint: ${thisDevice.Serial}.hostAddress`);
this.subscribeStates(`${thisDevice.Serial}.hostAddress`);
// connect to device
adapterLog.info(
`${thisDevice.Serial} - MQTT connection requested for [${thisDevice.hostAddress}${thisDevice.ipAddress ? `@${thisDevice.ipAddress}` : ''}].`,
);
thisDevice.mqttClient = mqtt.connect(`mqtt://${thisDevice.hostAddress}`, {