Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter exposes information earlier. Exclude based on endpoint #534

Merged
merged 1 commit into from
Oct 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -199,6 +208,9 @@
"excluded_keys": {
"$ref": "#/definitions/excluded_keys"
},
"excluded_endpoints": {
"$ref": "#/definitions/excluded_endpoints"
},
"values": {
"$ref": "#/definitions/values"
},
Expand Down Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ A possible configuration looks like this:
},
{
"id": "0xabcd1234abcd1234",
"excluded_endpoints": [
"l2"
],
"converters": {
"switch": {
"type": "outlet"
Expand Down Expand Up @@ -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.

Expand Down
6 changes: 6 additions & 0 deletions src/configModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const isMqttConfiguration = (x: any): x is MqttConfiguration => (
export interface BaseDeviceConfiguration extends Record<string, unknown> {
exclude?: boolean;
excluded_keys?: string[];
excluded_endpoints?: string[];
values?: PropertyValueConfiguration[];
converters?: object;
experimental?: string[];
Expand All @@ -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;
Expand Down
6 changes: 2 additions & 4 deletions src/converters/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 2 additions & 3 deletions src/converters/air_quality.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))) {
Expand Down
2 changes: 1 addition & 1 deletion src/converters/basic_sensors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion src/converters/battery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
16 changes: 4 additions & 12 deletions src/converters/climate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand All @@ -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.`);
Expand Down
4 changes: 2 additions & 2 deletions src/converters/cover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 0 additions & 4 deletions src/converters/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 7 additions & 7 deletions src/converters/light.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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.');
}
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
9 changes: 3 additions & 6 deletions src/converters/lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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.`);
}
Expand Down
5 changes: 2 additions & 3 deletions src/converters/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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}.`);
}
Expand Down
10 changes: 0 additions & 10 deletions src/docgen/docs_accessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading