UNPKG

@dotwee/homebridge-z2m

Version:

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

349 lines 14.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SwitchActionHelper = exports.SwitchActionMapping = void 0; class SwitchActionMapping { get subType() { var _a; if (this.extension === undefined || this.extension === 0) { return this.identifier; } return `${(_a = this._id) !== null && _a !== void 0 ? _a : ''}#ext${this.extension}`; } get identifier() { return this._id; } set identifier(value) { if (this._id !== undefined || this.serviceLabelIndex !== undefined || this.extension !== undefined) { throw new Error('Can only set identifier once and before serviceLabelIndex has been set.'); } this._id = value; } merge(other) { if (this.subType !== other.subType) { throw new Error('Can NOT merge SwitchActionMapping instances with different identifiers and/or extensions. ' + `(got subtype ${this.subType} and ${other.subType})`); } if (this.serviceLabelIndex === undefined) { this.serviceLabelIndex = other.serviceLabelIndex; } else { this.compareValuesForMerge('service label index', this.serviceLabelIndex, other.serviceLabelIndex); } if (this.valueSinglePress === undefined) { this.valueSinglePress = other.valueSinglePress; } else { this.compareValuesForMerge('single press', this.valueSinglePress, other.valueSinglePress); } if (this.valueDoublePress === undefined) { this.valueDoublePress = other.valueDoublePress; } else { this.compareValuesForMerge('double press', this.valueDoublePress, other.valueDoublePress); } if (this.valueLongPress === undefined) { this.valueLongPress = other.valueLongPress; } else { this.compareValuesForMerge('long press', this.valueLongPress, other.valueLongPress); } return this; } compareValuesForMerge(description, local, other) { if (other === undefined) { return; } if (other !== local) { throw new Error(`Can NOT merge SwitchActionMapping instances that both have a different value for ${description}. ` + `(got ${local} and ${other})`); } } hasValidValues() { return this.valueSinglePress !== undefined || this.valueDoublePress !== undefined || this.valueLongPress !== undefined; } isValidMapping() { return this.serviceLabelIndex !== undefined && this.hasValidValues(); } toString() { if (!this.isValidMapping()) { return undefined; } let str = `Button ${this.serviceLabelIndex}`; if (this.identifier !== undefined) { str += ` (${this.identifier})`; } if (this.extension !== undefined && this.extension > 0) { str += ` #ext${this.extension}`; } str += ':'; if (this.valueSinglePress !== undefined) { str += `\n\t- SINGLE: ${this.valueSinglePress}`; } if (this.valueDoublePress !== undefined) { str += `\n\t- DOUBLE: ${this.valueDoublePress}`; } if (this.valueLongPress !== undefined) { str += `\n\t- LONG : ${this.valueLongPress}`; } return str; } } exports.SwitchActionMapping = SwitchActionMapping; class SwitchActionHelper { constructor() { const additions = [ ...SwitchActionHelper.ignoredAdditions, ...SwitchActionHelper.singleAction, ...SwitchActionHelper.doubleAction, ...SwitchActionHelper.longAction, ...SwitchActionHelper.tripleAction, ...SwitchActionHelper.quadrupleAction, ].join('|'); const separatorsString = SwitchActionHelper.separators.join(''); this.regex_id_start = new RegExp(`^(${additions})[${separatorsString}]`, 'i'); this.regex_id_end = new RegExp(`[${separatorsString}](${additions})$`, 'i'); this.regex_separator_count = new RegExp(`[${separatorsString}]+`, 'ig'); } static getInstance() { if (SwitchActionHelper.instance === undefined) { SwitchActionHelper.instance = new SwitchActionHelper(); } return SwitchActionHelper.instance; } firstNumberInString(input) { var _a; const numbers = (_a = input.match(SwitchActionHelper.regex_number)) === null || _a === void 0 ? void 0 : _a.map((x) => parseInt(x)); if (numbers !== undefined && numbers.length > 0 && !isNaN(numbers[0])) { return numbers[0]; } return undefined; } numberOfSeparators(input) { return (input.match(this.regex_separator_count) || []).length; } matchActionValues(mapping, value, type = undefined) { if (type === undefined) { type = value; } type = type.toLowerCase(); if (SwitchActionHelper.singleAction.has(type)) { mapping.valueSinglePress = value; return true; } if (SwitchActionHelper.doubleAction.has(type)) { mapping.valueDoublePress = value; return true; } if (SwitchActionHelper.longAction.has(type)) { mapping.valueLongPress = value; return true; } // Extended actions if (SwitchActionHelper.tripleAction.has(type)) { mapping.extension = 1; mapping.valueSinglePress = value; return true; } if (SwitchActionHelper.quadrupleAction.has(type)) { mapping.extension = 1; mapping.valueDoublePress = value; return true; } return false; } valueToMapping(input) { // Devices that have a wildcard in the reported values can not be supported. if (input.indexOf('*') >= 0) { throw new Error('Device found with a wildcard in the exposed possible values for the action, which cannot be mapped: ' + input); } // Check exact matches first const mapping = new SwitchActionMapping(); if (this.matchActionValues(mapping, input)) { return mapping; } if (SwitchActionHelper.ignoredAdditions.has(input)) { return mapping; } mapping.identifier = input.replace(this.regex_id_start, '').replace(this.regex_id_end, ''); // Check if identifier is equal to the input // If so, consider it a single press action if (input === mapping.identifier) { mapping.valueSinglePress = input; return mapping; } // Determine action for value const startMatch = input.match(this.regex_id_start); if (startMatch && startMatch.length >= 2 && this.matchActionValues(mapping, input, startMatch[1])) { return mapping; } const endMatch = input.match(this.regex_id_end); if (endMatch && endMatch.length >= 2 && this.matchActionValues(mapping, input, endMatch[1])) { return mapping; } return mapping; } valuesToNumberedMappings(values) { // Convert and combine const groupedMappings = new Map(); for (const value of values) { const mapping = this.valueToMapping(value); const existingMapping = groupedMappings.get(mapping.subType); if (existingMapping !== undefined) { existingMapping.merge(mapping); } else { groupedMappings.set(mapping.subType, mapping); } } // Filter out invalid mappings and sort them const sortedMappings = this.sortMappingsByIdentifier([...groupedMappings.values()].filter((m) => m.hasValidValues())); // Determine labels this.labelSortedMappings(sortedMappings); return sortedMappings; } determineLabelStrategy(mappings) { var _a, _b, _c, _d; // Check which single digit numbers are present in the identifiers and how often the same number is used let maximumExtension = 0; let maximumTimesNumberIsUsed = 0; let highestNumber = 0; const foundNumbers = new Map(); for (const mapping of mappings) { const numericId = this.firstNumberInString((_a = mapping.identifier) !== null && _a !== void 0 ? _a : ''); if (((_b = mapping.extension) !== null && _b !== void 0 ? _b : 0) > maximumExtension) { maximumExtension = (_c = mapping.extension) !== null && _c !== void 0 ? _c : 0; } if (numericId !== undefined && numericId <= 9) { const newCount = ((_d = foundNumbers.get(numericId)) !== null && _d !== void 0 ? _d : 0) + 1; foundNumbers.set(numericId, newCount); if (newCount > maximumTimesNumberIsUsed) { maximumTimesNumberIsUsed = newCount; } if (numericId > highestNumber) { highestNumber = numericId; } } } // Determine numbering strategy const numericMultiplier = maximumTimesNumberIsUsed <= 1 ? 1 : 10; const useIncrementalNumbers = maximumTimesNumberIsUsed > 10; const startLabelForNonNumericIds = useIncrementalNumbers ? 1 : (highestNumber + 1) * numericMultiplier; return [numericMultiplier, useIncrementalNumbers, startLabelForNonNumericIds]; } labelSortedMappings(mappings) { var _a; // Determine strategy const [numericMultiplier, useIncrementalNumbers, startLabelForNonNumericIds] = this.determineLabelStrategy(mappings); // Apply service labels const usedLabels = new Set(); for (const mapping of mappings) { // Use first numeric value in identifier (if present) const foundNumber = this.firstNumberInString((_a = mapping.identifier) !== null && _a !== void 0 ? _a : ''); let maximumNumber = 255; let numericId = startLabelForNonNumericIds; if (!useIncrementalNumbers && foundNumber !== undefined && foundNumber > 0 && foundNumber <= 9) { numericId = foundNumber * numericMultiplier; maximumNumber = numericId + (numericMultiplier - 1); } // Find first available index while (usedLabels.has(numericId)) { ++numericId; if (numericId >= maximumNumber) { throw new Error('Service Label Index going out of range!'); } } mapping.serviceLabelIndex = numericId; // Store used label to prevent duplicates usedLabels.add(numericId); } } sortByFirstNumber(x, y) { const X_GOES_FIRST = -1; const Y_GOES_FIRST = 1; const firstNumberInX = this.firstNumberInString(x); const firstNumberInY = this.firstNumberInString(y); if (firstNumberInX !== undefined) { if (firstNumberInY === undefined) { return X_GOES_FIRST; } // Both have numbers, compare first number in string if (firstNumberInX === firstNumberInY) { // No preference return 0; } // Prefer any number over 0, as the service label in HomeKit can not be 0. if (firstNumberInX === 0) { return Y_GOES_FIRST; } if (firstNumberInY === 0) { return X_GOES_FIRST; } // No zero values, just compare them then. if (firstNumberInX < firstNumberInY) { return X_GOES_FIRST; } if (firstNumberInX > firstNumberInY) { return Y_GOES_FIRST; } } else { if (firstNumberInY !== undefined) { return Y_GOES_FIRST; } } return 0; } sortMappingsByIdentifier(ids) { return ids.sort((xMapping, yMapping) => { var _a, _b, _c, _d, _e, _f; const X_GOES_FIRST = -1; const Y_GOES_FIRST = 1; const x = (_b = (_a = xMapping.identifier) === null || _a === void 0 ? void 0 : _a.toLowerCase().trim()) !== null && _b !== void 0 ? _b : ''; const y = (_d = (_c = yMapping.identifier) === null || _c === void 0 ? void 0 : _c.toLowerCase().trim()) !== null && _d !== void 0 ? _d : ''; if (x === y) { const extensionX = (_e = xMapping.extension) !== null && _e !== void 0 ? _e : 0; const extensionY = (_f = yMapping.extension) !== null && _f !== void 0 ? _f : 0; return extensionX - extensionY; } // Prefer strings with a numeric sequence. // If both have one, the lowest number is preferred. const numberResult = this.sortByFirstNumber(x, y); if (numberResult !== 0) { return numberResult; } // Prefer empty string over other values if (x === '') { return X_GOES_FIRST; } if (y === '') { return Y_GOES_FIRST; } // Prefer strings with fewer separators const separatorCountX = this.numberOfSeparators(x); const separatorCountY = this.numberOfSeparators(y); if (separatorCountX < separatorCountY) { return X_GOES_FIRST; } if (separatorCountX > separatorCountY) { return Y_GOES_FIRST; } // Use normal alphabetic order if (x < y) { return X_GOES_FIRST; } if (x > y) { return Y_GOES_FIRST; } return 0; }); } } exports.SwitchActionHelper = SwitchActionHelper; SwitchActionHelper.singleAction = new Set(['single', 'click', 'press']); SwitchActionHelper.doubleAction = new Set(['double']); SwitchActionHelper.longAction = new Set(['hold', 'long']); SwitchActionHelper.tripleAction = new Set(['triple', 'tripple']); SwitchActionHelper.quadrupleAction = new Set(['quadruple']); SwitchActionHelper.ignoredAdditions = new Set(['release', 'hold-release']); SwitchActionHelper.separators = ['_', '-']; SwitchActionHelper.regex_number = new RegExp('(\\d{1,3})'); //# sourceMappingURL=action_helper.js.map