UNPKG

@dotwee/homebridge-z2m

Version:

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

351 lines 16.1 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"); const helpers_1 = require("./helpers"); 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); } } configureController(controller) { this.accessory.configureController(controller); } 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; } isEndpointExcluded(endpoint) { if (this.additionalConfig.excluded_endpoints === undefined || this.additionalConfig.excluded_endpoints.length === 0) { // No excluded endpoints defined return false; } return this.additionalConfig.excluded_endpoints.includes(endpoint !== null && endpoint !== void 0 ? endpoint : ''); } isExposesEntryExcluded(exposesEntry) { if (this.isPropertyExcluded(exposesEntry.property)) { return true; } return this.isEndpointExcluded(exposesEntry.endpoint); } filterValuesForExposesEntry(exposesEntry) { if (exposesEntry.values === undefined || exposesEntry.values.length === 0) { return []; } if (exposesEntry.property === undefined) { // Do not filter. return exposesEntry.values; } return exposesEntry.values.filter((v) => { var _a; return this.isValueAllowedForProperty((_a = exposesEntry.property) !== null && _a !== void 0 ? _a : '', v); }); } 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 && 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 && 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 for ${this.displayName}: ${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, _e; // Overwrite exposes information if available in configuration if ((info === null || info === void 0 ? void 0 : info.definition) !== undefined && info.definition !== null && (0, configModels_1.isDeviceConfiguration)(this.additionalConfig) && this.additionalConfig.exposes !== undefined && this.additionalConfig.exposes.length > 0) { info.definition.exposes = this.additionalConfig.exposes; } // Filter/sanitize exposes information if (((_a = info === null || info === void 0 ? void 0 : info.definition) === null || _a === void 0 ? void 0 : _a.exposes) !== undefined) { info.definition.exposes = (0, helpers_1.sanitizeAndFilterExposesEntries)(info.definition.exposes, (e) => { return !this.isExposesEntryExcluded(e); }, this.filterValuesForExposesEntry.bind(this)); } // 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, (_b = info.definition.vendor) !== null && _b !== void 0 ? _b : 'Zigbee2MQTT') .updateCharacteristic(hap_1.hap.Characteristic.Model, (_c = info.definition.model) !== null && _c !== void 0 ? _c : 'unknown') .updateCharacteristic(hap_1.hap.Characteristic.SerialNumber, this.serialNumber) .updateCharacteristic(hap_1.hap.Characteristic.HardwareRevision, (_d = info.date_code) !== null && _d !== void 0 ? _d : '?') .updateCharacteristic(hap_1.hap.Characteristic.FirmwareRevision, (_e = info.software_build_id) !== null && _e !== void 0 ? _e : '?'); // 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; } isAdaptiveLightingEnabled() { var _a, _b; return (_b = (_a = this.additionalConfig.adaptive_lighting) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : true; } getAdaptiveLightingMinimumColorTemperatureChange() { var _a, _b; return (_b = (_a = this.additionalConfig.adaptive_lighting) === null || _a === void 0 ? void 0 : _a.min_ct_change) !== null && _b !== void 0 ? _b : 0; } getAdaptiveLightingTransitionTime() { var _a, _b; return (_b = (_a = this.additionalConfig.adaptive_lighting) === null || _a === void 0 ? void 0 : _a.transition) !== null && _b !== void 0 ? _b : 0; } } exports.Zigbee2mqttAccessory = Zigbee2mqttAccessory; //# sourceMappingURL=platformAccessory.js.map