@dotwee/homebridge-z2m
Version:
Expose your Zigbee devices to HomeKit with ease, by integrating Zigbee2MQTT with Homebridge.
351 lines • 16.1 kB
JavaScript
"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