UNPKG

homebridge-dyson-bp01

Version:

A Homebridge plugin that adds a Dyson BP01 to HomeKit using a BroadLink RM

400 lines (399 loc) 18.8 kB
import BroadLinkJS from "kiwicam-broadlinkjs-rm"; import nodePersist from "node-persist"; import ping from "ping"; import * as constants from "./constants.js"; import * as messages from "./messages.js"; export default (api) => { api.registerAccessory(constants.ACCESSORY, DysonBP01); }; class DysonBP01 { logging; hap; accessoryConfig; broadLinkJS; device; alive; localStorage; services; fanV2Characteristics; sensorCharacteristics; skips; constructor(logging, accessoryConfig, api) { this.logging = logging; this.hap = api.hap; this.accessoryConfig = accessoryConfig; this.broadLinkJS = new BroadLinkJS(); this.device = undefined; this.alive = false; this.localStorage = nodePersist.create(); this.services = { accessoryInformation: new this.hap.Service.AccessoryInformation(this.accessoryConfig.name), fanV2: new this.hap.Service.Fanv2(), temperatureSensor: new this.hap.Service.TemperatureSensor(), humiditySensor: new this.hap.Service.HumiditySensor() }; this.fanV2Characteristics = { targetActive: this.hap.Characteristic.Active.INACTIVE, currentActive: this.hap.Characteristic.Active.INACTIVE, targetRotationSpeed: constants.MIN_STEP_ROTATION_SPEED, currentRotationSpeed: constants.MIN_STEP_ROTATION_SPEED, targetSwingMode: this.hap.Characteristic.SwingMode.SWING_DISABLED, currentSwingMode: this.hap.Characteristic.SwingMode.SWING_DISABLED }; this.sensorCharacteristics = { currentTemperature: 0, currentRelativeHumidity: 0 }; this.skips = { updateCurrentActive: 0, updateCurrentSwingMode: 0, updateSensorCharacteristics: 0, pingDeviceFail: 0 }; this.checkConfig(); this.init(api); } checkConfig() { if (this.accessoryConfig.serialNumber && !constants.SERIAL_NUMBER_REGEX.test(this.accessoryConfig.serialNumber)) { this.logging.warn(messages.SERIAL_NUMBER_MALFORMED); } if (this.accessoryConfig.macAddress && !constants.MAC_ADDRESS_REGEX.test(this.accessoryConfig.macAddress)) { this.logging.warn(messages.MAC_ADDRESS_MALFORMED); } if (this.accessoryConfig.exposeSensors && this.accessoryConfig.exposeSensors !== true && this.accessoryConfig.exposeSensors !== false) { this.logging.warn(messages.EXPOSE_SENSORS_MALFORMED); } } identify() { if (this.alive === true) { this.logging.info(messages.IDENTIFYING); let toggleCount = 0; let activeToggle = setInterval(async () => { if (toggleCount < constants.TOGGLES_IDENTIFY_ACTIVE) { if (this.fanV2Characteristics.targetActive === this.hap.Characteristic.Active.ACTIVE) { await this.setTargetActive(this.hap.Characteristic.Active.INACTIVE, () => { }); } else if (this.fanV2Characteristics.targetActive === this.hap.Characteristic.Active.INACTIVE) { await this.setTargetActive(this.hap.Characteristic.Active.ACTIVE, () => { }); } toggleCount++; } else if (this.fanV2Characteristics.targetActive === this.fanV2Characteristics.currentActive) { clearInterval(activeToggle); this.logging.success(messages.IDENTIFIED); } }, constants.INTERVAL); } else { this.logging.error(messages.DEVICE_NOT_CONNECTED); } } init(api) { this.localStorage.init({ dir: api.user.persistPath(), forgiveParseErrors: true }).then(() => { this.initFanV2Characteristics().then(() => { this.initDevice(); this.initInterval(); }); }); this.initServices(); } initInterval() { setInterval(async () => { if (this.device !== undefined) { await this.pingDevice(); if (this.alive === true) { await this.updateFanV2Characteristics(); if (this.accessoryConfig.exposeSensors === true) { await this.updateSensorCharacteristics(); } } this.doSkips(); } else { await this.broadLinkJS.discover(); } }, constants.INTERVAL); } doSkips() { this.doUpdateCurrentActiveSkip(); this.doUpdateCurrentSwingModeSkip(); this.doUpdateSensorCharacteristicsSkip(); this.doPingDeviceFailSkip(); } initServices() { this.initAccessoryInformationService(); this.initFanV2Service(); if (this.accessoryConfig.exposeSensors === true) { this.initSensorServices(); } } initAccessoryInformationService() { this.services.accessoryInformation .updateCharacteristic(this.hap.Characteristic.Manufacturer, messages.MANUFACTURER) .updateCharacteristic(this.hap.Characteristic.Model, messages.MODEL) .updateCharacteristic(this.hap.Characteristic.SerialNumber, (this.accessoryConfig.serialNumber !== undefined && constants.SERIAL_NUMBER_REGEX.test(this.accessoryConfig.serialNumber) === true) ? this.accessoryConfig.serialNumber.toUpperCase() : messages.SERIAL_NUMBER); } getServices() { let services = [ this.services.accessoryInformation, this.services.fanV2 ]; if (this.accessoryConfig.exposeSensors === true) { services.push(this.services.temperatureSensor, this.services.humiditySensor); } return services; } initDevice() { this.broadLinkJS.on("deviceReady", (device) => { let macAddress = device.mac.toString("hex").replace(/(.{2})/g, "$1:").slice(0, -1).toUpperCase(); this.logging.info(messages.DEVICE_DISCOVERED, macAddress); if (this.device === undefined && (this.accessoryConfig.macAddress === undefined || this.accessoryConfig.macAddress.toUpperCase() === macAddress)) { this.device = device; if (this.accessoryConfig.exposeSensors === true) { this.initSensors(); } this.logging.success(messages.DEVICE_USING, macAddress); } }); this.logging.info(messages.DEVICE_SEARCHING); } async pingDevice() { this.alive = await ping.promise.probe(this.device.host.address).then((pingResponse) => { return pingResponse.alive; }); if (this.alive === false) { if (this.skips.pingDeviceFail === 0) { this.logging.error(messages.DEVICE_CONNECTION_LOST); } this.skips.pingDeviceFail = constants.SKIPS_PING_DEVICE_FAIL; } else if (this.skips.pingDeviceFail > 0) { if (this.skips.pingDeviceFail === constants.SKIPS_PING_DEVICE_FAIL - 1) { this.logging.info(messages.DEVICE_RECONNECTING); } this.alive = false; } } doPingDeviceFailSkip() { if (this.skips.pingDeviceFail > 0) { this.skips.pingDeviceFail--; if (this.skips.pingDeviceFail === 0) { this.logging.success(messages.DEVICE_RECONNECTED); } } } async sendDeviceData(data) { await this.device.sendData(Buffer.from(data, "hex")); } initFanV2Service() { this.services.fanV2.getCharacteristic(this.hap.Characteristic.Active) .on("get", this.getTargetActive.bind(this)) .on("set", this.setTargetActive.bind(this)); this.services.fanV2.getCharacteristic(this.hap.Characteristic.RotationSpeed) .on("get", this.getTargetRotationSpeed.bind(this)) .on("set", this.setTargetRotationSpeed.bind(this)) .setProps({ minStep: constants.MIN_STEP_ROTATION_SPEED }); this.services.fanV2.getCharacteristic(this.hap.Characteristic.SwingMode) .on("get", this.getTargetSwingMode.bind(this)) .on("set", this.setTargetSwingMode.bind(this)); } async initFanV2Characteristics() { this.fanV2Characteristics = await this.localStorage.getItem(this.accessoryConfig.name) || this.fanV2Characteristics; this.logging.info(messages.INIT_TARGET_ACTIVE, this.fanV2Characteristics.targetActive); this.logging.info(messages.INIT_CURRENT_ACTIVE, this.fanV2Characteristics.currentActive); this.logging.info(messages.INIT_TARGET_ROTATION_SPEED, this.fanV2Characteristics.targetRotationSpeed); this.logging.info(messages.INIT_CURRENT_ROTATION_SPEED, this.fanV2Characteristics.currentRotationSpeed); this.logging.info(messages.INIT_TARGET_SWING_MODE, this.fanV2Characteristics.targetSwingMode); this.logging.info(messages.INIT_CURRENT_SWING_MODE, this.fanV2Characteristics.currentSwingMode); } async updateFanV2Characteristics() { if (this.canUpdateCurrentActive() === true) { await this.updateCurrentActive(); } else if (this.canUpdateCurrentRotationSpeed() === true) { await this.updateCurrentRotationSpeed(); } else if (this.canUpdateCurrentSwingMode() === true) { await this.updateCurrentSwingMode(); } } async saveFanV2Characteristics() { await this.localStorage.setItem(this.accessoryConfig.name, this.fanV2Characteristics); } getTargetActive(characteristicGetCallback) { characteristicGetCallback(this.alive !== undefined ? undefined : new Error(messages.DEVICE_NOT_CONNECTED), this.fanV2Characteristics.targetActive); } async setTargetActive(characteristicValue, characteristicSetCallback) { if (characteristicValue !== this.fanV2Characteristics.targetActive) { if (this.alive === true) { this.fanV2Characteristics.targetActive = characteristicValue; await this.saveFanV2Characteristics(); this.logging.info(messages.SET_TARGET_ACTIVE, this.fanV2Characteristics.targetActive); characteristicSetCallback(); } else { this.logging.error(messages.DEVICE_NOT_CONNECTED); characteristicSetCallback(new Error(messages.DEVICE_NOT_CONNECTED)); } } else { characteristicSetCallback(); } } canUpdateCurrentActive() { return this.fanV2Characteristics.currentActive !== this.fanV2Characteristics.targetActive && this.skips.updateCurrentActive === 0 && this.skips.updateCurrentSwingMode === 0; } async updateCurrentActive() { await this.sendDeviceData(constants.DATA_ACTIVE); this.fanV2Characteristics.currentActive = this.fanV2Characteristics.targetActive; if (this.fanV2Characteristics.currentActive === this.hap.Characteristic.Active.ACTIVE) { this.skips.updateCurrentActive = constants.SKIPS_UPDATE_CURRENT_ACTIVE_ACTIVE; } else if (this.fanV2Characteristics.currentActive === this.hap.Characteristic.Active.INACTIVE) { this.skips.updateCurrentActive = constants.SKIPS_UPDATE_CURRENT_ACTIVE_INACTIVE; } await this.saveFanV2Characteristics(); this.logging.info(messages.UPDATED_CURRENT_ACTIVE, this.fanV2Characteristics.currentActive); } doUpdateCurrentActiveSkip() { if (this.skips.updateCurrentActive > 0) { this.skips.updateCurrentActive--; } } getTargetRotationSpeed(characteristicGetCallback) { characteristicGetCallback(this.alive !== undefined ? undefined : new Error(messages.DEVICE_NOT_CONNECTED), this.fanV2Characteristics.targetRotationSpeed); } async setTargetRotationSpeed(characteristicValue, characteristicSetCallback) { let clampedCharacteristicValue = characteristicValue; if (clampedCharacteristicValue < constants.MIN_STEP_ROTATION_SPEED) { clampedCharacteristicValue = constants.MIN_STEP_ROTATION_SPEED; this.services.fanV2.updateCharacteristic(this.hap.Characteristic.RotationSpeed, clampedCharacteristicValue); this.logging.warn(messages.CLAMPED_TARGET_ROTATION_SPEED); } if (clampedCharacteristicValue !== this.fanV2Characteristics.targetRotationSpeed) { if (this.alive === true) { this.fanV2Characteristics.targetRotationSpeed = clampedCharacteristicValue; await this.saveFanV2Characteristics(); this.logging.info(messages.SET_TARGET_ROTATION_SPEED, this.fanV2Characteristics.targetRotationSpeed); characteristicSetCallback(); } else { this.logging.error(messages.DEVICE_NOT_CONNECTED); characteristicSetCallback(new Error(messages.DEVICE_NOT_CONNECTED)); } } else { characteristicSetCallback(); } } canUpdateCurrentRotationSpeed() { return this.fanV2Characteristics.currentRotationSpeed !== this.fanV2Characteristics.targetRotationSpeed && this.fanV2Characteristics.currentActive === this.hap.Characteristic.Active.ACTIVE && this.skips.updateCurrentActive === 0 && this.skips.updateCurrentSwingMode === 0; } async updateCurrentRotationSpeed() { if (this.fanV2Characteristics.currentRotationSpeed < this.fanV2Characteristics.targetRotationSpeed) { await this.sendDeviceData(constants.DATA_ROTATION_SPEED_INCREASE); this.fanV2Characteristics.currentRotationSpeed += constants.MIN_STEP_ROTATION_SPEED; } else if (this.fanV2Characteristics.currentRotationSpeed > this.fanV2Characteristics.targetRotationSpeed) { await this.sendDeviceData(constants.DATA_ROTATION_SPEED_DECREASE); this.fanV2Characteristics.currentRotationSpeed -= constants.MIN_STEP_ROTATION_SPEED; } await this.saveFanV2Characteristics(); this.logging.info(messages.UPDATED_CURRENT_ROTATION_SPEED, this.fanV2Characteristics.currentRotationSpeed); } getTargetSwingMode(characteristicGetCallback) { characteristicGetCallback(this.alive !== undefined ? undefined : new Error(messages.DEVICE_NOT_CONNECTED), this.fanV2Characteristics.targetSwingMode); } async setTargetSwingMode(characteristicValue, characteristicSetCallback) { if (characteristicValue !== this.fanV2Characteristics.targetSwingMode) { if (this.alive === true) { this.fanV2Characteristics.targetSwingMode = characteristicValue; await this.saveFanV2Characteristics(); this.logging.info(messages.SET_TARGET_SWING_MODE, this.fanV2Characteristics.targetSwingMode); characteristicSetCallback(); } else { this.logging.error(messages.DEVICE_NOT_CONNECTED); characteristicSetCallback(new Error(messages.DEVICE_NOT_CONNECTED)); } } else { characteristicSetCallback(); } } canUpdateCurrentSwingMode() { return this.fanV2Characteristics.currentSwingMode !== this.fanV2Characteristics.targetSwingMode && this.fanV2Characteristics.currentActive === this.hap.Characteristic.Active.ACTIVE && this.skips.updateCurrentActive === 0; } async updateCurrentSwingMode() { await this.sendDeviceData(constants.DATA_SWING_MODE); this.fanV2Characteristics.currentSwingMode = this.fanV2Characteristics.targetSwingMode; this.skips.updateCurrentSwingMode = constants.SKIPS_UPDATE_CURRENT_SWING_MODE; await this.saveFanV2Characteristics(); this.logging.info(messages.UPDATED_CURRENT_SWING_MODE, this.fanV2Characteristics.currentSwingMode); } doUpdateCurrentSwingModeSkip() { if (this.skips.updateCurrentSwingMode > 0) { this.skips.updateCurrentSwingMode--; } } initSensors() { this.device.on("temperature", (temp, humidity) => { this.setCurrentTemperature(temp); this.setCurrentRelativeHumidity(humidity); }); } initSensorServices() { this.services.temperatureSensor.getCharacteristic(this.hap.Characteristic.CurrentTemperature) .on("get", this.getCurrentTemperature.bind(this)) .setProps({ minStep: constants.MIN_STEP_CURRENT_TEMPERATURE }); this.services.humiditySensor.getCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity) .on("get", this.getCurrentRelativeHumidity.bind(this)) .setProps({ minStep: constants.MIN_STEP_CURRENT_RELATIVE_HUMIDITY }); } async updateSensorCharacteristics() { if (this.skips.updateSensorCharacteristics === 0) { await this.device.checkTemperature(); this.skips.updateSensorCharacteristics = constants.SKIPS_UPDATE_SENSOR_CHARACTERISTICS; } } doUpdateSensorCharacteristicsSkip() { if (this.skips.updateSensorCharacteristics > 0) { this.skips.updateSensorCharacteristics--; } } getCurrentTemperature(characteristicGetCallback) { characteristicGetCallback(this.alive !== undefined ? undefined : new Error(messages.DEVICE_NOT_CONNECTED), this.sensorCharacteristics.currentTemperature); } setCurrentTemperature(characteristicValue) { if (characteristicValue !== this.sensorCharacteristics.currentTemperature) { this.sensorCharacteristics.currentTemperature = characteristicValue; this.logging.info(messages.SET_CURRENT_TEMPERATURE, this.sensorCharacteristics.currentTemperature); } } getCurrentRelativeHumidity(characteristicGetCallback) { characteristicGetCallback(this.alive !== undefined ? undefined : new Error(messages.DEVICE_NOT_CONNECTED), this.sensorCharacteristics.currentRelativeHumidity); } setCurrentRelativeHumidity(characteristicValue) { if (characteristicValue !== this.sensorCharacteristics.currentRelativeHumidity) { this.sensorCharacteristics.currentRelativeHumidity = characteristicValue; this.logging.info(messages.SET_CURRENT_RELATIVE_HUMIDITY, this.sensorCharacteristics.currentRelativeHumidity); } } }