Skip to content

Commit

Permalink
Merge pull request #30 from mdaskalov/z2m-improvements
Browse files Browse the repository at this point in the history
Support more Zigbee2MQTT devices
  • Loading branch information
mdaskalov authored Nov 13, 2022
2 parents 6440a8d + 801adc0 commit 10bd2c0
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 276 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ By configuring a `powerTopic` it is possible to combine devices to a singe HomeK

# Zigbee2MQTT

It is also possible to controll devices using Zigbee2MQTT gateway/bridge. This is useful if you want to combine Zigbee and Tasmota devices using the `powerTopic`. Currently only `Switch` and `Lightbulb` accessories are supported. Supported features are queried directly from Zigbe2MQTT and configured automatically.
It is also possible to controll devices using Zigbee2MQTT gateway/bridge. This is useful if you want to combine Zigbee and Tasmota devices using the `powerTopic`.

Almost all accessory-types are supported but currently some characteristics are not mapped correctly (work in progress). Supported features are queried directly from Zigbe2MQTT and configured automatically.

# Binding with Zigbee2Tasmota

Expand Down
29 changes: 16 additions & 13 deletions src/mqttClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type DeviceHandler = {
callback: DeviceCallback;
};

export const DEFALT_TIMEOUT = 5000;

export class MQTTClient {

private topicHandlers: Array<TopicHandler> = [];
Expand Down Expand Up @@ -179,25 +181,26 @@ export class MQTTClient {
this.log.debug('MQTT: Published: %s %s', topic, message);
}

submit(topic: string, message: string, responseTopic = topic, timeOut = 2000): Promise<string> {
return new Promise((resolve: (message) => void, reject) => {
const startTS = Date.now();

const handlerId = this.subscribeTopic(responseTopic, msg => {
read(topic: string, timeout?: number, messageDump?: boolean): Promise<string> {
return new Promise((resolve: (message: string) => void, reject) => {
const start = Date.now();
const handlerId = this.subscribeTopic(topic, message => {
clearTimeout(timer);
resolve(msg);
}, true);

resolve(message);
}, messageDump === undefined ? true : messageDump, true);
const timer = setTimeout(() => {
this.log.error('MQTT: Submit: timeout after %sms %s',
Date.now() - startTS, message);
if (handlerId !== undefined) {
this.unsubscribe(handlerId);
}
reject('MQTT: Submit timeout');
}, timeOut);
this.publish(topic, message);
const elapsed = Date.now() - start;
reject(`MQTT: Read timeout after ${elapsed}ms`);
}, timeout === undefined ? DEFALT_TIMEOUT : timeout);
});
}

submit(topic: string, message: string, responseTopic = topic, timeout?: number, messageDump?: boolean): Promise<string> {
this.publish(topic, message);
return this.read(responseTopic, timeout, messageDump);
}
}

74 changes: 33 additions & 41 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@ import { ZbBridgeDevice } from './zbBridgeAccessory';
import { ZbBridgeLightbulb } from './zbBridgeLightbulb';
import { ZbBridgeSwitch } from './zbBridgeSwitch';
import { ZbBridgeSensor } from './zbBridgeSensor';
import { Zigbee2MQTTAcessory, Z2MDevice, Zigbee2MQTTDevice } from './zigbee2MQTTAcessory';
import { Zigbee2MQTTAcessory, Zigbee2MQTTDevice } from './zigbee2MQTTAcessory';

export class TasmotaZbBridgePlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
public readonly mqttClient = new MQTTClient(this.log, this.config);
// cached accessories
public readonly accessories: PlatformAccessory[] = [];
// zigbee2mqtt devices
public zigbee2mqttDevices: Z2MDevice[] = [];

constructor(
public readonly log: Logger,
Expand All @@ -29,15 +27,7 @@ export class TasmotaZbBridgePlatform implements DynamicPlatformPlugin {
log.debug('Executed didFinishLaunching callback');
this.cleanupCachedDevices();
if (Array.isArray(this.config.zigbee2mqttDevices) && this.config.zigbee2mqttDevices.length > 0) {
if (config.zigbee2mqttTopic === undefined) {
config.zigbee2mqttTopic = 'zigbee2mqtt';
}
this.mqttClient.subscribeTopic(config.zigbee2mqttTopic + '/bridge/devices', message => {
const devices: Z2MDevice[] = JSON.parse(message);
this.zigbee2mqttDevices = devices;
this.log.info('Found %s zigbee2mqtt devices', devices.length);
this.discoverZigbee2MQTTDevices();
}, false, true);
this.discoverZigbee2MQTTDevices();
}
if (Array.isArray(this.config.zbBridgeDevices) && this.config.zbBridgeDevices.length > 0) {
this.discoverZbBridgeDevices();
Expand All @@ -63,9 +53,8 @@ export class TasmotaZbBridgePlatform implements DynamicPlatformPlugin {
}

zigbee2MQTTDeviceUUID(device: Zigbee2MQTTDevice): string {
const identificator = device.ieee_address +
(device.powerTopic || '') +
(device.powerType || '');
const identificator = 'z2m' + device.ieee_address +
(device.powerTopic || '');
return this.api.hap.uuid.generate(identificator);
}

Expand All @@ -92,16 +81,6 @@ export class TasmotaZbBridgePlatform implements DynamicPlatformPlugin {
}
}

createZigbee2MQTTAcessory(accessory: PlatformAccessory) {
const device = this.zigbee2mqttDevices.find(d => d.ieee_address === accessory.context.device.addr);
if (device !== undefined) {
const serviceName = Zigbee2MQTTAcessory.getServiceName(device);
if (serviceName !== undefined) {
new Zigbee2MQTTAcessory(this, accessory, serviceName);
}
}
}

restoreAccessory(uuid: string, name: string): { restored: boolean; accessory: PlatformAccessory<UnknownContext> } {
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
Expand Down Expand Up @@ -129,26 +108,39 @@ export class TasmotaZbBridgePlatform implements DynamicPlatformPlugin {
}
}

discoverZigbee2MQTTDevices() {
for (const device of this.config.zigbee2mqttDevices) {
if ((<Zigbee2MQTTDevice>device)?.ieee_address && (<Zigbee2MQTTDevice>device)?.name) {
const z2mDevice = this.zigbee2mqttDevices.find(d => d.ieee_address === device.ieee_address);
if (z2mDevice !== undefined) {
const serviceName = Zigbee2MQTTAcessory.getServiceName(z2mDevice);
if (serviceName !== undefined) {
const { restored, accessory } = this.restoreAccessory(this.zigbee2MQTTDeviceUUID(device), device.name);
async discoverZigbee2MQTTDevices() {
if (this.config.zigbee2mqttTopic === undefined) {
this.config.zigbee2mqttTopic = 'zigbee2mqtt';
}
try {
const bridgeDevicesTopic = this.config.zigbee2mqttTopic + '/bridge/devices';
const message = await this.mqttClient.read(bridgeDevicesTopic, undefined, false);
const z2m_devices: Zigbee2MQTTDevice[] = JSON.parse(message);
if (!Array.isArray(z2m_devices)) {
throw (`topic (${bridgeDevicesTopic}) parse error`);
}
this.log.info('Found %s Zigbee2MQTT devices', z2m_devices.length);

for (const configured of this.config.zigbee2mqttDevices) {
if (configured.ieee_address && configured.name) {
const device = z2m_devices.find(d => d.ieee_address === configured.ieee_address);
if (device !== undefined) {
device.homekit_name = configured.name;
if (configured.powerTopic !== undefined) {
device.powerTopic = configured.powerTopic + '/' + (configured.powerType || 'POWER');
}
const { restored, accessory } = this.restoreAccessory(this.zigbee2MQTTDeviceUUID(device), configured.name);
accessory.context.device = device;
new Zigbee2MQTTAcessory(this, accessory, serviceName);
this.log.info('%s Zigbee2MQTTAcessory accessory: %s (%s) - %s',
restored ? 'Restoring' : 'Adding', device.name, device.ieee_address, serviceName);
} else {
this.log.error('Ignored unsupported Zigbee2MQTT device %s (%s)', device.name, device.ieee_address);
new Zigbee2MQTTAcessory(this, accessory);
this.log.info('%s Zigbee2MQTTAcessory accessory: %s (%s)',
restored ? 'Restoring' : 'Adding', configured.name, configured.ieee_address);
}
} else {
this.log.error('Ignored invalid Zigbee2MQTT configuration: %s', JSON.stringify(configured));
}
} else {
this.log.error('Ignored Zigbee2MQTT device configuration: ', JSON.stringify(device));
continue;
}
} catch (err) {
this.log.error(`Zigbee2MQTT devices initialization failed: ${err}`);
}
}

Expand Down
Loading

0 comments on commit 10bd2c0

Please sign in to comment.