UNPKG

homebridge-dyson-pure-cool

Version:
911 lines (794 loc) 56.9 kB
const productTypeInfo = require('./productTypeInfo'); const mqtt = require('mqtt'); /** * Represents a physical Dyson device. * @param platform The DysonPureCoolPlatform instance. * @param name The device information provided by the Dyson API. * @param serialNumber The device information provided by the Dyson API. * @param productType The device information provided by the Dyson API. * @param version The device information provided by the Dyson API. * @param password The device password used for MQTT connections. * @param config The device configuration. */ function DysonPureCoolDevice(platform, name, serialNumber, productType, version, password, config) { const device = this; const { UUIDGen, Accessory, Characteristic, Service } = platform; const temperatureStep = config.useFahrenheit ? 0.555 : 1; // Stores the information of the device that is used for shutdown device.serialNumber = serialNumber; device.platform = platform; device.mqttClient = null; // Logs the product type, so that new devices can be easily added in the future platform.log.info('Device with serial number ' + serialNumber + ': product type ' + productType); device.info = productTypeInfo(productType); device.info.name = name || 'Dyson'; // Makes sure that a name is set // Checks if heating is disabled by the configuration if (config.isHeatingDisabled) { device.info.hasHeating = false; } // Gets all accessories from the platform that match the serial number let unusedDeviceAccessories = platform.accessories.filter(function(a) { return a.context.serialNumber === serialNumber; }); let newDeviceAccessories = []; let deviceAccessories = []; // Gets the air purifier accessory let airPurifierAccessory = unusedDeviceAccessories.find(function(a) { return a.context.kind === 'AirPurifierAccessory'; }); if (airPurifierAccessory) { unusedDeviceAccessories.splice(unusedDeviceAccessories.indexOf(airPurifierAccessory), 1); } else { platform.log.info('Adding new accessory with serial number ' + serialNumber + ' and kind AirPurifierAccessory.'); airPurifierAccessory = new Accessory(device.info.name, UUIDGen.generate(serialNumber + 'AirPurifierAccessory')); airPurifierAccessory.context.serialNumber = serialNumber; airPurifierAccessory.context.kind = 'AirPurifierAccessory'; newDeviceAccessories.push(airPurifierAccessory); } deviceAccessories.push(airPurifierAccessory); // Gets the air quality accessory let airQualityAccessory = null; if (config.isAirQualitySensorEnabled) { if (config.isSingleAccessoryModeEnabled) { airQualityAccessory = airPurifierAccessory; } else { airQualityAccessory = unusedDeviceAccessories.find(function(a) { return a.context.kind === 'AirQualityAccessory'; }); if (airQualityAccessory) { unusedDeviceAccessories.splice(unusedDeviceAccessories.indexOf(airQualityAccessory), 1); } else { platform.log.info('Adding new accessory with serial number ' + serialNumber + ' and kind AirQualityAccessory.'); airQualityAccessory = new Accessory(device.info.name + ' Air Quality', UUIDGen.generate(serialNumber + 'AirQualityAccessory')); airQualityAccessory.context.serialNumber = serialNumber; airQualityAccessory.context.kind = 'AirQualityAccessory'; newDeviceAccessories.push(airQualityAccessory); } deviceAccessories.push(airQualityAccessory); } } // Gets the temperature accessory let temperatureAccessory = null; if (device.info.hasHeating || (device.info.hasTemperatureAndHumiditySensors && config.isTemperatureSensorEnabled)) { if (config.isSingleAccessoryModeEnabled) { temperatureAccessory = airPurifierAccessory; } else if (!device.info.hasHeating && config.isAirQualitySensorEnabled && config.isSingleSensorAccessoryModeEnabled) { temperatureAccessory = airQualityAccessory; } else { temperatureAccessory = unusedDeviceAccessories.find(function(a) { return a.context.kind === 'TemperatureAccessory'; }); if (temperatureAccessory) { unusedDeviceAccessories.splice(unusedDeviceAccessories.indexOf(temperatureAccessory), 1); } else { platform.log.info('Adding new accessory with serial number ' + serialNumber + ' and kind TemperatureAccessory.'); temperatureAccessory = new Accessory(device.info.name + ' Temperature', UUIDGen.generate(serialNumber + 'TemperatureAccessory')); temperatureAccessory.context.serialNumber = serialNumber; temperatureAccessory.context.kind = 'TemperatureAccessory'; newDeviceAccessories.push(temperatureAccessory); } deviceAccessories.push(temperatureAccessory); } } // Gets the humidity accessory let humidityAccessory = null; if (device.info.hasHumidifier || (device.info.hasTemperatureAndHumiditySensors && config.isHumiditySensorEnabled)) { if (config.isSingleAccessoryModeEnabled) { humidityAccessory = airPurifierAccessory; } else if (!device.info.hasHumidifier && config.isAirQualitySensorEnabled && config.isSingleSensorAccessoryModeEnabled) { humidityAccessory = airQualityAccessory; } else { humidityAccessory = unusedDeviceAccessories.find(function(a) { return a.context.kind === 'HumidityAccessory'; }); if (humidityAccessory) { unusedDeviceAccessories.splice(unusedDeviceAccessories.indexOf(humidityAccessory), 1); } else { platform.log.info('Adding new accessory with serial number ' + serialNumber + ' and kind HumidityAccessory.'); humidityAccessory = new Accessory(device.info.name + ' Humidity', UUIDGen.generate(serialNumber + 'HumidityAccessory')); humidityAccessory.context.serialNumber = serialNumber; humidityAccessory.context.kind = 'HumidityAccessory'; newDeviceAccessories.push(humidityAccessory); } deviceAccessories.push(humidityAccessory); } } // Gets the switch accessory let switchAccessory = null; if (config.isAutoModeEnabled || config.isNightModeEnabled || config.isContinuousMonitoringEnabled || (config.isJetFocusEnabled && device.info.hasJetFocus)) { if (config.isSingleAccessoryModeEnabled) { switchAccessory = airPurifierAccessory; } else { switchAccessory = unusedDeviceAccessories.find(function(a) { return a.context.kind === 'SwitchAccessory'; }); if (switchAccessory) { unusedDeviceAccessories.splice(unusedDeviceAccessories.indexOf(switchAccessory), 1); } else { platform.log.info('Adding new accessory with serial number ' + serialNumber + ' and kind SwitchAccessory.'); switchAccessory = new Accessory(device.info.name + ' Settings', UUIDGen.generate(serialNumber + 'SwitchAccessory')); switchAccessory.context.serialNumber = serialNumber; switchAccessory.context.kind = 'SwitchAccessory'; newDeviceAccessories.push(switchAccessory); } deviceAccessories.push(switchAccessory); } } // Registers the newly created accessories platform.api.registerPlatformAccessories(platform.pluginName, platform.platformName, newDeviceAccessories); // Removes all unused accessories for (let i = 0; i < unusedDeviceAccessories.length; i++) { const unusedDeviceAccessory = unusedDeviceAccessories[i]; platform.log.info('Removing unused accessory with serial number ' + serialNumber + ' and kind ' + unusedDeviceAccessory.context.kind + '.'); platform.accessories.splice(platform.accessories.indexOf(unusedDeviceAccessory), 1); } platform.api.unregisterPlatformAccessories(platform.pluginName, platform.platformName, unusedDeviceAccessories); // Updates the accessory information for (let i = 0; i < deviceAccessories.length; i++) { const deviceAccessory = deviceAccessories[i]; let accessoryInformationService = deviceAccessory.getService(Service.AccessoryInformation); if (!accessoryInformationService) { accessoryInformationService = deviceAccessory.addService(Service.AccessoryInformation); } accessoryInformationService .setCharacteristic(Characteristic.Manufacturer, 'Dyson') .setCharacteristic(Characteristic.Model, device.info.model) .setCharacteristic(Characteristic.SerialNumber, serialNumber) .setCharacteristic(Characteristic.FirmwareRevision, version) .setCharacteristic(Characteristic.HardwareRevision, device.info.hardwareRevision); } // Updates the air purifier let airPurifierService = airPurifierAccessory.getService(Service.AirPurifier); if (!airPurifierService) { airPurifierService = airPurifierAccessory.addService(Service.AirPurifier); } // Updates the filter life level unit airPurifierService .getCharacteristic(Characteristic.FilterLifeLevel) .setProps({ unit: "percentage" }); // Updates the rotation speed steps airPurifierService .getCharacteristic(Characteristic.RotationSpeed) .setProps({ minStep: 10, minValue: 0, maxValue: 100 }); // Updates the temperature service let temperatureService = null; if (device.info.hasHeating || device.info.hasTemperatureAndHumiditySensors) { if (!temperatureAccessory) { if (!config.isTemperatureIgnored) { temperatureService = airPurifierService; } } else { if (device.info.hasHeating) { // Uses a thermostat service temperatureService = temperatureAccessory.getService(Service.Thermostat); if (!temperatureService) { temperatureService = temperatureAccessory.addService(Service.Thermostat); } // Disables cooling temperatureService.getCharacteristic(Characteristic.CurrentHeatingCoolingState).setProps({ maxValue: 1, minValue: 0, validValues: [0, 1] }); temperatureService.getCharacteristic(Characteristic.TargetHeatingCoolingState).setProps({ maxValue: 1, minValue: 0, validValues: [0, 1] }); // Updates the target temperature for heating temperatureService.getCharacteristic(Characteristic.TargetTemperature).setProps({ maxValue: 38, minValue: 0, minStep: temperatureStep, unit: 'celsius' }); // Properly set the temperature display unit temperatureService.updateCharacteristic(Characteristic.TemperatureDisplayUnits, config.useFahrenheit ? 1 : 0); } else { // Uses a temperature sensor temperatureService = temperatureAccessory.getService(Service.TemperatureSensor); if (!temperatureService) { temperatureService = temperatureAccessory.addService(Service.TemperatureSensor); } } } // Updates the temperature steps if (temperatureService) { temperatureService .getCharacteristic(Characteristic.CurrentTemperature) .setProps({ minValue: -50, maxValue: 100, unit: 'celsius' }); } } // Updates the humidity sensor let humidifierService = null; let humidityService = null; if (device.info.hasHumidifier || device.info.hasTemperatureAndHumiditySensors) { if (!humidityAccessory) { if (!config.isHumidityIgnored) { humidityService = airPurifierService; } } else { if (device.info.hasHumidifier) { // Uses a humidifier service humidifierService = humidityAccessory.getService(Service.HumidifierDehumidifier); if (!humidifierService) { humidifierService = humidityAccessory.addService(Service.HumidifierDehumidifier); } // Disables dehumidifying humidifierService.getCharacteristic(Characteristic.CurrentHumidifierDehumidifierState).setProps({ maxValue: 2, minValue: 0, validValues: [0, 1, 2] }); humidifierService.getCharacteristic(Characteristic.TargetHumidifierDehumidifierState).setProps({ maxValue: 1, minValue: 0, validValues: [0, 1] }); // Updates the humidity threshold if (config.isFullRangeHumidity) { humidifierService.getCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold).setProps({ maxValue: 100, minValue: 0, minStep: 1 }); } else { humidifierService.getCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold).setProps({ maxValue: 70, minValue: 30, minStep: 10 }); } // Uses a humidify sensor if an additional sensor should be exposed if (config.isHumiditySensorEnabled) { humidityService = humidityAccessory.getService(Service.HumiditySensor); if (!humidityService) { humidityService = humidityAccessory.addService(Service.HumiditySensor); } } } else { // Uses a humidify sensor humidityService = humidityAccessory.getService(Service.HumiditySensor); if (!humidityService) { humidityService = humidityAccessory.addService(Service.HumiditySensor); } } } } // Updates the air quality sensor let airQualityService = null; if (!airQualityAccessory) { if (!config.isAirQualityIgnored) { airQualityService = airPurifierService; } } else { airQualityService = airQualityAccessory.getService(Service.AirQualitySensor); if (!airQualityService) { airQualityService = airQualityAccessory.addService(Service.AirQualitySensor); } } // Updates the night mode let nightModeSwitchService = null; if (switchAccessory && config.isNightModeEnabled) { nightModeSwitchService = switchAccessory.getServiceById(Service.Switch, 'NightMode'); if (!nightModeSwitchService) { nightModeSwitchService = switchAccessory.addService(Service.Switch, device.info.name + ' Night Mode', 'NightMode'); } } // Updates the auto mode let autoModeSwitchService = null; if (switchAccessory && config.isAutoModeEnabled) { autoModeSwitchService = switchAccessory.getServiceByUUIDAndSubType(Service.Switch, 'AutoMode'); if (!autoModeSwitchService) { autoModeSwitchService = switchAccessory.addService(Service.Switch, device.info.name + ' Auto Mode', 'AutoMode'); } } // Updates the jet focus mode let jetFocusSwitchService = null; if (switchAccessory && config.isJetFocusEnabled && device.info.hasJetFocus) { jetFocusSwitchService = switchAccessory.getServiceById(Service.Switch, 'JetFocus'); if (!jetFocusSwitchService) { jetFocusSwitchService = switchAccessory.addService(Service.Switch, device.info.name + ' Jet Focus', 'JetFocus'); } } // Updates the continuous monitoring let continuousMonitoringSwitchService = null; if (switchAccessory && config.isContinuousMonitoringEnabled) { continuousMonitoringSwitchService = switchAccessory.getServiceById(Service.Switch, 'ContinuousMonitoring'); if (!continuousMonitoringSwitchService) { continuousMonitoringSwitchService = switchAccessory.addService(Service.Switch, device.info.name + ' Continuous Monitoring', 'ContinuousMonitoring'); } } // Initializes the MQTT client for local communication with the device device.mqttClient = mqtt.connect('mqtt://' + config.ipAddress, { username: serialNumber, password: password, protocolVersion: 3, protocolId: 'MQIsdp' }); platform.log.debug(serialNumber + ' - MQTT connection requested for ' + config.ipAddress + '.'); // Subscribes for events of the MQTT client let updateIntervalHandle = null; device.mqttClient.on('connect', function () { platform.log.debug(serialNumber + ' - MQTT connection established.'); // Subscribes to the status topic to receive updates device.mqttClient.subscribe(productType + '/' + serialNumber + '/status/current', function () { // Sends an initial request for the current state device.mqttClient.publish(productType + '/' + serialNumber + '/command', JSON.stringify({ msg: 'REQUEST-CURRENT-STATE', time: new Date().toISOString() })); // Sets the interval for status updates updateIntervalHandle = setInterval(function() { try { device.mqttClient.publish(productType + '/' + serialNumber + '/command', JSON.stringify({ msg: 'REQUEST-CURRENT-STATE', time: new Date().toISOString() })); } catch (error) { platform.log.debug(serialNumber + ' - MQTT interval error: ' + error); } }, device.platform.config.updateInterval); }); }); device.mqttClient.on('error', function (error) { platform.log.debug(serialNumber + ' - MQTT error: ' + error); }); device.mqttClient.on('reconnect', function () { platform.log.debug(serialNumber + ' - MQTT reconnecting.'); }); device.mqttClient.on('close', function () { platform.log.debug(serialNumber + ' - MQTT disconnected.'); if (updateIntervalHandle) { clearInterval(updateIntervalHandle); updateIntervalHandle = null; } }); device.mqttClient.on('offline', function () { platform.log.debug(serialNumber + ' - MQTT offline.'); if (updateIntervalHandle) { clearInterval(updateIntervalHandle); updateIntervalHandle = null; } }); device.mqttClient.on('end', function () { platform.log.debug(serialNumber + ' - MQTT ended.'); if (updateIntervalHandle) { clearInterval(updateIntervalHandle); updateIntervalHandle = null; } }); device.mqttClient.on('message', function (_, payload) { platform.log.debug(serialNumber + ' - MQTT message received: ' + payload.toString()); // Parses the payload const content = JSON.parse(payload); // Updates the environmental data if (content.msg === 'ENVIRONMENTAL-CURRENT-SENSOR-DATA') { // Sets the sensor data for temperature if (device.info.hasHeating || device.info.hasTemperatureAndHumiditySensors) { if (content['data']['tact'] !== 'OFF' && temperatureService) { temperatureService.updateCharacteristic(Characteristic.CurrentTemperature, (Number.parseInt(content['data']['tact']) / 10.0) - 273.0 + (config.temperatureOffset || 0.0)); } } // Sets the sensor data for humidity if (device.info.hasHumidifier || device.info.hasTemperatureAndHumiditySensors) { if (content['data']['hact'] !== 'OFF' && humidityService) { humidityService.updateCharacteristic(Characteristic.CurrentRelativeHumidity, Number.parseInt(content['data']['hact']) + (config.humidityOffset || 0.0)); } if (content['data']['hact'] !== 'OFF' && humidifierService) { humidifierService.updateCharacteristic(Characteristic.CurrentRelativeHumidity, Number.parseInt(content['data']['hact']) + (config.humidityOffset || 0.0)); } } // Parses the air quality sensor data if (airQualityService) { let pm25 = -1; let pm10 = -1; let va10 = -1; let noxl = -1; let hcho = -1; let p = -1; let v = -1; if (device.info.hasAdvancedAirQualitySensors) { // Checks whether continuous monitoring is disabled if (content['data']['p25r'] === 'OFF') { return; } if (content['data']['p10r'] === 'OFF') { return; } if (content['data']['va10'] === 'OFF') { return; } if (content['data']['noxl'] === 'OFF') { return; } if (content['data']['p25r']) { pm25 = content['data']['p25r'] === 'INIT' ? 0 : Number.parseInt(content['data']['p25r']); if (isNaN(pm25)) { pm25 = 0; } } if (content['data']['p10r']) { pm10 = content['data']['p10r'] === 'INIT' ? 0 : Number.parseInt(content['data']['p10r']); if (isNaN(pm10)) { pm10 = 0; } } if (content['data']['va10']) { va10 = content['data']['va10'] === 'INIT' ? 0 : Number.parseInt(content['data']['va10']); if (isNaN(va10)) { va10 = 0; } } if (content['data']['noxl']) { noxl = content['data']['noxl'] === 'INIT' ? 0 : Number.parseInt(content['data']['noxl']); if (isNaN(noxl)) { noxl = 0; } } if (content['data']['hchr']) { hcho = content['data']['hchr'] === 'INIT' ? 0 : Number.parseInt(content['data']['hchr']); if (isNaN(hcho)) { hcho = 0; } } } else { // Checks whether continuous monitoring is disabled if (content['data']['pact'] === 'OFF') { return; } if (content['data']['vact'] === 'OFF') { return; } if (content['data']['pact']) { p = content['data']['pact'] === 'INIT' ? 0 : Number.parseInt(content['data']['pact']); if (isNaN(p)) { p = 0; } } if (content['data']['vact']) { v = content['data']['vact'] === 'INIT' ? 0 : Number.parseInt(content['data']['vact']); if (isNaN(v)) { v = 0; } } } // Sets the sensor data for air quality (the poorest sensor result wins) if (device.info.hasAdvancedAirQualitySensors) { // Maps the values of the sensors to the relative values described in the app (1 - 5 => Good, Medium, Bad, Very Bad, Extremely Bad) const pm25Quality = pm25 <= 35 ? 1 : (pm25 <= 53 ? 2 : (pm25 <= 70 ? 3 : (pm25 <= 150 ? 4 : 5))); const pm10Quality = pm10 <= 50 ? 1 : (pm10 <= 75 ? 2 : (pm10 <= 100 ? 3 : (pm10 <= 350 ? 4 : 5))); // Maps the VOC values to a self-created scale (as described values in the app don't fit) const va10Quality = (va10 * 0.125) <= 3 ? 1 : ((va10 * 0.125) <= 6 ? 2 : ((va10 * 0.125) <= 8 ? 3 : 4)); // Maps the NO2 value to a self-created scale const noxlQuality = noxl <= 30 ? 1 : (noxl <= 60 ? 2 : (noxl <= 80 ? 3 : (noxl <= 90 ? 4 : 5))); // Maps the HCHO value to a self-created scale const hchoQuality = hcho <= 99 ? 1 : (hcho <= 299 ? 2 : (hcho <= 499 ? 3 : 4)); airQualityService.updateCharacteristic(Characteristic.AirQuality, Math.max(pm25Quality, pm10Quality, va10Quality, noxlQuality, hchoQuality)); if (pm25 !== -1) { airQualityService.updateCharacteristic(Characteristic.PM2_5Density, pm25); } if (pm10 !== -1) { airQualityService.updateCharacteristic(Characteristic.PM10Density, pm10); } if (va10 !== -1) { airQualityService.updateCharacteristic(Characteristic.VOCDensity, va10); } if (noxl !== -1) { airQualityService.updateCharacteristic(Characteristic.NitrogenDioxideDensity, noxl); } } else { // Maps the values of the sensors to the relative values, these operations are copied from the newer devices as the app does not specify the correct values const pQuality = p <= 2 ? 1 : (p <= 4 ? 2 : (p <= 7 ? 3 : (p <= 9 ? 4 : 5))); const vQuality = (v * 0.125) <= 3 ? 1 : ((v * 0.125) <= 6 ? 2 : ((v * 0.125) <= 8 ? 3 : 4)); airQualityService.updateCharacteristic(Characteristic.AirQuality, Math.max(pQuality, vQuality)); } } return; } // Updates the state data if (content.msg === 'CURRENT-STATE') { // Sets the power state if (content['product-state']['fpwr']) { airPurifierService.updateCharacteristic(Characteristic.Active, content['product-state']['fpwr'] === 'OFF' ? Characteristic.Active.INACTIVE : Characteristic.Active.ACTIVE); } if (content['product-state']['fmod']) { airPurifierService.updateCharacteristic(Characteristic.Active, content['product-state']['fmod'] === 'OFF' ? Characteristic.Active.INACTIVE : Characteristic.Active.ACTIVE); } // Sets the heating mode and target temperature if (device.info.hasHeating) { if (content['product-state']['hmod']) { const poweredOff = airPurifierService.getCharacteristic(Characteristic.Active).value === Characteristic.Active.INACTIVE; temperatureService.updateCharacteristic(Characteristic.CurrentHeatingCoolingState, (poweredOff || content['product-state']['hmod'] === 'OFF') ? Characteristic.CurrentHeatingCoolingState.OFF : Characteristic.CurrentHeatingCoolingState.HEAT); temperatureService.updateCharacteristic(Characteristic.TargetHeatingCoolingState, (poweredOff || content['product-state']['hmod'] === 'OFF') ? Characteristic.TargetHeatingCoolingState.OFF : Characteristic.TargetHeatingCoolingState.HEAT); } if (content['product-state']['hmax']) { temperatureService.updateCharacteristic(Characteristic.TargetTemperature, (Number.parseInt(content['product-state']['hmax']) / 10.0) - 273.0 + (config.temperatureOffset || 0.0)); } } // Sets the humidifier mode and threshold if (device.info.hasHumidifier) { if (content['product-state']['hume']) { humidifierService.updateCharacteristic(Characteristic.Active, content['product-state']['hume'] === 'OFF' ? Characteristic.Active.INACTIVE : Characteristic.Active.ACTIVE); } if (content['product-state']['hume'] && content['product-state']['msta']) { humidifierService.updateCharacteristic(Characteristic.CurrentHumidifierDehumidifierState, content['product-state']['hume'] === 'OFF' ? Characteristic.CurrentHumidifierDehumidifierState.INACTIVE : (content['product-state']['msta'] === 'OFF' ? Characteristic.CurrentHumidifierDehumidifierState.IDLE : Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING)); } if (content['product-state']['haut']) { humidifierService.updateCharacteristic(Characteristic.TargetHumidifierDehumidifierState, content['product-state']['haut'] === 'OFF' ? Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER : Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER); } if (content['product-state']['humt']) { humidifierService.updateCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold, Number.parseInt(content['product-state']['humt']) + (config.humidityOffset || 0.0)); } } // Sets the operation mode if (content['product-state']['fpwr'] && content['product-state']['fnst'] && content['product-state']['auto']) { airPurifierService.updateCharacteristic(Characteristic.CurrentAirPurifierState, content['product-state']['fpwr'] === 'OFF' ? Characteristic.CurrentAirPurifierState.INACTIVE : (content['product-state']['fnst'] === 'OFF' ? Characteristic.CurrentAirPurifierState.IDLE : Characteristic.CurrentAirPurifierState.PURIFYING_AIR)); airPurifierService.updateCharacteristic(Characteristic.TargetAirPurifierState, content['product-state']['auto'] === 'OFF' ? Characteristic.TargetAirPurifierState.MANUAL : Characteristic.TargetAirPurifierState.AUTO); } if (content['product-state']['fmod'] && content['product-state']['fnst']) { airPurifierService.updateCharacteristic(Characteristic.CurrentAirPurifierState, content['product-state']['fmod'] === 'OFF' ? Characteristic.CurrentAirPurifierState.INACTIVE : (content['product-state']['fnst'] === 'OFF' ? Characteristic.CurrentAirPurifierState.IDLE : Characteristic.CurrentAirPurifierState.PURIFYING_AIR)); airPurifierService.updateCharacteristic(Characteristic.TargetAirPurifierState, content['product-state']['fmod'] === 'AUTO' ? Characteristic.TargetAirPurifierState.AUTO : Characteristic.TargetAirPurifierState.MANUAL); } // Sets the rotation status if (device.info.hasOscillation) { airPurifierService.updateCharacteristic(Characteristic.SwingMode, content['product-state']['oson'] === 'OFF' ? Characteristic.SwingMode.SWING_DISABLED : Characteristic.SwingMode.SWING_ENABLED); } // Sets the filter life if (content['product-state']['cflr'] && content['product-state']['hflr']) { const cflr = content['product-state']['cflr'] == "INV" ? 100 : Number.parseInt(content['product-state']['cflr']); const hflr = content['product-state']['hflr'] == "INV" ? 100 : Number.parseInt(content['product-state']['hflr']); airPurifierService.updateCharacteristic(Characteristic.FilterChangeIndication, Math.min(cflr, hflr) >= 10 ? Characteristic.FilterChangeIndication.FILTER_OK : Characteristic.FilterChangeIndication.CHANGE_FILTER); airPurifierService.updateCharacteristic(Characteristic.FilterLifeLevel, Math.min(cflr,hflr)); } if (content['product-state']['filf']) { // Calculates the filter life, assuming 12 hours a day, 360 days const filterLife = Number.parseInt(content['product-state']['filf']) / (360 * 12); airPurifierService.updateCharacteristic(Characteristic.FilterChangeIndication, Math.ceil(filterLife * 100) >= 10 ? Characteristic.FilterChangeIndication.FILTER_OK : Characteristic.FilterChangeIndication.CHANGE_FILTER); airPurifierService.updateCharacteristic(Characteristic.FilterLifeLevel, Math.ceil(filterLife * 100)); } // Sets the fan speed based on the auto setting if (content['product-state']['fnsp'] !== 'AUTO') { airPurifierService.updateCharacteristic(Characteristic.RotationSpeed, Number.parseInt(content['product-state']['fnsp']) * 10); } // Sets the state of the night mode switch if (nightModeSwitchService) { nightModeSwitchService.updateCharacteristic(Characteristic.On, content['product-state']['nmod'] !== 'OFF'); } // Sets the state of the auto mode switch if (autoModeSwitchService) { if (content['product-state']['fmod']) { autoModeSwitchService.updateCharacteristic(Characteristic.On, content['product-state']['fmod'] === 'AUTO'); } } // Sets the state of the jet focus switch if (jetFocusSwitchService) { if (content['product-state']['fdir']) { jetFocusSwitchService.updateCharacteristic(Characteristic.On, content['product-state']['fdir'] !== 'OFF'); } if (content['product-state']['ffoc']) { jetFocusSwitchService.updateCharacteristic(Characteristic.On, content['product-state']['ffoc'] !== 'OFF'); } } // Sets the state of the continuous monitoring switch if (continuousMonitoringSwitchService) { continuousMonitoringSwitchService.updateCharacteristic(Characteristic.On, content['product-state']['rhtm'] !== 'OFF'); } return; } // Starts a new request as the state should be updated if (content.msg === 'STATE-CHANGE') { // Sets the power state if (content['product-state']['fpwr']) { airPurifierService.updateCharacteristic(Characteristic.Active, content['product-state']['fpwr'][1] === 'OFF' ? Characteristic.Active.INACTIVE : Characteristic.Active.ACTIVE); } if (content['product-state']['fmod']) { airPurifierService.updateCharacteristic(Characteristic.Active, content['product-state']['fmod'][1] === 'OFF' ? Characteristic.Active.INACTIVE : Characteristic.Active.ACTIVE); } // Sets the heating mode and target temperature if (device.info.hasHeating) { if (content['product-state']['hmod']) { const poweredOff = airPurifierService.getCharacteristic(Characteristic.Active).value === Characteristic.Active.INACTIVE; temperatureService.updateCharacteristic(Characteristic.CurrentHeatingCoolingState, (poweredOff || content['product-state']['hmod'][1] === 'OFF') ? Characteristic.CurrentHeatingCoolingState.OFF : Characteristic.CurrentHeatingCoolingState.HEAT); temperatureService.updateCharacteristic(Characteristic.TargetHeatingCoolingState, (poweredOff || content['product-state']['hmod'][1] === 'OFF') ? Characteristic.TargetHeatingCoolingState.OFF : Characteristic.TargetHeatingCoolingState.HEAT); } if (content['product-state']['hmax']) { temperatureService.updateCharacteristic(Characteristic.TargetTemperature, (Number.parseInt(content['product-state']['hmax'][1]) / 10.0) - 273.0 + (config.temperatureOffset || 0.0)); } } // Sets the humidifier mode and threshold if (device.info.hasHumidifier) { if (content['product-state']['hume']) { humidifierService.updateCharacteristic(Characteristic.Active, content['product-state']['hume'][1] === 'OFF' ? Characteristic.Active.INACTIVE : Characteristic.Active.ACTIVE); } if (content['product-state']['hume'] && content['product-state']['msta']) { humidifierService.updateCharacteristic(Characteristic.CurrentHumidifierDehumidifierState, content['product-state']['hume'][1] === 'OFF' ? Characteristic.CurrentHumidifierDehumidifierState.INACTIVE : (content['product-state']['msta'][1] === 'OFF' ? Characteristic.CurrentHumidifierDehumidifierState.IDLE : Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING)); } if (content['product-state']['haut']) { humidifierService.updateCharacteristic(Characteristic.TargetHumidifierDehumidifierState, content['product-state']['haut'][1] === 'OFF' ? Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER : Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER); } if (content['product-state']['humt']) { humidifierService.updateCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold, Number.parseInt(content['product-state']['humt'][1]) + (config.humidityOffset || 0.0)); } } // Sets the operation mode if (content['product-state']['fpwr'] && content['product-state']['fnst'] && content['product-state']['auto']) { airPurifierService.updateCharacteristic(Characteristic.CurrentAirPurifierState, content['product-state']['fpwr'][1] === 'OFF' ? Characteristic.CurrentAirPurifierState.INACTIVE : (content['product-state']['fnst'][1] === 'OFF' ? Characteristic.CurrentAirPurifierState.IDLE : Characteristic.CurrentAirPurifierState.PURIFYING_AIR)); airPurifierService.updateCharacteristic(Characteristic.TargetAirPurifierState, content['product-state']['auto'][1] === 'OFF' ? Characteristic.TargetAirPurifierState.MANUAL : Characteristic.TargetAirPurifierState.AUTO); } if (content['product-state']['fmod'] && content['product-state']['fnst']) { airPurifierService.updateCharacteristic(Characteristic.CurrentAirPurifierState, content['product-state']['fmod'][1] === 'OFF' ? Characteristic.CurrentAirPurifierState.INACTIVE : (content['product-state']['fnst'][1] === 'OFF' ? Characteristic.CurrentAirPurifierState.IDLE : Characteristic.CurrentAirPurifierState.PURIFYING_AIR)); airPurifierService.updateCharacteristic(Characteristic.TargetAirPurifierState, content['product-state']['fmod'][1] === 'AUTO' ? Characteristic.TargetAirPurifierState.AUTO : Characteristic.TargetAirPurifierState.MANUAL); } // Sets the rotation status if (device.info.hasOscillation) { airPurifierService.updateCharacteristic(Characteristic.SwingMode, content['product-state']['oson'][1] === 'OFF' ? Characteristic.SwingMode.SWING_DISABLED : Characteristic.SwingMode.SWING_ENABLED); } // Sets the filter life if (content['product-state']['cflr'] && content['product-state']['hflr']) { const cflr = content['product-state']['cflr'][1] == "INV" ? 100 : Number.parseInt(content['product-state']['cflr'][1]); const hflr = content['product-state']['cflr'][1] == "INV" ? 100 : Number.parseInt(content['product-state']['cflr'][1]); airPurifierService.updateCharacteristic(Characteristic.FilterChangeIndication, Math.min(cflr, hflr) >= 10 ? Characteristic.FilterChangeIndication.FILTER_OK : Characteristic.FilterChangeIndication.CHANGE_FILTER); airPurifierService.updateCharacteristic(Characteristic.FilterLifeLevel, Math.min(cflr,hflr)); } if (content['product-state']['filf']) { // Calculates the filter life, assuming 12 hours a day, 360 days const filterLife = Number.parseInt(content['product-state']['filf'][1]) / (360 * 12); airPurifierService.updateCharacteristic(Characteristic.FilterChangeIndication, Math.ceil(filterLife * 100) >= 10 ? Characteristic.FilterChangeIndication.FILTER_OK : Characteristic.FilterChangeIndication.CHANGE_FILTER); airPurifierService.updateCharacteristic(Characteristic.FilterLifeLevel, Math.ceil(filterLife * 100)); } // Sets the fan speed based on the auto setting if (content['product-state']['fnsp'][1] !== 'AUTO') { airPurifierService.updateCharacteristic(Characteristic.RotationSpeed, Number.parseInt(content['product-state']['fnsp'][1]) * 10); } // Sets the state of the night mode switch if (nightModeSwitchService) { nightModeSwitchService.updateCharacteristic(Characteristic.On, content['product-state']['nmod'][1] !== 'OFF'); } // Sets the state of the auto mode switch if (autoModeSwitchService) { if (content['product-state']['fmod']) { autoModeSwitchService.updateCharacteristic(Characteristic.On, content['product-state']['fmod'][1] === 'AUTO'); } } // Sets the state of the jet focus switch if (jetFocusSwitchService) { if (content['product-state']['fdir']) { jetFocusSwitchService.updateCharacteristic(Characteristic.On, content['product-state']['fdir'][1] !== 'OFF'); } if (content['product-state']['ffoc']) { jetFocusSwitchService.updateCharacteristic(Characteristic.On, content['product-state']['ffoc'][1] !== 'OFF'); } } // Sets the state of the continuous monitoring switch if (continuousMonitoringSwitchService) { continuousMonitoringSwitchService.updateCharacteristic(Characteristic.On, content['product-state']['rhtm'][1] !== 'OFF'); } return; } }); // Defines the timeout handle for fixing a bug in the Home app: whenever the air purifier is switched on, the TargetAirPurifierState is set to AUTO // This is prevented by delaying changes of the TargetAirPurifierState. If the Active characteristic is changed milliseconds after the TargetAirPurifierState, // it is detected and changing of the TargetAirPurifierState won't be executed let timeoutHandle = null; // Subscribes for changes of the active characteristic airPurifierService.getCharacteristic(Characteristic.Active).on('set', function (value, callback) { // Checks if a timeout has been set, which has to be cleared if (timeoutHandle) { platform.log.info(serialNumber + ' - set Active to ' + value + ': setting TargetAirPurifierState cancelled'); clearTimeout(timeoutHandle); timeoutHandle = null; } // Checks if the device is already in the target mode if (airPurifierService.getCharacteristic(Characteristic.Active).value === value) { return callback(null); } // Gets the active mode based on the configuration let activeMode = config.enableAutoModeWhenActivating ? 'AUTO' : 'FAN'; // Builds the command data, which contains the active state and (optionally) the oscillation and night modes let commandData = { fpwr: value === Characteristic.Active.INACTIVE ? 'OFF' : 'ON', fmod: value === Characteristic.Active.INACTIVE ? 'OFF' : activeMode }; if (config.enableOscillationWhenActivating && device.info.hasOscillation) { commandData['oson'] = 'ON'; } if (config.enableNightModeWhenActivating) { commandData['nmod'] = 'ON'; } // The Dyson app disables heating when the device is turned on if (value === Characteristic.Active.ACTIVE && device.info.hasHeating && !config.isHeatingSafetyIgnored) { commandData['hmod'] = 'OFF'; } // Executes the actual change of the active state platform.log.info(serialNumber + ' - set Active to ' + value + ': ' + JSON.stringify(commandData)); device.mqttClient.publish(productType + '/' + serialNumber + '/command', JSON.stringify({ msg: 'STATE-SET', time: new Date().toISOString(), data: commandData })); callback(null); }); // Subscribes for changes of the target state characteristic airPurifierService.getCharacteristic(Characteristic.TargetAirPurifierState).on('set', function (value, callback) { // Checks if AUTO mode can be enabled when activating the device if (config.enableAutoModeWhenActivating || airPurifierService.getCharacteristic(Characteristic.Active).value) { // Directly sets the target state platform.log.info(serialNumber + ' - set TargetAirPurifierState to ' + value + ': ' + JSON.stringify({ auto: value === Characteristic.TargetAirPurifierState.MANUAL ? 'OFF' : 'ON', fmod: value === Characteristic.TargetAirPurifierState.MANUAL ? 'FAN' : 'AUTO' })); device.mqttClient.publish(productType + '/' + serialNumber + '/command', JSON.stringify({ msg: 'STATE-SET', time: new Date().toISOString(), data: { auto: value === Characteristic.TargetAirPurifierState.MANUAL ? 'OFF' : 'ON', fmod: value === Characteristic.TargetAirPurifierState.MANUAL ? 'FAN' : 'AUTO' } })); } else { // Sets a timeout that can be cancelled by the Active characteristic handler platform.log.info(serialNumber + ' - set TargetAirPurifierState to ' + value + ' with delay'); timeoutHandle = setTimeout(function () { platform.log.info(serialNumber + ' - set TargetAirPurifierState to ' + value + ': ' + JSON.stringify({ auto: value === Characteristic.TargetAirPurifierState.MANUAL ? 'OFF' : 'ON', fmod: value === Characteristic.TargetAirPurifierState.MANUAL ? 'FAN' : 'AUTO' })); device.mqttClient.publish(productType + '/' + serialNumber + '/command', JSON.stringify({ msg: 'STATE-SET', time: new Date().toISOString(), data: { auto: value === Characteristic.TargetAirPurifierState.MANUAL ? 'OFF' : 'ON', fmod: value === Characteristic.TargetAirPurifierState.MANUAL ? 'FAN' : 'AUTO' } })); timeoutHandle = null; }, 250); } callback(null); }); // Subscribes for changes of the swing mode characteristic airPurifierService.getCharacteristic(Characteristic.SwingMode).on('set', function (value, callback) { platform.log.info(serialNumber + ' - set SwingMode to ' + value + ': ' + JSON.stringify({ oson: value === Characteristic.SwingMode.SWING_DISABLED ? 'OFF' : 'ON' })); device.mqttClient.publish(productType + '/' + serialNumber + '/command', JSON.stringify({ msg: 'STATE-SET', time: new Date().toISOString(), data: { oson: value === Characteristic.SwingMode.SWING_DISABLED ? 'OFF' : 'ON' } })); callback(null); }); // Subscribes for changes of the rotation speed characteristic airPurifierService.getCharacteristic(Characteristic.RotationSpeed).on('set', function (value, callback) { platform.log.info(serialNumber + ' - set RotationSpeed to ' + value + ': ' + JSON.stringify({ fnsp: ('0000' + Math.round(value / 10.0).toString()).slice(-4) })); device.mqttClient.publish(productType + '/' + serialNumber + '/command', JSON.stringify({ msg: 'STATE-SET', time: new Date().toISOString(), data: { fnsp: ('0000' + Math.round(value / 10.0).toString()).slice(-4) } })); callback(null); }); // Subscribes for changes of the heating mode and target temperature if (device.info.hasHeating) { temperatureService.getCharacteristic(Characteristic.TargetHeatingCoolingState).on('set', function (value, callback) { platform.log.info(serialNumber + ' - set TargetHeatingCoolingState to ' + value + ': ' + JSON.stringify({ hmod: value === Characteristic.TargetHeatingCoolingState.OFF ? 'OFF' : 'HEAT' })); device.mqttClient.publish(productType + '/' + serialNumber + '/command', JSON.stringify({ msg: 'STATE-SET', time: new Date().toISOString(), data: { hmod: value === Characteristic.TargetHeatingCoolingState.OFF ? 'OFF' : 'HEAT' } })); callback(null); }); temperatureService.getCharacteristic(Characteristic.TargetTemperature).on('set', function (value, callback) { platform.log.info(serialNumber + ' - set TargetTemperature to ' + value + ': ' + JSON.stringify({ hmax: ('0000' + Math.round((val