UNPKG

homebridge-z2m

Version:

Expose your Zigbee devices to HomeKit with ease, by integrating Zigbee2MQTT with Homebridge.

310 lines 13.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Zigbee2mqttAccessory = void 0; const timer_1 = require("./timer"); const hap_1 = require("./hap"); const creators_1 = require("./converters/creators"); const z2mModels_1 = require("./z2mModels"); const configModels_1 = require("./configModels"); class Zigbee2mqttAccessory { constructor(platform, accessory, additionalConfig, serviceCreatorManager) { this.platform = platform; this.accessory = accessory; this.additionalConfig = additionalConfig; this.serviceHandlers = new Map(); this.serviceIds = new Set(); // Store ServiceCreatorManager if (serviceCreatorManager === undefined) { this.serviceCreatorManager = creators_1.BasicServiceCreatorManager.getInstance(); } else { this.serviceCreatorManager = serviceCreatorManager; } // Log experimental features if (this.additionalConfig.experimental !== undefined && this.additionalConfig.experimental.length > 0) { this.log.warn(`Experimental features enabled for ${this.displayName}: ${this.additionalConfig.experimental.join(', ')}`); } // Setup delayed publishing this.pendingPublishData = {}; this.publishIsScheduled = false; // Setup delayed get this.pendingGetKeys = new Set(); this.getIsScheduled = false; // Log additional config this.platform.log.debug(`Config for accessory ${this.displayName} : ${JSON.stringify(this.additionalConfig)}`); this.updateDeviceInformation(accessory.context.device, true); // Ask Zigbee2MQTT for a status update at least once every 4 hours. this.updateTimer = new timer_1.ExtendedTimer(() => { this.queueAllKeysForGet(); }, (4 * 60 * 60 * 1000)); // Immediately request an update to start off. this.queueAllKeysForGet(); } get log() { return this.platform.log; } get displayName() { return this.accessory.context.device.friendly_name; } get deviceTopic() { if ((0, z2mModels_1.isDeviceListEntryForGroup)(this.accessory.context.device) || 'group_id' in this.accessory.context.device) { return this.accessory.context.device.friendly_name; } return this.accessory.context.device.ieee_address; } get groupId() { if ((0, z2mModels_1.isDeviceListEntryForGroup)(this.accessory.context.device) || 'group_id' in this.accessory.context.device) { return this.accessory.context.device.group_id; } return undefined; } get serialNumber() { if ((0, z2mModels_1.isDeviceListEntryForGroup)(this.accessory.context.device) || 'group_id' in this.accessory.context.device) { return `GROUP:${this.accessory.context.device.group_id}`; } return this.accessory.context.device.ieee_address; } getConverterConfiguration(tag) { return this.additionalConfig.converters !== undefined ? this.additionalConfig.converters[tag] : undefined; } isExperimentalFeatureEnabled(feature) { if (this.platform.isExperimentalFeatureEnabled(feature)) { // Enabled globally return true; } if (this.additionalConfig.experimental !== undefined) { // Enabled for this accessory return this.additionalConfig.experimental.includes(feature.trim().toLocaleUpperCase()); } return false; } registerServiceHandler(handler) { const key = handler.identifier; if (this.serviceHandlers.has(key)) { this.log.error(`DUPLICATE SERVICE HANDLER with identifier ${key} for accessory ${this.displayName}. New one will not stored.`); } else { this.serviceHandlers.set(key, handler); } } isServiceHandlerIdKnown(identifier) { return this.serviceHandlers.has(identifier); } isPropertyExcluded(property) { var _a, _b; if (property === undefined) { // Property is undefined, so it can't be excluded. // This is accepted so all exposes models can easily be checked. return false; } if (Array.isArray(this.additionalConfig.included_keys) && this.additionalConfig.included_keys.includes(property)) { // Property is explicitly included return false; } return (_b = (_a = this.additionalConfig.excluded_keys) === null || _a === void 0 ? void 0 : _a.includes(property)) !== null && _b !== void 0 ? _b : false; } isValueAllowedForProperty(property, value) { var _a; const config = (_a = this.additionalConfig.values) === null || _a === void 0 ? void 0 : _a.find(c => c.property === property); if (config) { if (config.include && config.include.length > 0) { if (config.include.findIndex(p => this.doesValueMatchPattern(value, p)) < 0) { // Value doesn't match any of the include patterns return false; } } if (config.exclude && config.exclude.length > 0) { if (config.exclude.findIndex(p => this.doesValueMatchPattern(value, p)) >= 0) { // Value matches one of the exclude patterns return false; } } } return true; } doesValueMatchPattern(value, pattern) { if (pattern.length === 0) { return false; } if (pattern.length >= 2) { // Need at least 2 characters for the wildcard to work if (pattern.startsWith('*')) { return value.endsWith(pattern.substr(1)); } if (pattern.endsWith('*')) { return value.startsWith(pattern.substr(0, pattern.length - 1)); } } return value === pattern; } queueAllKeysForGet() { const keys = [...this.serviceHandlers.values()].map(h => h.getableKeys).reduce((a, b) => { return a.concat(b); }, []); if (keys.length > 0) { this.queueKeyForGetAction(keys); } } publishPendingGetKeys() { const keys = [...this.pendingGetKeys]; this.pendingGetKeys.clear(); this.getIsScheduled = false; if (keys.length > 0) { const data = {}; for (const k of keys) { data[k] = 0; } // Publish using ieeeAddr, as that will never change and the friendly_name might. this.platform.publishMessage(`${this.deviceTopic}/get`, JSON.stringify(data), { qos: this.getMqttQosLevel(1) }); } } queueKeyForGetAction(key) { if (Array.isArray(key)) { for (const k of key) { this.pendingGetKeys.add(k); } } else { this.pendingGetKeys.add(key); } this.log.debug(`Pending get: ${[...this.pendingGetKeys].join(', ')}`); if (!this.getIsScheduled) { this.getIsScheduled = true; process.nextTick(() => { this.publishPendingGetKeys(); }); } } static getUniqueIdForService(service) { if (service.subtype === undefined) { return service.UUID; } return `${service.UUID}_${service.subtype}`; } getOrAddService(service) { this.serviceIds.add(Zigbee2mqttAccessory.getUniqueIdForService(service)); const existingService = this.accessory.services.find(e => e.UUID === service.UUID && e.subtype === service.subtype); if (existingService !== undefined) { return existingService; } return this.accessory.addService(service); } queueDataForSetAction(data) { this.pendingPublishData = { ...this.pendingPublishData, ...data }; this.log.debug(`Pending data: ${JSON.stringify(this.pendingPublishData)}`); if (!this.publishIsScheduled) { this.publishIsScheduled = true; process.nextTick(() => { this.publishPendingSetData(); }); } } publishPendingSetData() { this.platform.publishMessage(`${this.deviceTopic}/set`, JSON.stringify(this.pendingPublishData), { qos: this.getMqttQosLevel(2) }); this.publishIsScheduled = false; this.pendingPublishData = {}; } get UUID() { return this.accessory.UUID; } get ieeeAddress() { return this.accessory.context.device.ieee_address; } matchesIdentifier(id) { return (id === this.ieeeAddress || this.accessory.context.device.friendly_name === id); } updateDeviceInformation(info, force_update = false) { var _a, _b, _c, _d; // Overwrite exposes information if available in configuration if (info !== undefined && info.definition !== undefined && info.definition !== null) { if ((0, configModels_1.isDeviceConfiguration)(this.additionalConfig) && this.additionalConfig.exposes !== undefined && this.additionalConfig.exposes.length > 0) { info.definition.exposes = this.additionalConfig.exposes; } } // Only update the device if a valid device list entry is passed. // This is done so that old, pre-v1.0.0 accessories will only get updated when new device information is received. if ((0, z2mModels_1.isDeviceListEntry)(info) && (force_update || !(0, z2mModels_1.deviceListEntriesAreEqual)(this.accessory.context.device, info))) { const oldFriendlyName = this.accessory.context.device.friendly_name; const friendlyNameChanged = (force_update || info.friendly_name.localeCompare(this.accessory.context.device.friendly_name) !== 0); // Device info has changed this.accessory.context.device = info; if (!(0, z2mModels_1.isDeviceDefinition)(info.definition)) { this.log.error(`No device definition for device ${info.friendly_name} (${this.ieeeAddress}).`); } else { // Update accessory info // Note: getOrAddService is used so that the service is known in this.serviceIds and will not get filtered out. this.getOrAddService(new hap_1.hap.Service.AccessoryInformation()) .updateCharacteristic(hap_1.hap.Characteristic.Name, info.friendly_name) .updateCharacteristic(hap_1.hap.Characteristic.Manufacturer, (_a = info.definition.vendor) !== null && _a !== void 0 ? _a : 'Zigbee2MQTT') .updateCharacteristic(hap_1.hap.Characteristic.Model, (_b = info.definition.model) !== null && _b !== void 0 ? _b : 'unknown') .updateCharacteristic(hap_1.hap.Characteristic.SerialNumber, this.serialNumber) .updateCharacteristic(hap_1.hap.Characteristic.HardwareRevision, (_c = info.date_code) !== null && _c !== void 0 ? _c : '?') .updateCharacteristic(hap_1.hap.Characteristic.FirmwareRevision, (_d = info.software_build_id) !== null && _d !== void 0 ? _d : '?'); // Create (new) services this.serviceCreatorManager.createHomeKitEntitiesFromExposes(this, info.definition.exposes); } this.cleanStaleServices(); if (friendlyNameChanged) { this.platform.log.debug(`Updating service names for ${info.friendly_name} (from ${oldFriendlyName})`); this.updateServiceNames(); } } this.platform.api.updatePlatformAccessories([this.accessory]); } cleanStaleServices() { // Remove all services of which identifier is not known const staleServices = this.accessory.services.filter(s => !this.serviceIds.has(Zigbee2mqttAccessory.getUniqueIdForService(s))); staleServices.forEach((s) => { this.log.debug(`Clean up stale service ${s.displayName} (${s.UUID}) for accessory ${this.displayName} (${this.ieeeAddress}).`); this.accessory.removeService(s); }); } updateServiceNames() { // Update the name of all services for (const service of this.accessory.services) { if (service.UUID === hap_1.hap.Service.AccessoryInformation.UUID) { continue; } const nameCharacteristic = service.getCharacteristic(hap_1.hap.Characteristic.Name); if (nameCharacteristic !== undefined) { const displayName = this.getDefaultServiceDisplayName(service.subtype); nameCharacteristic.updateValue(displayName); } } } getMqttQosLevel(defaultQoS) { var _a; if ((_a = this.platform.config) === null || _a === void 0 ? void 0 : _a.mqtt.disable_qos) { return 0; } return defaultQoS; } updateStates(state) { // Restart timer this.updateTimer.restart(); // Filter out all properties that have a null/undefined value for (const key in state) { if (state[key] === null || state[key] === undefined) { delete state[key]; } } // Call updates for (const handler of this.serviceHandlers.values()) { handler.updateState(state); } } getDefaultServiceDisplayName(subType) { let name = this.displayName; if (subType !== undefined) { name += ` ${subType}`; } return name; } } exports.Zigbee2mqttAccessory = Zigbee2mqttAccessory; //# sourceMappingURL=platformAccessory.js.map