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