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
JavaScript
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);
}
}
}