diff --git a/CHANGELOG.md b/CHANGELOG.md index fa43dfda..4ff73f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ Since version 1.0.0, we try to follow the [Semantic Versioning](https://semver.o ## [Unreleased] +### Added + +- Properties/exposes information can now be excluded based on the `endpoint`, using the `excluded_endpoints` configuration option. (relates to [#517](https://github.com/itavero/homebridge-z2m/issues/517)) + +### Changed + +- Exposes information is now filtered before passing it to the service handlers. This should make the behavior more consistent and reduce complexity of the service handlers for improved maintainability. + ## [1.9.2] - 2022-10-01 ### Fixed diff --git a/config.schema.json b/config.schema.json index 9d4c5c0c..e2ff9397 100644 --- a/config.schema.json +++ b/config.schema.json @@ -20,6 +20,15 @@ "minLength": 1 } }, + "excluded_endpoints": { + "title": "Excluded endpoints", + "type": "array", + "required": false, + "items": { + "type": "string", + "minLength": 0 + } + }, "values": { "title": "Include/exclude values", "type": "array", @@ -199,6 +208,9 @@ "excluded_keys": { "$ref": "#/definitions/excluded_keys" }, + "excluded_endpoints": { + "$ref": "#/definitions/excluded_endpoints" + }, "values": { "$ref": "#/definitions/values" }, @@ -237,6 +249,12 @@ "functionBody": "return !model.devices[arrayIndices].exclude;" } }, + "excluded_endpoints": { + "$ref": "#/definitions/excluded_endpoints", + "condition": { + "functionBody": "return !model.devices[arrayIndices].exclude;" + } + }, "included_keys": { "title": "Included properties (keys)", "type": "array", diff --git a/docs/config.md b/docs/config.md index d14bcf48..f6a9c7f3 100644 --- a/docs/config.md +++ b/docs/config.md @@ -34,6 +34,9 @@ A possible configuration looks like this: }, { "id": "0xabcd1234abcd1234", + "excluded_endpoints": [ + "l2" + ], "converters": { "switch": { "type": "outlet" @@ -82,7 +85,8 @@ Currently the following options are available: * `exclude`: if set to `true` this device will not be fully ignored. * `excluded_keys`: an array of properties/keys (known as the `property` in the exposes information) that should be ignored/excluded for this device. * `included_keys`: an array of properties/keys (known as the `property` in the exposes information) that should be included for this device, even if they are excluded in the global default device configuration (see below). -* `values`: Per property, you can specify an include and/or exclude list to ignore certain values. The values may start or end with an asterisk (`*`) as a wildcard. This is currently only applied in the [Stateless Programmable Switch](action.md). +* `excluded_endpoints`: an array of endpoints that should be ignored/excluded for this device. To ignore properties without an endpoint, add `''` (empty string) to the array. +* `values`: Per property, you can specify an include and/or exclude list to ignore certain values. The values may start or end with an asterisk (`*`) as a wildcard. * `exposes`: An array of exposes information, using the [structures defined by Zigbee2MQTT](https://www.zigbee2mqtt.io/guide/usage/exposes.html). * `converters`: An object to optionally provide additional configuration for specific converters. More information can be found in the documentation of the [converters](converters.md), if applicable. diff --git a/src/configModels.ts b/src/configModels.ts index 14e163c2..18ee8516 100644 --- a/src/configModels.ts +++ b/src/configModels.ts @@ -91,6 +91,7 @@ export const isMqttConfiguration = (x: any): x is MqttConfiguration => ( export interface BaseDeviceConfiguration extends Record { exclude?: boolean; excluded_keys?: string[]; + excluded_endpoints?: string[]; values?: PropertyValueConfiguration[]; converters?: object; experimental?: string[]; @@ -114,6 +115,11 @@ export const isBaseDeviceConfiguration = (x: any): x is BaseDeviceConfiguration return false; } + // Optional excluded_endpoints which must be an array of strings if present + if (x.excluded_endpoints !== undefined && !isStringArray(x.excluded_endpoints)) { + return false; + } + // Optional 'experimental' which must be an array of strings if present if (x.experimental !== undefined && !isStringArray(x.experimental)) { return false; diff --git a/src/converters/action.ts b/src/converters/action.ts index 0f3536fb..fa9b935b 100644 --- a/src/converters/action.ts +++ b/src/converters/action.ts @@ -9,16 +9,14 @@ import { SwitchActionHelper, SwitchActionMapping } from './action_helper'; export class StatelessProgrammableSwitchCreator implements ServiceCreator { createServicesFromExposes(accessory: BasicAccessory, exposes: ExposesEntry[]): void { - const actionExposes = exposes.filter(e => exposesIsPublished(e) && exposesHasEnumProperty(e) && e.name === 'action' - && !accessory.isPropertyExcluded(e.property)) + const actionExposes = exposes.filter(e => exposesIsPublished(e) && exposesHasEnumProperty(e) && e.name === 'action') .map(e => e as ExposesEntryWithEnumProperty); for (const expose of actionExposes) { // Each action expose can map to multiple instances of a Stateless Programmable Switch, // depending on the values provided. try { - const allowedValues = expose.values.filter(v => accessory.isValueAllowedForProperty(expose.property, v)); - const mappings = SwitchActionHelper.getInstance().valuesToNumberedMappings(allowedValues).filter(m => m.isValidMapping()); + const mappings = SwitchActionHelper.getInstance().valuesToNumberedMappings(expose.values).filter(m => m.isValidMapping()); const logEntries: string[] = [`Mapping of property '${expose.property}' of device '${accessory.displayName}':`]; for (const mapping of mappings) { try { diff --git a/src/converters/air_quality.ts b/src/converters/air_quality.ts index c36c09d3..59c04d05 100644 --- a/src/converters/air_quality.ts +++ b/src/converters/air_quality.ts @@ -8,9 +8,8 @@ import { Characteristic, CharacteristicValue, Service, WithUUID } from 'homebrid export class AirQualitySensorCreator implements ServiceCreator { createServicesFromExposes(accessory: BasicAccessory, exposes: ExposesEntry[]): void { - const endpointMap = groupByEndpoint(exposes.filter(e => - exposesHasProperty(e) && exposesIsPublished(e) && !accessory.isPropertyExcluded(e.property) && - AirQualitySensorHandler.propertyFactories.find((f) => f.canUseExposesEntry(e)) !== undefined, + const endpointMap = groupByEndpoint(exposes.filter(e => exposesHasProperty(e) && exposesIsPublished(e) + && AirQualitySensorHandler.propertyFactories.find((f) => f.canUseExposesEntry(e)) !== undefined, ).map(e => e as ExposesEntryWithProperty)); endpointMap.forEach((value, key) => { if (!accessory.isServiceHandlerIdKnown(AirQualitySensorHandler.generateIdentifier(key))) { diff --git a/src/converters/basic_sensors.ts b/src/converters/basic_sensors.ts index 959339ac..1904ff9b 100644 --- a/src/converters/basic_sensors.ts +++ b/src/converters/basic_sensors.ts @@ -66,7 +66,7 @@ export class BasicSensorCreator implements ServiceCreator { } createServicesFromExposes(accessory: BasicAccessory, exposes: ExposesEntry[]): void { - const endpointMap = groupByEndpoint(exposes.filter(e => exposesHasProperty(e) && !accessory.isPropertyExcluded(e.property) + const endpointMap = groupByEndpoint(exposes.filter(e => exposesHasProperty(e) && exposesIsPublished(e)).map(e => e as ExposesEntryWithProperty)); endpointMap.forEach((value, key) => { diff --git a/src/converters/battery.ts b/src/converters/battery.ts index c3b4bcea..f2791cc1 100644 --- a/src/converters/battery.ts +++ b/src/converters/battery.ts @@ -14,7 +14,7 @@ import { export class BatteryCreator implements ServiceCreator { createServicesFromExposes(accessory: BasicAccessory, exposes: ExposesEntry[]): void { const endpointMap = groupByEndpoint(exposes.filter(e => - exposesHasProperty(e) && exposesIsPublished(e) && !accessory.isPropertyExcluded(e.property) && ( + exposesHasProperty(e) && exposesIsPublished(e) && ( (e.name === 'battery' && exposesHasNumericRangeProperty(e)) || (e.name === 'battery_low' && exposesHasBinaryProperty(e)) )).map(e => e as ExposesEntryWithProperty)); diff --git a/src/converters/climate.ts b/src/converters/climate.ts index 4a62c098..6823dae5 100644 --- a/src/converters/climate.ts +++ b/src/converters/climate.ts @@ -87,14 +87,12 @@ class ThermostatHandler implements ServiceHandler { } public static hasRequiredFeatures(accessory: BasicAccessory, e: ExposesEntryWithFeatures): boolean { - if (e.features.findIndex(f => f.name === 'occupied_cooling_setpoint' && !accessory.isPropertyExcluded(f.property)) >= 0) { + if (e.features.findIndex(f => f.name === 'occupied_cooling_setpoint') >= 0) { // For now ignore devices that have a cooling setpoint as I haven't figured our how to handle this correctly in HomeKit. return false; } - return exposesHasAllRequiredFeatures(e, - [ThermostatHandler.PREDICATE_SETPOINT, ThermostatHandler.PREDICATE_LOCAL_TEMPERATURE], - accessory.isPropertyExcluded.bind(accessory)); + return exposesHasAllRequiredFeatures(e, [ThermostatHandler.PREDICATE_SETPOINT, ThermostatHandler.PREDICATE_LOCAL_TEMPERATURE]); } private monitors: CharacteristicMonitor[] = []; @@ -110,25 +108,19 @@ class ThermostatHandler implements ServiceHandler { // Store all required features const possibleLocalTemp = expose.features.find(ThermostatHandler.PREDICATE_LOCAL_TEMPERATURE); - if (possibleLocalTemp === undefined || accessory.isPropertyExcluded(possibleLocalTemp.property)) { + if (possibleLocalTemp === undefined) { throw new Error('Local temperature feature not found.'); } this.localTemperatureExpose = possibleLocalTemp as ExposesEntryWithProperty; const possibleSetpoint = expose.features.find(ThermostatHandler.PREDICATE_SETPOINT); - if (possibleSetpoint === undefined || accessory.isPropertyExcluded(possibleSetpoint.property)) { + if (possibleSetpoint === undefined) { throw new Error('Setpoint feature not found.'); } this.setpointExpose = possibleSetpoint as ExposesEntryWithProperty; this.targetModeExpose = expose.features.find(ThermostatHandler.PREDICATE_TARGET_MODE) as ExposesEntryWithEnumProperty; - if (this.targetModeExpose !== undefined && accessory.isPropertyExcluded(this.targetModeExpose.property)) { - this.targetModeExpose = undefined; - } this.currentStateExpose = expose.features.find(ThermostatHandler.PREDICATE_CURRENT_STATE) as ExposesEntryWithEnumProperty; - if (this.currentStateExpose !== undefined && accessory.isPropertyExcluded(this.currentStateExpose.property)) { - this.currentStateExpose = undefined; - } if (this.targetModeExpose === undefined || this.currentStateExpose === undefined) { if (this.targetModeExpose !== undefined) { this.accessory.log.debug(`${accessory.displayName}: ignore ${this.targetModeExpose.property}; no current state exposed.`); diff --git a/src/converters/cover.ts b/src/converters/cover.ts index aa64f46c..c5569973 100644 --- a/src/converters/cover.ts +++ b/src/converters/cover.ts @@ -47,9 +47,9 @@ class CoverHandler implements ServiceHandler { const endpoint = expose.endpoint; this.identifier = CoverHandler.generateIdentifier(endpoint); - let positionExpose = expose.features.find(e => exposesHasNumericRangeProperty(e) && !accessory.isPropertyExcluded(e.property) + let positionExpose = expose.features.find(e => exposesHasNumericRangeProperty(e) && e.name === 'position' && exposesCanBeSet(e) && exposesIsPublished(e)) as ExposesEntryWithNumericRangeProperty; - this.tiltExpose = expose.features.find(e => exposesHasNumericRangeProperty(e) && !accessory.isPropertyExcluded(e.property) + this.tiltExpose = expose.features.find(e => exposesHasNumericRangeProperty(e) && e.name === 'tilt' && exposesCanBeSet(e) && exposesIsPublished(e)) as ExposesEntryWithNumericRangeProperty | undefined; if (positionExpose === undefined) { diff --git a/src/converters/interfaces.ts b/src/converters/interfaces.ts index f7e9bf5c..a96000e7 100644 --- a/src/converters/interfaces.ts +++ b/src/converters/interfaces.ts @@ -15,10 +15,6 @@ export interface BasicAccessory { queueKeyForGetAction(key: string | string[]): void; - isPropertyExcluded(property: string | undefined): boolean; - - isValueAllowedForProperty(property: string, value: string): boolean; - registerServiceHandler(handler: ServiceHandler): void; isServiceHandlerIdKnown(identifier: string): boolean; diff --git a/src/converters/light.ts b/src/converters/light.ts index 37f1d5ec..f689b72c 100644 --- a/src/converters/light.ts +++ b/src/converters/light.ts @@ -18,7 +18,7 @@ import { EXP_COLOR_MODE } from '../experimental'; export class LightCreator implements ServiceCreator { createServicesFromExposes(accessory: BasicAccessory, exposes: ExposesEntry[]): void { exposes.filter(e => e.type === ExposesKnownTypes.LIGHT && exposesHasFeatures(e) - && exposesHasAllRequiredFeatures(e, [LightHandler.PREDICATE_STATE], accessory.isPropertyExcluded.bind(accessory)) + && exposesHasAllRequiredFeatures(e, [LightHandler.PREDICATE_STATE]) && !accessory.isServiceHandlerIdKnown(LightHandler.generateIdentifier(e.endpoint))) .forEach(e => this.createService(e as ExposesEntryWithFeatures, accessory)); } @@ -58,11 +58,11 @@ class LightHandler implements ServiceHandler { const endpoint = expose.endpoint; this.identifier = LightHandler.generateIdentifier(endpoint); - const features = expose.features.filter(e => exposesHasProperty(e) && !accessory.isPropertyExcluded(e.property)) + const features = expose.features.filter(e => exposesHasProperty(e)) .map(e => e as ExposesEntryWithProperty); // On/off characteristic (required by HomeKit) - const potentialStateExpose = features.find(e => LightHandler.PREDICATE_STATE(e) && !accessory.isPropertyExcluded(e.property)); + const potentialStateExpose = features.find(e => LightHandler.PREDICATE_STATE(e)); if (potentialStateExpose === undefined) { throw new Error('Required "state" property not found for Light.'); } @@ -84,7 +84,7 @@ class LightHandler implements ServiceHandler { this.tryCreateBrightness(features, service); // Color: Hue/Saturation or X/Y - this.tryCreateColor(expose, service, accessory); + this.tryCreateColor(expose, service); // Color temperature this.tryCreateColorTemperature(features, service); @@ -137,17 +137,17 @@ class LightHandler implements ServiceHandler { this.monitors.forEach(m => m.callback(state)); } - private tryCreateColor(expose: ExposesEntryWithFeatures, service: Service, accessory: BasicAccessory) { + private tryCreateColor(expose: ExposesEntryWithFeatures, service: Service) { // First see if color_hs is present this.colorExpose = expose.features.find(e => exposesHasFeatures(e) && e.type === ExposesKnownTypes.COMPOSITE && e.name === 'color_hs' - && e.property !== undefined && !accessory.isPropertyExcluded(e.property)) as ExposesEntryWithFeatures | undefined; + && e.property !== undefined) as ExposesEntryWithFeatures | undefined; // Otherwise check for color_xy if (this.colorExpose === undefined) { this.colorExpose = expose.features.find(e => exposesHasFeatures(e) && e.type === ExposesKnownTypes.COMPOSITE && e.name === 'color_xy' - && e.property !== undefined && !accessory.isPropertyExcluded(e.property)) as ExposesEntryWithFeatures | undefined; + && e.property !== undefined) as ExposesEntryWithFeatures | undefined; } if (this.colorExpose !== undefined && this.colorExpose.property !== undefined) { diff --git a/src/converters/lock.ts b/src/converters/lock.ts index 0ce84786..09c4ae55 100644 --- a/src/converters/lock.ts +++ b/src/converters/lock.ts @@ -14,8 +14,7 @@ import { export class LockCreator implements ServiceCreator { createServicesFromExposes(accessory: BasicAccessory, exposes: ExposesEntry[]): void { exposes.filter(e => e.type === ExposesKnownTypes.LOCK && exposesHasFeatures(e) - && exposesHasAllRequiredFeatures(e, [LockHandler.PREDICATE_LOCK_STATE, LockHandler.PREDICATE_STATE], - accessory.isPropertyExcluded.bind(accessory)) + && exposesHasAllRequiredFeatures(e, [LockHandler.PREDICATE_LOCK_STATE, LockHandler.PREDICATE_STATE]) && !accessory.isServiceHandlerIdKnown(LockHandler.generateIdentifier(e.endpoint))) .forEach(e => this.createService(e as ExposesEntryWithFeatures, accessory)); } @@ -55,15 +54,13 @@ class LockHandler implements ServiceHandler { const endpoint = expose.endpoint; this.identifier = LockHandler.generateIdentifier(endpoint); - const potentialStateExpose = expose.features.find(e => LockHandler.PREDICATE_STATE(e) - && !accessory.isPropertyExcluded(e.property)) as ExposesEntryWithBinaryProperty; + const potentialStateExpose = expose.features.find(e => LockHandler.PREDICATE_STATE(e)) as ExposesEntryWithBinaryProperty; if (potentialStateExpose === undefined) { throw new Error(`Required "${LockHandler.NAME_STATE}" property not found for Lock.`); } this.stateExpose = potentialStateExpose; - const potentialLockStateExpose = expose.features.find(e => LockHandler.PREDICATE_LOCK_STATE(e) - && !accessory.isPropertyExcluded(e.property)) as ExposesEntryWithEnumProperty; + const potentialLockStateExpose = expose.features.find(e => LockHandler.PREDICATE_LOCK_STATE(e)) as ExposesEntryWithEnumProperty; if (potentialLockStateExpose === undefined) { throw new Error(`Required "${LockHandler.NAME_LOCK_STATE}" property not found for Lock.`); } diff --git a/src/converters/switch.ts b/src/converters/switch.ts index d92beebf..35d76a91 100644 --- a/src/converters/switch.ts +++ b/src/converters/switch.ts @@ -41,7 +41,7 @@ export class SwitchCreator implements ServiceCreator { exposeAsOutlet = true; } exposes.filter(e => e.type === ExposesKnownTypes.SWITCH && exposesHasFeatures(e) - && exposesHasAllRequiredFeatures(e, [SwitchHandler.PREDICATE_STATE], accessory.isPropertyExcluded.bind(accessory)) + && exposesHasAllRequiredFeatures(e, [SwitchHandler.PREDICATE_STATE]) && !accessory.isServiceHandlerIdKnown(SwitchHandler.generateIdentifier(exposeAsOutlet, e.endpoint))) .forEach(e => this.createService(e as ExposesEntryWithFeatures, accessory, exposeAsOutlet)); } @@ -69,8 +69,7 @@ class SwitchHandler implements ServiceHandler { this.identifier = SwitchHandler.generateIdentifier(exposeAsOutlet, endpoint); - const potentialStateExpose = expose.features.find(e => SwitchHandler.PREDICATE_STATE(e) - && !accessory.isPropertyExcluded(e.property)) as ExposesEntryWithBinaryProperty; + const potentialStateExpose = expose.features.find(e => SwitchHandler.PREDICATE_STATE(e)) as ExposesEntryWithBinaryProperty; if (potentialStateExpose === undefined) { throw new Error(`Required "state" property not found for ${serviceTypeName}.`); } diff --git a/src/docgen/docs_accessory.ts b/src/docgen/docs_accessory.ts index 50d781fc..138d70da 100644 --- a/src/docgen/docs_accessory.ts +++ b/src/docgen/docs_accessory.ts @@ -77,16 +77,6 @@ export class DocsAccessory implements BasicAccessory { // Do nothing } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - isPropertyExcluded(property: string | undefined): boolean { - return false; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - isValueAllowedForProperty(property: string, value: string): boolean { - return true; - } - registerServiceHandler(handler: ServiceHandler): void { this.handlerIds.add(handler.identifier); } diff --git a/src/helpers.ts b/src/helpers.ts index e5350982..adc8a2c4 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,5 +1,5 @@ import { Characteristic, Service, WithUUID } from 'homebridge'; -import { ExposesEntry, exposesHasNumericRangeProperty } from './z2mModels'; +import { ExposesEntry, exposesHasFeatures, exposesHasNumericRangeProperty } from './z2mModels'; export function errorToString(e: unknown): string { if (typeof e === 'string') { @@ -46,4 +46,53 @@ export function groupByEndpoint(entries: Entry[]): M } }); return endpointMap; +} + +export function getAllEndpoints(entries: ExposesEntry[], parentEndpoint?: string): (string | undefined)[] { + const endpoints = new Set(); + entries.forEach((entry) => { + const endpoint = entry.endpoint ?? parentEndpoint; + if (endpoint !== undefined || entry.property !== undefined) { + endpoints.add(endpoint); + } + if (exposesHasFeatures(entry)) { + getAllEndpoints(entry.features, endpoint).forEach((e) => { + endpoints.add(e); + }); + } + }); + const result = Array.from(endpoints); + result.sort(); + return result; +} + +export function sanitizeAndFilterExposesEntries(input: ExposesEntry[], + filter?: (entry: ExposesEntry) => boolean, valueFilter?: (entry: ExposesEntry) => string[], + parentEndpoint?: string | undefined): ExposesEntry[] { + + return input.filter(e => filter === undefined || filter(e)) + .map(e => sanitizeAndFilterExposesEntry(e, filter, valueFilter, parentEndpoint)); +} + +function sanitizeAndFilterExposesEntry(input: ExposesEntry, + filter?: (entry: ExposesEntry) => boolean, valueFilter?: (entry: ExposesEntry) => string[], + parentEndpoint?: string | undefined): ExposesEntry { + const output: ExposesEntry = { + ...input, + }; + + if (output.endpoint === undefined && parentEndpoint !== undefined) { + // Make sure features inherit the endpoint from their parent, if it is not defined explicitly. + output.endpoint = parentEndpoint; + } + + if (exposesHasFeatures(output)) { + output.features = sanitizeAndFilterExposesEntries(output.features, filter, valueFilter, output.endpoint); + } + + if (Array.isArray(output.values) && valueFilter !== undefined) { + output.values = valueFilter(output); + } + + return output; } \ No newline at end of file diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index 8d196755..e6ce89e7 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -5,9 +5,12 @@ import { hap } from './hap'; import { BasicServiceCreatorManager, ServiceCreatorManager } from './converters/creators'; import { BasicAccessory, ServiceHandler } from './converters/interfaces'; import { BasicLogger } from './logger'; -import { deviceListEntriesAreEqual, DeviceListEntry, isDeviceDefinition, isDeviceListEntry, isDeviceListEntryForGroup } from './z2mModels'; +import { + deviceListEntriesAreEqual, DeviceListEntry, ExposesEntry, isDeviceDefinition, isDeviceListEntry, isDeviceListEntryForGroup, +} from './z2mModels'; import { BaseDeviceConfiguration, isDeviceConfiguration } from './configModels'; import { QoS } from 'mqtt'; +import { sanitizeAndFilterExposesEntries } from './helpers'; export class Zigbee2mqttAccessory implements BasicAccessory { private readonly updateTimer: ExtendedTimer; @@ -121,7 +124,7 @@ export class Zigbee2mqttAccessory implements BasicAccessory { return this.serviceHandlers.has(identifier); } - isPropertyExcluded(property: string | undefined): boolean { + private isPropertyExcluded(property: string | undefined): boolean { if (property === undefined) { // Property is undefined, so it can't be excluded. // This is accepted so all exposes models can easily be checked. @@ -137,7 +140,40 @@ export class Zigbee2mqttAccessory implements BasicAccessory { return this.additionalConfig.excluded_keys?.includes(property) ?? false; } - isValueAllowedForProperty(property: string, value: string): boolean { + private isEndpointExcluded(endpoint: string | undefined): boolean { + 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 ?? ''); + } + + private isExposesEntryExcluded(exposesEntry: ExposesEntry): boolean { + if (this.isPropertyExcluded(exposesEntry.property)) { + return true; + } + + if (this.isEndpointExcluded(exposesEntry.endpoint)) { + return true; + } + + return false; + } + + private filterValuesForExposesEntry(exposesEntry: ExposesEntry): string[] { + if (exposesEntry.values === undefined || exposesEntry.values.length === 0) { + return []; + } + + if (exposesEntry.property === undefined) { + // Do not filter. + return exposesEntry.values; + } + + return exposesEntry.values.filter(v => this.isValueAllowedForProperty(exposesEntry.property ?? '', v)); + } + + private isValueAllowedForProperty(property: string, value: string): boolean { const config = this.additionalConfig.values?.find(c => c.property === property); if (config) { if (config.include && config.include.length > 0) { @@ -277,6 +313,13 @@ export class Zigbee2mqttAccessory implements BasicAccessory { } } + // Filter/sanitize exposes information + if (info?.definition?.exposes !== undefined) { + info.definition.exposes = 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 (isDeviceListEntry(info) diff --git a/src/z2mModels.ts b/src/z2mModels.ts index 65ab029b..cf840bfd 100644 --- a/src/z2mModels.ts +++ b/src/z2mModels.ts @@ -113,10 +113,9 @@ export interface ExposesPredicate { (expose: ExposesEntry): boolean; } -export function exposesHasAllRequiredFeatures(entry: ExposesEntryWithFeatures, features: ExposesPredicate[], - isPropertyExcluded: ((property: string | undefined) => boolean) = () => false): boolean { +export function exposesHasAllRequiredFeatures(entry: ExposesEntryWithFeatures, features: ExposesPredicate[]): boolean { for (const f of features) { - if (entry.features.findIndex(e => f(e) && !isPropertyExcluded(e.property)) < 0) { + if (entry.features.findIndex(e => f(e)) < 0) { // given feature not found return false; } @@ -145,7 +144,7 @@ export function exposesGetOverlap(first: ExposesEntry[], second: ExposesEntry[]) } // Removes endpoint specific info and possible duplicates -function normalizeExposes(entries: ExposesEntry[]): ExposesEntry[] { +export function normalizeExposes(entries: ExposesEntry[]): ExposesEntry[] { const result: ExposesEntry[] = []; for (const entry of entries) { const normalized = exposesRemoveEndpoint(entry); diff --git a/test/action.spec.ts b/test/action.spec.ts index dfc0acb9..f7e28dca 100644 --- a/test/action.spec.ts +++ b/test/action.spec.ts @@ -4,6 +4,7 @@ import { ExposesEntry } from '../src/z2mModels'; import * as hapNodeJs from 'hap-nodejs'; import 'jest-chain'; import { loadExposesFromFile, ServiceHandlersTestHarness } from './testHelpers'; +import { sanitizeAndFilterExposesEntries } from '../src/helpers'; describe('Action', () => { beforeAll(() => { @@ -32,17 +33,17 @@ describe('Action', () => { serviceIdLeft = `${hap.Service.StatelessProgrammableSwitch.UUID}#left`; const leftService = newHarness.getOrAddHandler(hap.Service.StatelessProgrammableSwitch, 'left', serviceIdLeft) .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent) - .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false, undefined, false); + .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false); serviceIdRight = `${hap.Service.StatelessProgrammableSwitch.UUID}#right`; const rightService = newHarness.getOrAddHandler(hap.Service.StatelessProgrammableSwitch, 'right', serviceIdRight) - .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty, false) - .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false, undefined, false); + .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty) + .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false); serviceIdBoth = `${hap.Service.StatelessProgrammableSwitch.UUID}#both`; const bothService = newHarness.getOrAddHandler(hap.Service.StatelessProgrammableSwitch, 'both', serviceIdBoth) - .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty, false) - .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false, undefined, false); + .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty) + .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false); newHarness.prepareCreationMocks(); @@ -128,17 +129,17 @@ describe('Action', () => { serviceIdClose = `${hap.Service.StatelessProgrammableSwitch.UUID}#close`; const closeService = newHarness.getOrAddHandler(hap.Service.StatelessProgrammableSwitch, 'close', serviceIdClose) .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent) - .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false, undefined, false); + .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false); serviceIdOpen = `${hap.Service.StatelessProgrammableSwitch.UUID}#open`; const openService = newHarness.getOrAddHandler(hap.Service.StatelessProgrammableSwitch, 'open', serviceIdOpen) - .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty, false) - .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false, undefined, false); + .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty) + .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false); serviceIdStop = `${hap.Service.StatelessProgrammableSwitch.UUID}#stop`; const stopService = newHarness.getOrAddHandler(hap.Service.StatelessProgrammableSwitch, 'stop', serviceIdStop) - .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty, false) - .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false, undefined, false); + .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty) + .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false); newHarness.prepareCreationMocks(); @@ -226,8 +227,8 @@ describe('Action', () => { expect(deviceExposes.length).toBeGreaterThan(0); const newHarness = new ServiceHandlersTestHarness(); - // For this test set explicit included values (to check that function from the accessory is used correctly) - newHarness.configureAllowedValues('action', [ + // For this test explicitly include certain values (to check that function from the accessory is used correctly) + const allowedActionValues = [ 'button_1_hold', 'button_1_release', 'button_1_single', @@ -241,34 +242,40 @@ describe('Action', () => { 'button_6_hold', 'button_6_release', 'button_6_single', - 'button_6_double']); + 'button_6_double']; + deviceExposes = sanitizeAndFilterExposesEntries(deviceExposes, undefined, e => { + if (e.property === 'action' && Array.isArray(e.values)) { + return e.values.filter(v => allowedActionValues.includes(v)); + } + return e.values ?? []; + }); // Expect 4 services (one for each button) serviceIdButton1 = `${hap.Service.StatelessProgrammableSwitch.UUID}#button_1`; const serviceButton1 = newHarness.getOrAddHandler(hap.Service.StatelessProgrammableSwitch, 'button_1', serviceIdButton1) .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent) - .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false, undefined, false); + .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false); serviceIdButton2 = `${hap.Service.StatelessProgrammableSwitch.UUID}#button_2`; const serviceButton2 = newHarness.getOrAddHandler(hap.Service.StatelessProgrammableSwitch, 'button_2', serviceIdButton2) - .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty, false) - .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false, undefined, false); + .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty) + .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false); serviceIdButton5 = `${hap.Service.StatelessProgrammableSwitch.UUID}#button_5`; const serviceButton5 = newHarness.getOrAddHandler(hap.Service.StatelessProgrammableSwitch, 'button_5', serviceIdButton5) - .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty, false) - .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false, undefined, false); + .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty) + .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false); serviceIdButton5E = `${hap.Service.StatelessProgrammableSwitch.UUID}#button_5#ext1`; const serviceButton5E = newHarness.getOrAddHandler(hap.Service.StatelessProgrammableSwitch, 'button_5#ext1', serviceIdButton5E) - .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty, false) - .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false, undefined, false); + .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty) + .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false); serviceIdButton6 = `${hap.Service.StatelessProgrammableSwitch.UUID}#button_6`; const serviceButton6 = newHarness.getOrAddHandler(hap.Service.StatelessProgrammableSwitch, 'button_6', serviceIdButton6) - .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty, false) - .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false, undefined, false); + .addExpectedCharacteristic(actionProperty, hap.Characteristic.ProgrammableSwitchEvent, false, actionProperty) + .addExpectedCharacteristic(serviceLabelCharacteristic, hap.Characteristic.ServiceLabelIndex, false); newHarness.prepareCreationMocks(); diff --git a/test/air_quality.spec.ts b/test/air_quality.spec.ts index 2d5e2ff9..6b3b52f4 100644 --- a/test/air_quality.spec.ts +++ b/test/air_quality.spec.ts @@ -27,7 +27,7 @@ describe('Air Quality Sensor', () => { // Check service creation newHarness.getOrAddHandler(hap.Service.AirQualitySensor) .addExpectedCharacteristic('voc', hap.Characteristic.VOCDensity) - .addExpectedCharacteristic('aq', hap.Characteristic.AirQuality, false, undefined, false); + .addExpectedCharacteristic('aq', hap.Characteristic.AirQuality); newHarness.prepareCreationMocks(); newHarness.callCreators(deviceExposes); diff --git a/test/climate.spec.ts b/test/climate.spec.ts index 16081ef9..6f43d727 100644 --- a/test/climate.spec.ts +++ b/test/climate.spec.ts @@ -29,7 +29,7 @@ describe('Climate', () => { .addExpectedCharacteristic('local_temperature', hap.Characteristic.CurrentTemperature) .addExpectedCharacteristic('system_mode', hap.Characteristic.TargetHeatingCoolingState, true) .addExpectedCharacteristic('running_state', hap.Characteristic.CurrentHeatingCoolingState) - .addExpectedCharacteristic('unit', hap.Characteristic.TemperatureDisplayUnits, false, undefined, false); + .addExpectedCharacteristic('unit', hap.Characteristic.TemperatureDisplayUnits); newHarness.prepareCreationMocks(); newHarness.callCreators(deviceExposes); diff --git a/test/cover.spec.ts b/test/cover.spec.ts index 6f6b1a17..5fe7fbbf 100644 --- a/test/cover.spec.ts +++ b/test/cover.spec.ts @@ -28,8 +28,8 @@ describe('Cover', () => { // Check service creation const windowCovering = newHarness.getOrAddHandler(hap.Service.WindowCovering) .addExpectedCharacteristic('position', hap.Characteristic.CurrentPosition, false) - .addExpectedCharacteristic('target_position', hap.Characteristic.TargetPosition, true, undefined, false) - .addExpectedCharacteristic('position_state', hap.Characteristic.PositionState, false, undefined, false); + .addExpectedCharacteristic('target_position', hap.Characteristic.TargetPosition, true) + .addExpectedCharacteristic('position_state', hap.Characteristic.PositionState, false); newHarness.prepareCreationMocks(); const positionCharacteristicMock = windowCovering.getCharacteristicMock('position'); @@ -151,10 +151,10 @@ describe('Cover', () => { // Check service creation const windowCovering = newHarness.getOrAddHandler(hap.Service.WindowCovering) .addExpectedCharacteristic('position', hap.Characteristic.CurrentPosition, false) - .addExpectedCharacteristic('target_position', hap.Characteristic.TargetPosition, true, undefined, false) - .addExpectedCharacteristic('position_state', hap.Characteristic.PositionState, false, undefined, false) + .addExpectedCharacteristic('target_position', hap.Characteristic.TargetPosition, true) + .addExpectedCharacteristic('position_state', hap.Characteristic.PositionState, false) .addExpectedCharacteristic('tilt', hap.Characteristic.CurrentHorizontalTiltAngle, false) - .addExpectedCharacteristic('target_tilt', hap.Characteristic.TargetHorizontalTiltAngle, true, undefined, false); + .addExpectedCharacteristic('target_tilt', hap.Characteristic.TargetHorizontalTiltAngle, true); newHarness.prepareCreationMocks(); const positionCharacteristicMock = windowCovering.getCharacteristicMock('position'); @@ -261,8 +261,8 @@ describe('Cover', () => { // Check service creation const windowCovering = newHarness.getOrAddHandler(hap.Service.WindowCovering) .addExpectedCharacteristic('position', hap.Characteristic.CurrentPosition, false, 'tilt') - .addExpectedCharacteristic('target_position', hap.Characteristic.TargetPosition, true, undefined, false) - .addExpectedCharacteristic('position_state', hap.Characteristic.PositionState, false, undefined, false); + .addExpectedCharacteristic('target_position', hap.Characteristic.TargetPosition, true) + .addExpectedCharacteristic('position_state', hap.Characteristic.PositionState, false); newHarness.prepareCreationMocks(); const positionCharacteristicMock = windowCovering.getCharacteristicMock('position'); diff --git a/test/exposes/siglis/zfp-1a-ch.json b/test/exposes/siglis/zfp-1a-ch.json new file mode 100644 index 00000000..32ca08fe --- /dev/null +++ b/test/exposes/siglis/zfp-1a-ch.json @@ -0,0 +1,269 @@ +[ + { + "type": "light", + "features": [ + { + "type": "binary", + "name": "state", + "property": "state_l1", + "access": 7, + "value_on": "ON", + "value_off": "OFF", + "value_toggle": "TOGGLE", + "description": "On/off state of this light", + "endpoint": "l1" + }, + { + "type": "numeric", + "name": "brightness", + "property": "brightness_l1", + "access": 7, + "value_min": 0, + "value_max": 254, + "description": "Brightness of this light", + "endpoint": "l1" + }, + { + "type": "composite", + "property": "color_l1", + "name": "color_xy", + "features": [ + { + "type": "numeric", + "name": "x", + "property": "x", + "access": 7 + }, + { + "type": "numeric", + "name": "y", + "property": "y", + "access": 7 + } + ], + "description": "Color of this light in the CIE 1931 color space (x/y)", + "endpoint": "l1" + } + ], + "endpoint": "l1" + }, + { + "type": "light", + "features": [ + { + "type": "binary", + "name": "state", + "property": "state_l2", + "access": 7, + "value_on": "ON", + "value_off": "OFF", + "value_toggle": "TOGGLE", + "description": "On/off state of this light", + "endpoint": "l2" + }, + { + "type": "numeric", + "name": "brightness", + "property": "brightness_l2", + "access": 7, + "value_min": 0, + "value_max": 254, + "description": "Brightness of this light", + "endpoint": "l2" + } + ], + "endpoint": "l2" + }, + { + "type": "light", + "features": [ + { + "type": "binary", + "name": "state", + "property": "state_l3", + "access": 7, + "value_on": "ON", + "value_off": "OFF", + "value_toggle": "TOGGLE", + "description": "On/off state of this light", + "endpoint": "l3" + }, + { + "type": "numeric", + "name": "brightness", + "property": "brightness_l3", + "access": 7, + "value_min": 0, + "value_max": 254, + "description": "Brightness of this light", + "endpoint": "l3" + } + ], + "endpoint": "l3" + }, + { + "type": "light", + "features": [ + { + "type": "binary", + "name": "state", + "property": "state_l4", + "access": 7, + "value_on": "ON", + "value_off": "OFF", + "value_toggle": "TOGGLE", + "description": "On/off state of this light", + "endpoint": "l4" + }, + { + "type": "numeric", + "name": "brightness", + "property": "brightness_l4", + "access": 7, + "value_min": 0, + "value_max": 254, + "description": "Brightness of this light", + "endpoint": "l4" + } + ], + "endpoint": "l4" + }, + { + "type": "light", + "features": [ + { + "type": "binary", + "name": "state", + "property": "state_l5", + "access": 7, + "value_on": "ON", + "value_off": "OFF", + "value_toggle": "TOGGLE", + "description": "On/off state of this light", + "endpoint": "l5" + }, + { + "type": "numeric", + "name": "brightness", + "property": "brightness_l5", + "access": 7, + "value_min": 0, + "value_max": 254, + "description": "Brightness of this light", + "endpoint": "l5" + } + ], + "endpoint": "l5" + }, + { + "type": "cover", + "features": [ + { + "type": "enum", + "name": "state", + "property": "state_l6", + "access": 7, + "values": [ + "OPEN", + "CLOSE", + "STOP" + ], + "endpoint": "l6" + }, + { + "type": "numeric", + "name": "position", + "property": "position_l6", + "access": 7, + "value_min": 0, + "value_max": 100, + "description": "Position of this cover", + "endpoint": "l6" + }, + { + "type": "numeric", + "name": "tilt", + "property": "tilt_l6", + "access": 7, + "value_min": 0, + "value_max": 100, + "description": "Tilt of this cover", + "endpoint": "l6" + } + ], + "endpoint": "l6" + }, + { + "type": "cover", + "features": [ + { + "type": "enum", + "name": "state", + "property": "state_l7", + "access": 7, + "values": [ + "OPEN", + "CLOSE", + "STOP" + ], + "endpoint": "l7" + }, + { + "type": "numeric", + "name": "position", + "property": "position_l7", + "access": 7, + "value_min": 0, + "value_max": 100, + "description": "Position of this cover", + "endpoint": "l7" + }, + { + "type": "numeric", + "name": "tilt", + "property": "tilt_l7", + "access": 7, + "value_min": 0, + "value_max": 100, + "description": "Tilt of this cover", + "endpoint": "l7" + } + ], + "endpoint": "l7" + }, + { + "type": "enum", + "name": "action", + "property": "action", + "access": 1, + "values": [ + "button_1_single", + "button_1_double", + "button_1_hold", + "button_1_release", + "button_2_single", + "button_2_double", + "button_2_hold", + "button_2_release", + "button_3_single", + "button_3_double", + "button_3_hold", + "button_3_release", + "button_4_single", + "button_4_double", + "button_4_hold", + "button_4_release" + ], + "description": "Triggered action (e.g. a button click)" + }, + { + "type": "numeric", + "name": "linkquality", + "property": "linkquality", + "access": 1, + "unit": "lqi", + "description": "Link quality (signal strength)", + "value_min": 0, + "value_max": 255 + } +] \ No newline at end of file diff --git a/test/helper.spec.ts b/test/helper.spec.ts new file mode 100644 index 00000000..be6f4078 --- /dev/null +++ b/test/helper.spec.ts @@ -0,0 +1,43 @@ + +import 'jest-chain'; +import { getAllEndpoints, sanitizeAndFilterExposesEntries } from '../src/helpers'; +import { exposesCollectionsAreEqual, normalizeExposes } from '../src/z2mModels'; +import { loadExposesFromFile } from './testHelpers'; + +describe('Helper functions', () => { + + test('Add missing endpoints to ExposesEntry', () => { + const exposes = loadExposesFromFile('siglis/zfp-1a-ch.json'); + const sanitized = sanitizeAndFilterExposesEntries(exposes); + + // Should not be identical, as explicit endpoint information is added. + expect(exposesCollectionsAreEqual(exposes, sanitized)).toBe(false); + + // After normalization (a.k.a. removing endpoints), the collections should be identical. + const originalNormalized = normalizeExposes(exposes); + const sanitizedNormalized = normalizeExposes(sanitized); + expect(exposesCollectionsAreEqual(originalNormalized, sanitizedNormalized)).toBe(true); + + // TODO: Check if the added endpoints are correct (a.k.a. if it is actually sanitized) + }); + + test('Get all endpoints', () => { + const exposes = loadExposesFromFile('siglis/zfp-1a-ch.json'); + const endpoints = getAllEndpoints(exposes); + endpoints.sort(); + + const expectedEndpoints = [ + undefined, + 'l1', + 'l2', + 'l3', + 'l4', + 'l5', + 'l6', + 'l7', + ]; + expectedEndpoints.sort(); + + expect(endpoints).toEqual(expectedEndpoints); + }); +}); \ No newline at end of file diff --git a/test/light.spec.ts b/test/light.spec.ts index d5274459..a6c3970a 100644 --- a/test/light.spec.ts +++ b/test/light.spec.ts @@ -171,8 +171,8 @@ describe('Light', () => { .addExpectedCharacteristic('brightness', hap.Characteristic.Brightness, true) .addExpectedCharacteristic('color_temp', hap.Characteristic.ColorTemperature, true) .addExpectedPropertyCheck('color') - .addExpectedCharacteristic('hue', hap.Characteristic.Hue, true, undefined, false) - .addExpectedCharacteristic('saturation', hap.Characteristic.Saturation, true, undefined, false); + .addExpectedCharacteristic('hue', hap.Characteristic.Hue, true) + .addExpectedCharacteristic('saturation', hap.Characteristic.Saturation, true); newHarness.prepareCreationMocks(); newHarness.callCreators(deviceExposes); @@ -474,8 +474,8 @@ describe('Light', () => { .addExpectedCharacteristic('brightness', hap.Characteristic.Brightness, true) .addExpectedCharacteristic('color_temp', hap.Characteristic.ColorTemperature, true) .addExpectedPropertyCheck('color') - .addExpectedCharacteristic('hue', hap.Characteristic.Hue, true, undefined, false) - .addExpectedCharacteristic('saturation', hap.Characteristic.Saturation, true, undefined, false); + .addExpectedCharacteristic('hue', hap.Characteristic.Hue, true) + .addExpectedCharacteristic('saturation', hap.Characteristic.Saturation, true); newHarness.prepareCreationMocks(); newHarness.callCreators(deviceExposes); @@ -647,8 +647,8 @@ describe('Light', () => { .addExpectedCharacteristic('brightness', hap.Characteristic.Brightness, true) .addExpectedCharacteristic('color_temp', hap.Characteristic.ColorTemperature, true) .addExpectedPropertyCheck('color') - .addExpectedCharacteristic('hue', hap.Characteristic.Hue, true, undefined, false) - .addExpectedCharacteristic('saturation', hap.Characteristic.Saturation, true, undefined, false); + .addExpectedCharacteristic('hue', hap.Characteristic.Hue, true) + .addExpectedCharacteristic('saturation', hap.Characteristic.Saturation, true); newHarness.prepareCreationMocks(); newHarness.callCreators(deviceExposes); diff --git a/test/testHelpers.ts b/test/testHelpers.ts index 8a8777d5..462e6fff 100644 --- a/test/testHelpers.ts +++ b/test/testHelpers.ts @@ -116,7 +116,6 @@ class TestCharacteristic { readonly topLevelProperty: string, readonly characteristic: WithUUID<{ new(): Characteristic }> | undefined, readonly doExpectSet: boolean, - readonly doExpectCheckPropertyExcluded: boolean, ) { if (characteristic !== undefined) { this.mock = mock(); @@ -129,7 +128,7 @@ export declare type ServiceIdentifier = string | WithUUID<{ new(): Service }>; export interface ServiceHandlerContainer { addExpectedPropertyCheck(property: string): ServiceHandlerContainer; addExpectedCharacteristic(identifier: string, characteristic: WithUUID<{ new(): Characteristic }>, doExpectSet?: boolean, - property?: string, doExpectCheckPropertyExcluded?: boolean): ServiceHandlerContainer; + property?: string): ServiceHandlerContainer; checkCharacteristicPropertiesHaveBeenSet(identifier: string, props: Partial): ServiceHandlerContainer; @@ -161,19 +160,18 @@ class ServiceHandlerTestData implements ServiceHandlerContainer { addExpectedPropertyCheck(property: string): ServiceHandlerContainer { expect(this.characteristics.has(property)).toBeFalsy(); - this.characteristics.set(property, new TestCharacteristic(property, undefined, false, true)); + this.characteristics.set(property, new TestCharacteristic(property, undefined, false)); return this; } addExpectedCharacteristic(identifier: string, characteristic: WithUUID<{ new(): Characteristic }>, doExpectSet = false, - property: string | undefined = undefined, doExpectCheckPropertyExcluded = true): ServiceHandlerContainer { + property: string | undefined = undefined): ServiceHandlerContainer { if (property === undefined) { property = identifier; } expect(this.characteristics.has(identifier)).toBeFalsy(); - this.characteristics.set(identifier, new TestCharacteristic(property, characteristic, doExpectSet, - doExpectCheckPropertyExcluded)); + this.characteristics.set(identifier, new TestCharacteristic(property, characteristic, doExpectSet)); return this; } @@ -276,7 +274,6 @@ class ServiceHandlerTestData implements ServiceHandlerContainer { export class ServiceHandlersTestHarness { private readonly handlers = new Map(); - private readonly allowedValues = new Map(); private readonly experimentalFeatures = new Set(); private readonly converterConfig = new Map(); readonly accessoryMock: MockProxy & BasicAccessory; @@ -286,11 +283,6 @@ export class ServiceHandlersTestHarness { this.accessoryMock.log = mock(); // Mock implementations of certain accessory functions - this.accessoryMock.isValueAllowedForProperty - .mockImplementation((property: string, value: string): boolean => { - return this.allowedValues.get(property)?.includes(value) ?? true; - }); - this.accessoryMock.isExperimentalFeatureEnabled .mockImplementation((feature: string): boolean => { return this.experimentalFeatures.has(feature.trim().toLocaleUpperCase()); @@ -332,10 +324,6 @@ export class ServiceHandlersTestHarness { }); } - configureAllowedValues(property: string, values: string[]) { - this.allowedValues.set(property, values); - } - addExperimentalFeatureFlags(feature: string): void { this.experimentalFeatures.add(feature); } @@ -393,11 +381,6 @@ export class ServiceHandlersTestHarness { prepareCreationMocks(): void { for (const data of this.handlers.values()) { for (const mapping of data.characteristics.values()) { - if (mapping.doExpectCheckPropertyExcluded) { - when(this.accessoryMock.isPropertyExcluded) - .calledWith(mapping.topLevelProperty) - .mockReturnValue(false); - } if (mapping.characteristic !== undefined) { when(data.serviceMock.getCharacteristic) .calledWith(mapping.characteristic) @@ -453,11 +436,6 @@ export class ServiceHandlersTestHarness { expect(this.accessoryMock.registerServiceHandler.mock.calls.length).toBeGreaterThanOrEqual(expectedCallsToRegisterServiceHandler); for (const mapping of handler.characteristics.values()) { - if (mapping.doExpectCheckPropertyExcluded) { - expect(this.accessoryMock.isPropertyExcluded) - .toBeCalledWith(mapping.topLevelProperty); - } - if (mapping.characteristic !== undefined) { expect(handler.serviceMock.getCharacteristic) .toBeCalledWith(mapping.characteristic);