Skip to content

Commit

Permalink
v2.10.0 (#851)
Browse files Browse the repository at this point in the history
## [Version 2.10.0](https://github.com/OpenWonderLabs/homebridge-switchbot/releases/tag/v2.10.0) (2023-10-31)

## What's Changed

- Added webhook event listener for Meter, Meter Plus, & Hub 2, Thanks [@banboobee](https://github.com/banboobee) [#850](#850)
- Housekeeping and updated dependencies.

**Full Changelog**: <v2.9.2....v2.10.0>

Co-authored-by: banboobee <98196664+banboobee@users.noreply.github.com>
  • Loading branch information
donavanbecker and banboobee committed Nov 1, 2023
1 parent bc7837e commit c34149d
Show file tree
Hide file tree
Showing 8 changed files with 615 additions and 93 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to this project will be documented in this file. This project uses [Semantic Versioning](https://semver.org/)

## [Version 2.10.0](https://github.com/OpenWonderLabs/homebridge-switchbot/releases/tag/v2.10.0) (2023-10-31)

## What's Changed

- Added webhook event listener for Meter, Meter Plus, & Hub 2, Thanks [@banboobee](https://github.com/banboobee) [#850](https://github.com/OpenWonderLabs/homebridge-switchbot/pull/850)
- Housekeeping and updated dependencies.

**Full Changelog**: <https://github.com/OpenWonderLabs/homebridge-switchbot/compare/v2.9.2....v2.10.0>

## [Version 2.9.2](https://github.com/OpenWonderLabs/homebridge-switchbot/releases/tag/v2.9.2) (2023-10-26)

## What's Changed
Expand Down
428 changes: 341 additions & 87 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"displayName": "Homebridge SwitchBot",
"name": "@switchbot/homebridge-switchbot",
"version": "2.9.2",
"version": "2.10.0",
"description": "The [Homebridge](https://homebridge.io) SwitchBot plugin allows you to access your [SwitchBot](https://www.switch-bot.com) device(s) from HomeKit.",
"author": "SwitchBot <support@wondertechlabs.com> (https://github.com/SwitchBot)",
"license": "ISC",
Expand Down Expand Up @@ -58,7 +58,7 @@
"@homebridge/plugin-ui-utils": "^1.0.0",
"async-mqtt": "^2.6.3",
"fakegato-history": "^0.6.4",
"homebridge-lib": "^6.6.3",
"homebridge-lib": "^6.6.4",
"rxjs": "^7.8.1",
"undici": "^5.27.0"
},
Expand All @@ -68,9 +68,9 @@
"node-switchbot": "^1.9.0"
},
"devDependencies": {
"@types/node": "^20.8.9",
"@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.9.0",
"@types/node": "^20.8.10",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"eslint": "^8.52.0",
"homebridge": "^1.6.1",
"nodemon": "^3.0.1",
Expand Down
26 changes: 25 additions & 1 deletion src/device/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,30 @@ export class Hub {
.subscribe(async () => {
await this.refreshStatus();
});

//regisiter webhook event handler
if (this.device.webhook) {
this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`);
this.platform.webhookEventHandler[this.device.deviceId] = async (context) => {
try {
this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`);
if (context.scale === 'CELSIUS') {
const { temperature, humidity } = context;
const { CurrentTemperature, CurrentRelativeHumidity } = this;
this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` +
'(temperature, humidity) = ' +
`Webhook:(${temperature}, ${humidity}), ` +
`current:(${CurrentTemperature}, ${CurrentRelativeHumidity})`);
this.CurrentRelativeHumidity = humidity;
this.CurrentTemperature = temperature;
this.updateHomeKitCharacteristics();
}
} catch (e: any) {
this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} `
+ `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`);
}
};
}
}

