@dotwee/homebridge-z2m
Version:
Expose your Zigbee devices to HomeKit with ease, by integrating Zigbee2MQTT with Homebridge.
535 lines • 25.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Zigbee2mqttPlatform = void 0;
const settings_1 = require("./settings");
const platformAccessory_1 = require("./platformAccessory");
const configModels_1 = require("./configModels");
const mqtt = __importStar(require("mqtt"));
const fs = __importStar(require("fs"));
const z2mModels_1 = require("./z2mModels");
const semver = __importStar(require("semver"));
const helpers_1 = require("./helpers");
const creators_1 = require("./converters/creators");
class Zigbee2mqttPlatform {
constructor(log, config, api) {
this.log = log;
this.api = api;
// this is used to track restored cached accessories
this.accessories = [];
this.lastReceivedDevices = [];
this.lastReceivedGroups = [];
this.groupUpdatePending = false;
this.deviceUpdatePending = false;
// Prepare internal states, variables and such
this.onMessage = this.onMessage.bind(this);
this.didReceiveDevices = false;
this.lastReceivedZigbee2MqttVersion = undefined;
// Set device defaults
this.baseDeviceConfig = {};
// Validate configuration
if ((0, configModels_1.isPluginConfiguration)(config, creators_1.BasicServiceCreatorManager.getInstance(), log)) {
this.config = config;
}
else {
log.error(`INVALID CONFIGURATION FOR PLUGIN: ${settings_1.PLUGIN_NAME}\nThis plugin will NOT WORK until this problem is resolved.`);
return;
}
// Use configuration
if (this.config !== undefined) {
// Normalize experimental feature flags
if (this.config.experimental !== undefined) {
this.config.experimental = this.config.experimental.map((feature) => feature.trim().toLocaleUpperCase());
if (this.config.experimental.length > 0) {
this.log.warn(`Experimental features enabled: ${this.config.experimental.join(', ')}`);
}
}
// Merge defaults from the plugin configuration
if (this.config.defaults !== undefined) {
this.baseDeviceConfig = { ...this.baseDeviceConfig, ...this.config.defaults };
}
if (this.baseDeviceConfig.exclude === false) {
// Set to undefined; as this is already the default behavior and might conflict with exclude_grouped_devices otherwise.
this.log.debug('Changing default value for exclude from false to undefined.');
this.baseDeviceConfig.exclude = undefined;
}
this.log.debug(`Default device config: ${JSON.stringify(this.baseDeviceConfig)}`);
this.mqttClient = this.initializeMqttClient(this.config);
}
}
initializeMqttClient(config) {
if (!config.mqtt.server || !config.mqtt.base_topic) {
this.log.error('No MQTT server and/or base_topic defined!');
}
this.log.info(`Connecting to MQTT server at ${config.mqtt.server}`);
const options = Zigbee2mqttPlatform.createMqttOptions(this.log, config);
const mqttClient = mqtt.connect(config.mqtt.server, options);
mqttClient.on('connect', () => {
this.log.info('Connected to MQTT server');
setTimeout(() => {
if (!this.didReceiveDevices) {
this.log.error('DID NOT RECEIVE ANY DEVICES AFTER BEING CONNECTED FOR TWO MINUTES.\n' +
`Please verify that Zigbee2MQTT is running and that it is v${Zigbee2mqttPlatform.MIN_Z2M_VERSION} or newer.`);
}
}, 120000);
});
this.api.on('didFinishLaunching', () => {
var _a, _b;
if (this.config !== undefined) {
// Setup MQTT callbacks and subscription
(_a = this.mqttClient) === null || _a === void 0 ? void 0 : _a.on('message', this.onMessage);
(_b = this.mqttClient) === null || _b === void 0 ? void 0 : _b.subscribe(this.config.mqtt.base_topic + '/#');
}
});
return mqttClient;
}
isExperimentalFeatureEnabled(feature) {
var _a;
if (((_a = this.config) === null || _a === void 0 ? void 0 : _a.experimental) === undefined) {
return false;
}
return this.config.experimental.includes(feature.trim().toLocaleUpperCase());
}
static createMqttOptions(log, config) {
const options = {};
if (config.mqtt.version) {
options.protocolVersion = config.mqtt.version;
}
if (config.mqtt.keepalive) {
log.debug(`Using MQTT keepalive: ${config.mqtt.keepalive}`);
options.keepalive = config.mqtt.keepalive;
}
if (config.mqtt.ca) {
log.debug(`MQTT SSL/TLS: Path to CA certificate = ${config.mqtt.ca}`);
options.ca = fs.readFileSync(config.mqtt.ca);
}
if (config.mqtt.key && config.mqtt.cert) {
log.debug(`MQTT SSL/TLS: Path to client key = ${config.mqtt.key}`);
log.debug(`MQTT SSL/TLS: Path to client certificate = ${config.mqtt.cert}`);
options.key = fs.readFileSync(config.mqtt.key);
options.cert = fs.readFileSync(config.mqtt.cert);
}
if (config.mqtt.user && config.mqtt.password) {
options.username = config.mqtt.user;
options.password = config.mqtt.password;
}
if (config.mqtt.client_id) {
log.debug(`Using MQTT client ID: '${config.mqtt.client_id}'`);
options.clientId = config.mqtt.client_id;
}
if (config.mqtt.reject_unauthorized !== undefined && !config.mqtt.reject_unauthorized) {
log.debug('MQTT reject_unauthorized set false, ignoring certificate warnings.');
options.rejectUnauthorized = false;
}
return options;
}
isHomebridgeServerVersionGreaterOrEqualTo(version) {
if (this.api.versionGreaterOrEqual !== undefined) {
return this.api.versionGreaterOrEqual(version);
}
return semver.gte(this.api.serverVersion, version);
}
checkZigbee2MqttVersion(version, topic) {
if (version !== this.lastReceivedZigbee2MqttVersion) {
// Only log the version if it is different from what we have previously received.
this.lastReceivedZigbee2MqttVersion = version;
this.log.info(`Using Zigbee2MQTT v${version} (identified via ${topic})`);
}
// Ignore -dev suffix if present, because Zigbee2MQTT appends this to the latest released version
// for the future development build (instead of applying semantic versioning).
const strippedVersion = version.replace(/-dev$/, '');
if (semver.lt(strippedVersion, Zigbee2mqttPlatform.MIN_Z2M_VERSION)) {
this.log.error('!!! UPDATE OF ZIGBEE2MQTT REQUIRED !!! \n' +
`Zigbee2MQTT v${version} is TOO OLD. The minimum required version is v${Zigbee2mqttPlatform.MIN_Z2M_VERSION}. \n` +
`This means that ${settings_1.PLUGIN_NAME} MIGHT NOT WORK AS EXPECTED!`);
}
}
onMessage(topic, payload) {
var _a, _b;
const fullTopic = topic;
try {
const baseTopic = `${(_a = this.config) === null || _a === void 0 ? void 0 : _a.mqtt.base_topic}/`;
if (!topic.startsWith(baseTopic)) {
this.log.debug('Ignore message, because topic is unexpected.', topic);
return;
}
topic = topic.substring(baseTopic.length);
let updateGroups = false;
let updateDevices = false;
if (topic.startsWith(Zigbee2mqttPlatform.TOPIC_BRIDGE)) {
topic = topic.substring(Zigbee2mqttPlatform.TOPIC_BRIDGE.length);
if (topic === 'devices') {
// Update accessories
this.lastReceivedDevices = JSON.parse(payload.toString());
if (((_b = this.config) === null || _b === void 0 ? void 0 : _b.exclude_grouped_devices) === true) {
if (this.lastReceivedGroups.length === 0) {
this.deviceUpdatePending = true;
}
else {
updateDevices = true;
}
}
else {
updateDevices = true;
}
if (this.groupUpdatePending) {
updateGroups = true;
this.groupUpdatePending = false;
}
}
else if (topic === 'groups') {
this.lastReceivedGroups = JSON.parse(payload.toString());
if (this.lastReceivedDevices.length === 0) {
this.groupUpdatePending = true;
}
else {
updateGroups = true;
}
if (this.deviceUpdatePending) {
updateDevices = true;
this.deviceUpdatePending = false;
}
}
else if (topic === 'state') {
const state = payload.toString();
if (state === 'offline') {
this.log.error('Zigbee2MQTT is OFFLINE!');
// TODO Mark accessories as offline somehow.
}
}
else if (topic === 'info' || topic === 'config') {
// New topic (bridge/info) and legacy topic (bridge/config) should both contain the version number.
this.checkZigbee2MqttVersionAndConfig(payload.toString(), fullTopic);
}
}
else if (!topic.endsWith('/get') && !topic.endsWith('/set')) {
// Probably a status update from a device
this.handleDeviceUpdate(topic, payload.toString());
}
if (updateDevices) {
this.handleReceivedDevices(this.lastReceivedDevices);
}
if (updateGroups) {
this.createGroupAccessories(this.lastReceivedGroups);
}
if (updateDevices || updateGroups) {
this.removeStaleDevices();
}
}
catch (Error) {
this.log.error(`Failed to process MQTT message on '${fullTopic}'. (Maybe check the MQTT version?)`);
this.log.error((0, helpers_1.errorToString)(Error));
}
}
checkZigbee2MqttVersionAndConfig(payload, fullTopic) {
var _a;
const info = JSON.parse(payload);
if ('version' in info) {
if (info.version !== this.lastReceivedZigbee2MqttVersion) {
// Only log the version if it is different from what we have previously received.
this.lastReceivedZigbee2MqttVersion = info.version;
this.log.info(`Using Zigbee2MQTT v${info.version} (identified via ${fullTopic})`);
}
// Ignore -dev suffix if present, because Zigbee2MQTT appends this to the latest released version
// for the future development build (instead of applying semantic versioning).
const strippedVersion = info.version.replace(/-dev$/, '');
if (semver.lt(strippedVersion, Zigbee2mqttPlatform.MIN_Z2M_VERSION)) {
this.log.error('!!!UPDATE OF ZIGBEE2MQTT REQUIRED!!! \n' +
`Zigbee2MQTT v${info.version} is TOO OLD. The minimum required version is v${Zigbee2mqttPlatform.MIN_Z2M_VERSION}. \n` +
`This means that ${settings_1.PLUGIN_NAME} MIGHT NOT WORK AS EXPECTED!`);
}
}
else {
this.log.error(`No version found in message on '${fullTopic}'.`);
}
// Also check for potentially incorrect configurations:
if ('config' in info) {
const outputFormat = (_a = info.config.experimental) === null || _a === void 0 ? void 0 : _a.output;
if (outputFormat !== undefined) {
if (!outputFormat.includes('json')) {
this.log.error('Zigbee2MQTT MUST output JSON in order for this plugin to work correctly. ' +
`Currently 'experimental.output' is set to '${outputFormat}'. Please adjust your configuration.`);
}
else {
this.log.debug(`Zigbee2MQTT 'experimental.output' is set to '${outputFormat}'`);
}
}
}
}
async handleDeviceUpdate(topic, statePayload) {
if (statePayload === '') {
this.log.debug('Ignore update, because payload is empty.', topic);
return;
}
const accessory = this.accessories.find((acc) => acc.matchesIdentifier(topic));
if (accessory) {
try {
const state = JSON.parse(statePayload);
accessory.updateStates(state);
this.log.debug(`Handled device update for ${topic}: ${statePayload}`);
}
catch (Error) {
this.log.error(`Failed to process status update with payload: ${statePayload}`);
this.log.error((0, helpers_1.errorToString)(Error));
}
}
else {
this.log.debug(`Unhandled message on topic: ${topic}`);
}
}
removeStaleDevices() {
// Remove devices that are no longer present
const staleAccessories = [];
for (let i = this.accessories.length - 1; i >= 0; --i) {
const foundIndex = this.lastReceivedDevices.findIndex((d) => d.ieee_address === this.accessories[i].ieeeAddress);
const foundGroupIndex = this.lastReceivedGroups.findIndex((g) => g.id === this.accessories[i].groupId);
if ((foundIndex < 0 && foundGroupIndex < 0) || this.isDeviceExcluded(this.accessories[i].accessory.context.device)) {
// Not found or excluded; remove it.
this.log.debug(`Removing accessory ${this.accessories[i].displayName} (${this.accessories[i].ieeeAddress})`);
staleAccessories.push(this.accessories[i].accessory);
this.accessories.splice(i, 1);
}
}
if (staleAccessories.length > 0) {
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, staleAccessories);
}
}
handleReceivedDevices(devices) {
this.log.debug('Received devices...');
this.didReceiveDevices = true;
devices.forEach((d) => this.createOrUpdateAccessory(d));
}
configureAccessory(accessory) {
this.addAccessory(accessory);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static getIdentifiersFromDevice(device) {
const identifiers = [];
if (typeof device === 'string') {
identifiers.push(device.toLocaleLowerCase());
}
else {
if ('ieee_address' in device) {
identifiers.push(device.ieee_address.toLocaleLowerCase());
}
if ('friendly_name' in device) {
identifiers.push(device.friendly_name.toLocaleLowerCase());
}
if ('id' in device) {
identifiers.push(device.id.toString().toLocaleLowerCase());
}
}
return identifiers;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getAdditionalConfigForDevice(device) {
var _a;
if (((_a = this.config) === null || _a === void 0 ? void 0 : _a.devices) !== undefined) {
const identifiers = Zigbee2mqttPlatform.getIdentifiersFromDevice(device);
for (const devConfig of this.config.devices) {
if (identifiers.includes(devConfig.id.toLocaleLowerCase())) {
return this.mergeDeviceConfig(devConfig);
}
}
}
return this.baseDeviceConfig;
}
mergeDeviceConfig(devConfig) {
const result = { ...this.baseDeviceConfig, ...devConfig };
// Merge converter configs correctly
if (this.baseDeviceConfig.converters !== undefined && devConfig.converters !== undefined) {
result.converters = { ...this.baseDeviceConfig.converters, ...devConfig.converters };
}
if (result.experimental !== undefined) {
// Normalize experimental feature flags
result.experimental = result.experimental.map((feature) => feature.trim().toLocaleUpperCase());
}
return result;
}
isDeviceExcluded(device) {
var _a;
const additionalConfig = this.getAdditionalConfigForDevice(device);
if ((additionalConfig === null || additionalConfig === void 0 ? void 0 : additionalConfig.exclude) === true) {
this.log.debug(`Device is excluded: ${additionalConfig.id}`);
return true;
}
if ((additionalConfig === null || additionalConfig === void 0 ? void 0 : additionalConfig.exclude) === false) {
// Device is explicitly NOT excluded (via device config or default device config)
return false;
}
if (((_a = this.config) === null || _a === void 0 ? void 0 : _a.exclude_grouped_devices) === true && this.lastReceivedGroups !== undefined) {
const id = typeof device === 'string' ? device : device.ieee_address;
for (const group of this.lastReceivedGroups) {
if (group.members.findIndex((m) => m.ieee_address === id) >= 0) {
this.log.debug(`Device (${id}) is excluded because it is in a group: ${group.friendly_name} (${group.id})`);
return true;
}
}
}
return false;
}
addAccessory(accessory) {
var _a;
const ieee_address = (_a = accessory.context.device.ieee_address) !== null && _a !== void 0 ? _a : accessory.context.device.ieeeAddr;
if (this.isDeviceExcluded(accessory.context.device)) {
this.log.warn(`Excluded device found on startup: ${accessory.context.device.friendly_name} (${ieee_address}).`);
process.nextTick(() => {
try {
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
}
catch (error) {
this.log.error('Failed to delete accessory.');
this.log.error((0, helpers_1.errorToString)(error));
}
});
return;
}
if (!(0, z2mModels_1.isDeviceListEntry)(accessory.context.device)) {
this.log.warn(`Restoring old (pre v1.0.0) accessory ${accessory.context.device.friendly_name} (${ieee_address}). This accessory ` +
`will not work until updated device information is received from Zigbee2MQTT v${Zigbee2mqttPlatform.MIN_Z2M_VERSION} or newer.`);
}
if (this.accessories.findIndex((acc) => acc.UUID === accessory.UUID) < 0) {
// New entry
this.log.info(`Restoring accessory: ${accessory.displayName} (${ieee_address})`);
const acc = new platformAccessory_1.Zigbee2mqttAccessory(this, accessory, this.getAdditionalConfigForDevice(accessory.context.device));
this.accessories.push(acc);
}
}
createOrUpdateAccessory(device) {
if (!device.supported || device.definition === undefined || this.isDeviceExcluded(device)) {
return;
}
const uuid_input = (0, z2mModels_1.isDeviceListEntryForGroup)(device) ? `group-${device.group_id}` : device.ieee_address;
const uuid = this.api.hap.uuid.generate(uuid_input);
const existingAcc = this.accessories.find((acc) => acc.UUID === uuid);
if (existingAcc) {
existingAcc.updateDeviceInformation(device);
}
else {
// New entry
this.log.info('New accessory:', device.friendly_name);
const accessory = new this.api.platformAccessory(device.friendly_name, uuid);
accessory.context.device = device;
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
const acc = new platformAccessory_1.Zigbee2mqttAccessory(this, accessory, this.getAdditionalConfigForDevice(device));
this.accessories.push(acc);
}
}
isConnected() {
return this.mqttClient !== undefined && !this.mqttClient.reconnecting;
}
async publishMessage(topic, payload, options) {
var _a, _b, _c;
if (this.config !== undefined) {
topic = `${this.config.mqtt.base_topic}/${topic}`;
options = { qos: 0, retain: false, ...options };
if (!this.isConnected) {
this.log.error('Not connected to MQTT server!');
this.log.error(`Cannot send message to '${topic}': '${payload}`);
return;
}
this.log.log(((_c = (_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.log) === null || _b === void 0 ? void 0 : _b.mqtt_publish) !== null && _c !== void 0 ? _c : "debug" /* LogLevel.DEBUG */), `Publish to '${topic}': '${payload}'`);
return new Promise((resolve) => {
var _a;
(_a = this.mqttClient) === null || _a === void 0 ? void 0 : _a.publish(topic, payload, options, () => resolve());
});
}
}
createGroupAccessories(groups) {
this.log.debug('Received groups...');
for (const group of groups) {
const device = this.createDeviceListEntryFromGroup(group);
if (device !== undefined) {
this.createOrUpdateAccessory(device);
}
}
}
createDeviceListEntryFromGroup(group) {
let exposes = this.determineExposesForGroup(group);
if (exposes.length === 0) {
// No exposes found. Check if additional config is given.
const config = this.getAdditionalConfigForDevice(group);
if (config !== undefined &&
(config.exclude === undefined || config.exclude === false) &&
(0, configModels_1.isDeviceConfiguration)(config) &&
config.exposes !== undefined &&
config.exposes.length > 0) {
// Additional config is given and it is not excluded.
exposes = config.exposes;
}
else {
// No exposes info found, so can't expose the group
this.log.debug(`Group ${group.friendly_name} (${group.id}) has no usable exposes information.`);
return undefined;
}
}
else {
// Exposes found.
this.log.debug(`Group ${group.friendly_name} (${group.id}) exposes (auto-determined):\n${JSON.stringify(exposes, null, 2)}`);
}
const device = {
friendly_name: group.friendly_name,
ieee_address: group.id.toString(),
group_id: group.id,
supported: true,
definition: {
vendor: 'Zigbee2MQTT',
model: `GROUP-${group.id}`,
exposes: exposes,
},
};
return device;
}
determineExposesForGroup(group) {
var _a;
let exposes = [];
let firstEntry = true;
for (const member of group.members) {
const device = this.lastReceivedDevices.find((dev) => dev.ieee_address === member.ieee_address);
if (device === undefined) {
this.log.warn(`Cannot find group member in devices: ${member.ieee_address}`);
continue;
}
if (((_a = device.definition) === null || _a === void 0 ? void 0 : _a.exposes) === undefined) {
this.log.warn(`No exposes info for group member: ${member.ieee_address}`);
continue;
}
if (firstEntry) {
// Exclude link quality information
exposes = device.definition.exposes.filter((e) => e.name !== 'linkquality');
firstEntry = false;
}
else {
// Try to merge exposes
exposes = (0, z2mModels_1.exposesGetOverlap)(exposes, device.definition.exposes);
}
}
return exposes;
}
}
exports.Zigbee2mqttPlatform = Zigbee2mqttPlatform;
Zigbee2mqttPlatform.MIN_Z2M_VERSION = '1.17.0';
Zigbee2mqttPlatform.TOPIC_BRIDGE = 'bridge/';
//# sourceMappingURL=platform.js.map