Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MQTT support for meter and curtain devices. #337

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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