Skip to content

Commit

Permalink
MQTT support for meter and curtain devices. (#337)
Browse files Browse the repository at this point in the history
* v1.12.8 (#299)

- Housekeeping and updated dependencies.

**Full Changelog**: 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 <[email protected]>
  • Loading branch information
banboobee and donavanbecker authored May 3, 2022
1 parent b56baa1 commit 96899b2
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 5 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
42 changes: 42 additions & 0 deletions src/device/curtain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -59,12 +61,16 @@ export class Curtain {
curtainUpdateInProgress!: boolean;
doCurtainUpdate!: Subject<void>;

//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;
Expand Down Expand Up @@ -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<void> {
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
*/
Expand Down Expand Up @@ -656,25 +691,29 @@ 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)) {
this.debugLog(`Curtain: ${this.accessory.displayName} CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`);
} 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) {
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -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) {
Expand Down
53 changes: 48 additions & 5 deletions src/device/meter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -52,6 +54,9 @@ export class Meter {
meterUpdateInProgress!: boolean;
doMeterUpdate: Subject<void>;

//MQTT stuff
mqttClient: MqttClient | null = null;

// EVE history service handler
historyService: any;

Expand All @@ -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 {
Expand Down Expand Up @@ -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<void> {
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.
*/
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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}, ` +
Expand Down Expand Up @@ -383,13 +420,15 @@ export class Meter {
* Updates the status for each of the HomeKit Characteristics
*/
async updateHomeKitCharacteristics(): Promise<void> {
let mqttmessage: string[] = [];
let entry = {time: Math.round(new Date().valueOf()/1000)};
if (!this.device.meter?.hide_humidity) {
if (this.CurrentRelativeHumidity === undefined) {
this.debugLog(`Meter: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`);
} 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;
}
}
Expand All @@ -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;
}
}
Expand All @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -58,6 +59,9 @@ export interface devicesConfig extends device {
scanDuration?: number;
hide_device?: boolean;
offline?: boolean;
mqttURL?: string;
mqttOptions?: IClientOptions;
mqttPubOptions?: IClientOptions;
history?: boolean;
}

Expand Down

0 comments on commit 96899b2

Please sign in to comment.