From 96899b2e82e2024d2bc64aeec77cb8d745a80951 Mon Sep 17 00:00:00 2001 From: banboobee <98196664+banboobee@users.noreply.github.com> Date: Wed, 4 May 2022 06:49:30 +0900 Subject: [PATCH] MQTT support for meter and curtain devices. (#337) * v1.12.8 (#299) - Housekeeping and updated dependencies. **Full Changelog**: https://github.com/OpenWonderLabs/homebridge-switchbot/compare/v1.12.7...v1.12.8 * Added MQTT support for meter and curtain. * Fixed broken characters. * Fixed typo. * Fixed typo. Co-authored-by: Donavan Becker --- package.json | 1 + src/device/curtain.ts | 42 ++++++++++++++++++++++++++++++++++ src/device/meter.ts | 53 +++++++++++++++++++++++++++++++++++++++---- src/settings.ts | 4 ++++ 4 files changed, 95 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d908365a..e742f818 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@homebridge/plugin-ui-utils": "^0.0.19", "axios": "^0.26.1", "rxjs": "^7.5.5", + "async-mqtt": "^2.6.2", "fakegato-history": "^0.6.3" }, "devDependencies": { diff --git a/src/device/curtain.ts b/src/device/curtain.ts index d91e021b..bb75bd82 100644 --- a/src/device/curtain.ts +++ b/src/device/curtain.ts @@ -5,6 +5,8 @@ import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; import { DeviceURL, device, devicesConfig, serviceData, switchbot, deviceStatusResponse, payload, deviceStatus, ad } from '../settings'; import { Context } from 'vm'; +import { MqttClient } from 'mqtt'; +import { connectAsync } from 'async-mqtt'; export class Curtain { // Services @@ -59,12 +61,16 @@ export class Curtain { curtainUpdateInProgress!: boolean; doCurtainUpdate!: Subject; + //MQTT stuff + mqttClient: MqttClient | null = null; + constructor(private readonly platform: SwitchBotPlatform, private accessory: PlatformAccessory, public device: device & devicesConfig) { // default placeholders this.logs(device); this.refreshRate(device); this.scan(device); this.config(device); + this.setupMqtt(device); this.CurrentPosition = 0; this.TargetPosition = 0; this.PositionState = this.platform.Characteristic.PositionState.STOPPED; @@ -201,6 +207,35 @@ export class Curtain { }); } + /* + * Publish MQTT message for topics of + * 'homebridge-switchbot/curtain/xx:xx:xx:xx:xx:xx' + */ + mqttPublish(topic: string, message: any) { + const mac = this.device.deviceId?.toLowerCase().match(/[\s\S]{1,2}/g)?.join(':'); + const options = this.device.mqttPubOptions || {}; + this.mqttClient?.publish(`homebridge-switchbot/curtain/${mac}/${topic}`, `${message}`, options); + this.debugLog(`Meter: ${this.accessory.displayName} MQTT message: ${topic}/${message} options:${JSON.stringify(options)}`); + } + + /* + * Setup MQTT hadler if URL is specifed. + */ + async setupMqtt(device: device & devicesConfig): Promise { + if (device.mqttURL) { + try { + this.mqttClient = await connectAsync(device.mqttURL, device.mqttOptions || {}); + this.debugLog(`Meter: ${this.accessory.displayName} MQTT connection has been established successfully.`) + this.mqttClient.on('error', (e: Error) => { + this.errorLog(`Meter: ${this.accessory.displayName} Failed to publish MQTT messages. ${e}`) + }); + } catch (e) { + this.mqttClient = null; + this.errorLog(`Meter: ${this.accessory.displayName} Failed to establish MQTT connection. ${e}`) + } + } + } + /** * Parse the device status from the SwitchBot api */ @@ -656,18 +691,21 @@ export class Curtain { } else { this.windowCoveringService.updateCharacteristic(this.platform.Characteristic.CurrentPosition, Number(this.CurrentPosition)); this.debugLog(`Curtain: ${this.accessory.displayName} updateCharacteristic CurrentPosition: ${this.CurrentPosition}`); + this.mqttPublish('CurrentPosition', this.CurrentPosition); } if (this.PositionState === undefined) { this.debugLog(`Curtain: ${this.accessory.displayName} PositionState: ${this.PositionState}`); } else { this.windowCoveringService.updateCharacteristic(this.platform.Characteristic.PositionState, Number(this.PositionState)); this.debugLog(`Curtain: ${this.accessory.displayName} updateCharacteristic PositionState: ${this.PositionState}`); + this.mqttPublish('PositionState', this.PositionState); } if (this.TargetPosition === undefined || Number.isNaN(this.TargetPosition)) { this.debugLog(`Curtain: ${this.accessory.displayName} TargetPosition: ${this.TargetPosition}`); } else { this.windowCoveringService.updateCharacteristic(this.platform.Characteristic.TargetPosition, Number(this.TargetPosition)); this.debugLog(`Curtain: ${this.accessory.displayName} updateCharacteristic TargetPosition: ${this.TargetPosition}`); + this.mqttPublish('TargetPosition', this.TargetPosition); } if (!this.device.curtain?.hide_lightsensor) { if (this.CurrentAmbientLightLevel === undefined || Number.isNaN(this.CurrentAmbientLightLevel)) { @@ -675,6 +713,7 @@ export class Curtain { } else { this.lightSensorService?.updateCharacteristic(this.platform.Characteristic.CurrentAmbientLightLevel, this.CurrentAmbientLightLevel); this.debugLog(`Curtain: ${this.accessory.displayName}` + ` updateCharacteristic CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`); + this.mqttPublish('CurrentAmbientLightLevel', this.CurrentAmbientLightLevel); } } if (this.device.ble) { @@ -683,12 +722,14 @@ export class Curtain { } else { this.batteryService?.updateCharacteristic(this.platform.Characteristic.BatteryLevel, this.BatteryLevel); this.debugLog(`Curtain: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); + this.mqttPublish('BatteryLevel', this.BatteryLevel); } if (this.StatusLowBattery === undefined) { this.debugLog(`Curtain: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); } else { this.batteryService?.updateCharacteristic(this.platform.Characteristic.StatusLowBattery, this.StatusLowBattery); this.debugLog(`Curtain: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); + this.mqttPublish('StatusLowBattery', this.StatusLowBattery); } } } @@ -745,6 +786,7 @@ export class Curtain { this.debugLog(`Curtain: ${this.accessory.displayName} TargetPosition: ${value}`); this.TargetPosition = value; + this.mqttPublish('TargetPosition', this.TargetPosition); await this.setMinMax(); if (value > this.CurrentPosition) { diff --git a/src/device/meter.ts b/src/device/meter.ts index 4d08d391..44667cd6 100644 --- a/src/device/meter.ts +++ b/src/device/meter.ts @@ -4,6 +4,8 @@ import { SwitchBotPlatform } from '../platform'; import { Service, PlatformAccessory, Units, CharacteristicValue } from 'homebridge'; import { DeviceURL, device, devicesConfig, serviceData, ad, switchbot, deviceStatusResponse, temperature, deviceStatus } from '../settings'; import { Context } from 'vm'; +import { MqttClient } from 'mqtt'; +import { connectAsync } from 'async-mqtt'; import { hostname } from "os"; /** @@ -52,6 +54,9 @@ export class Meter { meterUpdateInProgress!: boolean; doMeterUpdate: Subject; + //MQTT stuff + mqttClient: MqttClient | null = null; + // EVE history service handler historyService: any; @@ -62,6 +67,7 @@ export class Meter { this.setupHistoryService(device); this.refreshRate(device); this.config(device); + this.setupMqtt(device); if (this.CurrentRelativeHumidity === undefined) { this.CurrentRelativeHumidity = 0; } else { @@ -171,6 +177,35 @@ export class Meter { }); } + /* + * Publish MQTT message for topics of + * 'homebridge-switchbot/meter/xx:xx:xx:xx:xx:xx' + */ + mqttPublish(message: any) { + const mac = this.device.deviceId?.toLowerCase().match(/[\s\S]{1,2}/g)?.join(':'); + const options = this.device.mqttPubOptions || {}; + this.mqttClient?.publish(`homebridge-switchbot/meter/${mac}`, `${message}`, options); + this.debugLog(`Meter: ${this.accessory.displayName} MQTT message: ${message} options:${JSON.stringify(options)}`); + } + + /* + * Setup MQTT hadler if URL is specifed. + */ + async setupMqtt(device: device & devicesConfig): Promise { + if (device.mqttURL) { + try { + this.mqttClient = await connectAsync(device.mqttURL, device.mqttOptions || {}); + this.debugLog(`Meter: ${this.accessory.displayName} MQTT connection has been established successfully.`) + this.mqttClient.on('error', (e: Error) => { + this.errorLog(`Meter: ${this.accessory.displayName} Failed to publish MQTT messages. ${e}`) + }); + } catch (e) { + this.mqttClient = null; + this.errorLog(`Meter: ${this.accessory.displayName} Failed to establish MQTT connection. ${e}`) + } + } + } + /* * Setup EVE history graph feature if enabled. */ @@ -205,7 +240,7 @@ export class Meter { } else { this.StatusLowBattery = this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}, StatusLowBattery: ${this.StatusLowBattery}`); + this.debugLog(`Meter: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}, StatusLowBattery: ${this.StatusLowBattery}`); // Humidity if (!this.device.meter?.hide_humidity) { @@ -279,10 +314,12 @@ export class Meter { this.infoLog(`Meter: ${this.accessory.displayName} BLE Address Found: ${this.address}`); this.infoLog(`Meter: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac}`); } - this.serviceData = ad.serviceData; - this.temperature = ad.serviceData.temperature!.c; - this.humidity = ad.serviceData.humidity; - this.battery = ad.serviceData.battery; + if (ad.serviceData.humidity! > 0) { // reject unreliable data + this.serviceData = ad.serviceData; + this.temperature = ad.serviceData.temperature!.c; + this.humidity = ad.serviceData.humidity; + this.battery = ad.serviceData.battery; + } this.debugLog(`Meter: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); this.debugLog( `Meter: ${this.accessory.displayName} model: ${ad.serviceData.model}, modelName: ${ad.serviceData.modelName}, ` + @@ -383,6 +420,7 @@ export class Meter { * Updates the status for each of the HomeKit Characteristics */ async updateHomeKitCharacteristics(): Promise { + let mqttmessage: string[] = []; let entry = {time: Math.round(new Date().valueOf()/1000)}; if (!this.device.meter?.hide_humidity) { if (this.CurrentRelativeHumidity === undefined) { @@ -390,6 +428,7 @@ export class Meter { } else { this.humidityservice?.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, this.CurrentRelativeHumidity); this.debugLog(`Meter: ${this.accessory.displayName} updateCharacteristic CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + mqttmessage.push(`"humidity": ${this.CurrentRelativeHumidity}`); entry["humidity"] = this.CurrentRelativeHumidity; } } @@ -399,6 +438,7 @@ export class Meter { } else { this.temperatureservice?.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.CurrentTemperature); this.debugLog(`Meter: ${this.accessory.displayName} updateCharacteristic CurrentTemperature: ${this.CurrentTemperature}`); + mqttmessage.push(`"temperature": ${this.CurrentTemperature}`); entry["temp"] = this.CurrentTemperature; } } @@ -408,14 +448,17 @@ export class Meter { } else { this.batteryService?.updateCharacteristic(this.platform.Characteristic.BatteryLevel, this.BatteryLevel); this.debugLog(`Meter: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); + mqttmessage.push(`"battery": ${this.BatteryLevel}`); } if (this.StatusLowBattery === undefined) { this.debugLog(`Meter: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); } else { this.batteryService?.updateCharacteristic(this.platform.Characteristic.StatusLowBattery, this.StatusLowBattery); this.debugLog(`Meter: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); + mqttmessage.push(`"lowBattery": ${this.StatusLowBattery}`); } } + this.mqttPublish(`{${mqttmessage.join(',')}}`) if (this.CurrentRelativeHumidity > 0) { // reject unreliable data this.historyService?.addEntry(entry); } diff --git a/src/settings.ts b/src/settings.ts index 14bcb3e1..5136c104 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,4 +1,5 @@ import { MacAddress, PlatformConfig } from 'homebridge'; +import { IClientOptions } from 'async-mqtt'; /** * This is the name of the platform that users will use to register the plugin in the Homebridge config.json */ @@ -58,6 +59,9 @@ export interface devicesConfig extends device { scanDuration?: number; hide_device?: boolean; offline?: boolean; + mqttURL?: string; + mqttOptions?: IClientOptions; + mqttPubOptions?: IClientOptions; history?: boolean; }