/**
Expand Down Expand Up @@ -422,7 +446,7 @@ export class Hub {
.match(/[\s\S]{1,2}/g)
?.join(':');
const options = this.device.mqttPubOptions || {};
this.mqttClient?.publish(`homebridge-switchbot/meter/${mac}`, `${message}`, options);
this.mqttClient?.publish(`homebridge-switchbot/hub/${mac}`, `${message}`, options);
this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT message: ${message} options:${JSON.stringify(options)}`);
}

Expand Down
24 changes: 24 additions & 0 deletions src/device/meter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,30 @@ export class Meter {
.subscribe(async () => {
await this.refreshStatus();
});

//regisiter webhook event handler
if (this.device.webhook) {
this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`);
this.platform.webhookEventHandler[this.device.deviceId] = async (context) => {
try {
this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`);
if (context.scale === 'CELSIUS') {
const { temperature, humidity } = context;
const { CurrentTemperature, CurrentRelativeHumidity } = this;
this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` +
'(temperature, humidity) = ' +
`Webhook:(${temperature}, ${humidity}), ` +
`current:(${CurrentTemperature}, ${CurrentRelativeHumidity})`);
this.CurrentRelativeHumidity = humidity;
this.CurrentTemperature = temperature;
this.updateHomeKitCharacteristics();
}
} catch (e: any) {
this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} `
+ `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`);
}
};
}
}

/**
Expand Down
24 changes: 24 additions & 0 deletions src/device/meterplus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,30 @@ export class MeterPlus {
.subscribe(async () => {
await this.refreshStatus();
});

//regisiter webhook event handler
if (this.device.webhook) {
this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`);
this.platform.webhookEventHandler[this.device.deviceId] = async (context) => {
try {
this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`);
if (context.scale === 'CELSIUS') {
const { temperature, humidity } = context;
const { CurrentTemperature, CurrentRelativeHumidity } = this;
this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` +
'(temperature, humidity) = ' +
`Webhook:(${temperature}, ${humidity}), ` +
`current:(${CurrentTemperature}, ${CurrentRelativeHumidity})`);
this.CurrentRelativeHumidity = humidity;
this.CurrentTemperature = temperature;
this.updateHomeKitCharacteristics();
}
} catch (e: any) {
this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} `
+ `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`);
}
};
}
}

/**
Expand Down
182 changes: 182 additions & 0 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import { Buffer } from 'buffer';
import { queueScheduler } from 'rxjs';
import fakegato from 'fakegato-history';
import { EveHomeKitTypes } from 'homebridge-lib';
import { MqttClient } from 'mqtt';
import { connectAsync } from 'async-mqtt';
import * as http from 'http';

import { readFileSync, writeFileSync } from 'fs';
import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, Service, Characteristic } from 'homebridge';
Expand All @@ -49,9 +52,12 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin {
version = process.env.npm_package_version || '2.9.0';
debugMode!: boolean;
platformLogging?: string;
webhookEventListener: http.Server | null = null;
mqttClient: MqttClient | null = null;

public readonly fakegatoAPI: any;
public readonly eve: any;
public readonly webhookEventHandler: { [x: string]: (context: { [x: string]: any }) => void } = {};

constructor(
public readonly log: Logger,
Expand Down Expand Up @@ -108,6 +114,182 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin {
this.debugErrorLog(`Failed to Discover, Error: ${e}`);
}
});

this.setupMqtt();
this.setupwebhook();
}

async setupMqtt(): Promise<void> {
if (this.config.options?.mqttURL) {
try {
this.mqttClient = await connectAsync(this.config.options?.mqttURL, this.config.options.mqttOptions || {});
this.debugLog('MQTT connection has been established successfully.');
this.mqttClient.on('error', (e: Error) => {
this.errorLog(`Failed to publish MQTT messages. ${e}`);
});
if (!this.config.options?.webhookURL) {
// receive webhook events via MQTT
this.infoLog(`Webhook is configured to be received through ${this.config.options.mqttURL}/homebridge-switchbot/webhook.`);
this.mqttClient.subscribe('homebridge-switchbot/webhook/+');
this.mqttClient.on('message', async (topic: string, message) => {
try {
this.debugLog(`Received Webhook via MQTT: ${topic}=${message}`);
const context = JSON.parse(message.toString());
await this.webhookEventHandler[context.deviceMac]?.(context);
} catch (e: any) {
this.errorLog(`Failed to handle webhook event. Error:${e}`);
}
});
}
} catch (e) {
this.mqttClient = null;
this.errorLog(`Failed to establish MQTT connection. ${e}`);
}
}
}

async setupwebhook() {
//webhook configutation
if (this.config.options?.webhookURL) {
const url = this.config.options?.webhookURL;

try {
const xurl = new URL(url);
const port = Number(xurl.port);
const path = xurl.pathname;
this.webhookEventListener = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => {
try {
if (request.url === path && request.method === 'POST') {
request.on('data', async (data) => {
try {
const body = JSON.parse(data);
this.debugLog(`Received Webhook: ${JSON.stringify(body)}`);
if (this.config.options?.mqttURL) {
const mac = body.context.deviceMac
?.toLowerCase()
.match(/[\s\S]{1,2}/g)
?.join(':');
const options = this.config.options?.mqttPubOptions || {};
this.mqttClient?.publish(`homebridge-switchbot/webhook/${mac}`, `${JSON.stringify(body.context)}`, options);
}
await this.webhookEventHandler[body.context.deviceMac]?.(body.context);
} catch (e: any) {
this.errorLog(`Failed to handle webhook event. Error:${e}`);
}
});
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.end('OK');
}
// else {
// response.writeHead(403, {'Content-Type': 'text/plain'});
// response.end(`NG`);
// }
} catch (e: any) {
this.errorLog(`Failed to handle webhook event. Error:${e}`);
}
}).listen(port ? port : 80);
} catch (e: any) {
this.errorLog(`Failed to create webhook listener. Error:${e.message}`);
return;
}

try {
const { body, statusCode, headers } = await request(
'https://api.switch-bot.com/v1.1/webhook/setupWebhook', {
method: 'POST',
headers: this.generateHeaders(),
body: JSON.stringify({
'action': 'setupWebhook',
'url': url,
'deviceList': 'ALL',
}),
});
const response: any = await body.json();
this.debugLog(`setupWebhook: url:${url}`);
this.debugLog(`setupWebhook: body:${JSON.stringify(response)}`);
this.debugLog(`setupWebhook: statusCode:${statusCode}`);
this.debugLog(`setupWebhook: headers:${JSON.stringify(headers)}`);
if (statusCode !== 200 || response?.statusCode !== 100) {
this.errorLog(`Failed to configure webhook. Existing webhook well be overridden. HTTP:${statusCode} API:${response?.statusCode} `
+ `message:${response?.message}`);
}
} catch (e: any) {
this.errorLog(`Failed to configure webhook. Error: ${e.message}`);
}

try {
const { body, statusCode, headers } = await request(
'https://api.switch-bot.com/v1.1/webhook/updateWebhook', {
method: 'POST',
headers: this.generateHeaders(),
body: JSON.stringify({
'action': 'updateWebhook',
'config': {
'url': url,
'enable': true,
},
}),
});
const response: any = await body.json();
this.debugLog(`updateWebhook: url:${url}`);
this.debugLog(`updateWebhook: body:${JSON.stringify(response)}`);
this.debugLog(`updateWebhook: statusCode:${statusCode}`);
this.debugLog(`updateWebhook: headers:${JSON.stringify(headers)}`);
if (statusCode !== 200 || response?.statusCode !== 100) {
this.errorLog(`Failed to update webhook. HTTP:${statusCode} API:${response?.statusCode} message:${response?.message}`);
}
} catch (e: any) {
this.errorLog(`Failed to update webhook. Error:${e.message}`);
}

try {
const { body, statusCode, headers } = await request(
'https://api.switch-bot.com/v1.1/webhook/queryWebhook', {
method: 'POST',
headers: this.generateHeaders(),
body: JSON.stringify({
'action': 'queryUrl',
}),
});
const response: any = await body.json();
this.debugLog(`queryWebhook: body:${JSON.stringify(response)}`);
this.debugLog(`queryWebhook: statusCode:${statusCode}`);
this.debugLog(`queryWebhook: headers:${JSON.stringify(headers)}`);
if (statusCode !== 200 || response?.statusCode !== 100) {
this.errorLog(`Failed to query webhook. HTTP:${statusCode} API:${response?.statusCode} message:${response?.message}`);
} else {
this.infoLog(`Listening webhook on ${response?.body?.urls[0]}`);
}
} catch (e: any) {
this.errorLog(`Failed to query webhook. Error:${e}`);
}

this.api.on('shutdown', async () => {
try {
const { body, statusCode, headers } = await request(
'https://api.switch-bot.com/v1.1/webhook/deleteWebhook', {
method: 'POST',
headers: this.generateHeaders(),
body: JSON.stringify({
'action': 'deleteWebhook',
'url': url,
}),
});
const response: any = await body.json();
this.debugLog(`deleteWebhook: url:${url}`);
this.debugLog(`deleteWebhook: body:${JSON.stringify(response)}`);
this.debugLog(`deleteWebhook: statusCode:${statusCode}`);
this.debugLog(`deleteWebhook: headers:${JSON.stringify(headers)}`);
if (statusCode !== 200 || response?.statusCode !== 100) {
this.errorLog(`Failed to delete webhook. HTTP:${statusCode} API:${response?.statusCode} message:${response?.message}`);
} else {
this.infoLog('Unregistered webhook to close listening.');
}
} catch (e: any) {
this.errorLog(`Failed to delete webhook. Error:${e.message}`);
}
});
}
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export type options = {
logging?: string;
devices?: Array<devicesConfig>;
irdevices?: Array<irDevicesConfig>;
webhookURL?: string;
mqttURL?: string;
mqttOptions?: IClientOptions;
mqttPubOptions?: IClientOptions;
};

export interface devicesConfig extends device {
Expand All @@ -56,6 +60,7 @@ export interface devicesConfig extends device {
mqttOptions?: IClientOptions;
mqttPubOptions?: IClientOptions;
history?: boolean;
webhook?: boolean;
bot?: bot;
meter?: meter;
humidifier?: humidifier;
Expand Down

0 comments on commit c34149d

Please sign in to comment.