Skip to content

Commit

Permalink
Improve check in createOrUpdateAccessory to check validity of device …
Browse files Browse the repository at this point in the history
…definition.
  • Loading branch information
itavero committed Jan 2, 2024
1 parent 62e15d4 commit 59512ae
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 150 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Since version 1.0.0, we try to follow the [Semantic Versioning](https://semver.o

### Fixed

- Type checks on Z2M models now explicitly check that the input is not null or undefined, to prevent crashes when we get unexpected data (see [#794](https://github.com/itavero/homebridge-z2m/issues/794))
- Type checks on Z2M models now explicitly check that the input is not null or undefined, to prevent crashes when we get unexpected data. (see [#794](https://github.com/itavero/homebridge-z2m/issues/794))
- When creating or updating an accessory, previously it was only checked if the device definition was not undefined. Now we check if it seems to be a valid device definition. (see [#794](https://github.com/itavero/homebridge-z2m/issues/794))

## [1.9.2] - 2022-10-01

Expand Down
9 changes: 7 additions & 2 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
import * as mqtt from 'mqtt';
import * as fs from 'fs';
import {
DeviceListEntry, DeviceListEntryForGroup, ExposesEntry, exposesGetOverlap, GroupListEntry, isDeviceListEntry, isDeviceListEntryForGroup,
DeviceListEntry, DeviceListEntryForGroup, ExposesEntry, exposesGetOverlap, GroupListEntry, isDeviceDefinition, isDeviceListEntry,
isDeviceListEntryForGroup,
} from './z2mModels';
import * as semver from 'semver';
import { errorToString } from './helpers';
Expand Down Expand Up @@ -422,7 +423,11 @@ export class Zigbee2mqttPlatform implements DynamicPlatformPlugin {
}

private createOrUpdateAccessory(device: DeviceListEntry) {
if (!device.supported || device.definition === undefined || this.isDeviceExcluded(device)) {
if (
!device.supported ||
!isDeviceDefinition(device.definition) ||
this.isDeviceExcluded(device)
) {
return;
}
const uuid_input = isDeviceListEntryForGroup(device) ? `group-${device.group_id}` : device.ieee_address;
Expand Down
209 changes: 62 additions & 147 deletions src/z2mModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,14 @@ export const isExposesEntry = (x: any): x is ExposesEntry => {
return false;
}

return (
x.name !== undefined ||
x.property !== undefined ||
x.access !== undefined ||
x.endpoint !== undefined ||
x.values !== undefined ||
(x.value_off !== undefined && x.value_on !== undefined) ||
(x.value_min !== undefined && x.value_max !== undefined) ||
Array.isArray(x.features)
);
return (x.name !== undefined
|| x.property !== undefined
|| x.access !== undefined
|| x.endpoint !== undefined
|| x.values !== undefined
|| (x.value_off !== undefined && x.value_on !== undefined)
|| (x.value_min !== undefined && x.value_max !== undefined)
|| Array.isArray(x.features));
};

export interface ExposesEntryWithFeatures extends ExposesEntry {
Expand All @@ -67,17 +65,15 @@ export interface ExposesEntryWithProperty extends ExposesEntry {
access: number;
}

export interface ExposesEntryWithNumericRangeProperty
extends ExposesEntryWithProperty {
export interface ExposesEntryWithNumericRangeProperty extends ExposesEntryWithProperty {
name: string;
property: string;
access: number;
value_min: number;
value_max: number;
}

export interface ExposesEntryWithBinaryProperty
extends ExposesEntryWithProperty {
export interface ExposesEntryWithBinaryProperty extends ExposesEntryWithProperty {
name: string;
property: string;
access: number;
Expand All @@ -92,71 +88,38 @@ export interface ExposesEntryWithEnumProperty extends ExposesEntryWithProperty {
values: string[];
}

export const exposesHasFeatures = (
x: ExposesEntry,
): x is ExposesEntryWithFeatures => 'features' in x;
export const exposesHasProperty = (
x: ExposesEntry,
): x is ExposesEntryWithProperty =>
x.name !== undefined && x.property !== undefined && x.access !== undefined;
export const exposesHasNumericProperty = (
x: ExposesEntry,
): x is ExposesEntryWithProperty =>
exposesHasProperty(x) && x.type === ExposesKnownTypes.NUMERIC;
export const exposesHasNumericRangeProperty = (
x: ExposesEntry,
): x is ExposesEntryWithNumericRangeProperty =>
exposesHasNumericProperty(x) &&
x.value_min !== undefined &&
x.value_max !== undefined;
export const exposesHasBinaryProperty = (
x: ExposesEntry,
): x is ExposesEntryWithBinaryProperty =>
exposesHasProperty(x) &&
x.type === ExposesKnownTypes.BINARY &&
x.value_on !== undefined &&
x.value_off !== undefined;
export const exposesHasEnumProperty = (
x: ExposesEntry,
): x is ExposesEntryWithEnumProperty =>
exposesHasProperty(x) &&
x.type === ExposesKnownTypes.ENUM &&
x.values !== undefined &&
x.values.length > 0;
export const exposesHasFeatures = (x: ExposesEntry): x is ExposesEntryWithFeatures => ('features' in x);
export const exposesHasProperty = (x: ExposesEntry): x is ExposesEntryWithProperty => (x.name !== undefined
&& x.property !== undefined && x.access !== undefined);
export const exposesHasNumericProperty = (x: ExposesEntry): x is ExposesEntryWithProperty => (exposesHasProperty(x)
&& x.type === ExposesKnownTypes.NUMERIC);
export const exposesHasNumericRangeProperty = (x: ExposesEntry): x is ExposesEntryWithNumericRangeProperty => (exposesHasNumericProperty(x)
&& x.value_min !== undefined && x.value_max !== undefined);
export const exposesHasBinaryProperty = (x: ExposesEntry): x is ExposesEntryWithBinaryProperty => (exposesHasProperty(x)
&& x.type === ExposesKnownTypes.BINARY && x.value_on !== undefined && x.value_off !== undefined);
export const exposesHasEnumProperty = (x: ExposesEntry): x is ExposesEntryWithEnumProperty => (exposesHasProperty(x)
&& x.type === ExposesKnownTypes.ENUM && x.values !== undefined && x.values.length > 0);

export function exposesCanBeSet(entry: ExposesEntry): boolean {
return (
entry.access !== undefined && (entry.access & ExposesAccessLevel.SET) !== 0
);
return (entry.access !== undefined) && ((entry.access & ExposesAccessLevel.SET) !== 0);
}

export function exposesCanBeGet(entry: ExposesEntry): boolean {
return (
entry.access !== undefined && (entry.access & ExposesAccessLevel.GET) !== 0
);
return (entry.access !== undefined) && ((entry.access & ExposesAccessLevel.GET) !== 0);
}

export function exposesIsPublished(entry: ExposesEntry): boolean {
return (
entry.access !== undefined &&
(entry.access & ExposesAccessLevel.PUBLISHED) !== 0
);
return (entry.access !== undefined) && ((entry.access & ExposesAccessLevel.PUBLISHED) !== 0);
}

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[],
isPropertyExcluded: ((property: string | undefined) => boolean) = () => false): boolean {
for (const f of features) {
if (
entry.features.findIndex((e) => f(e) && !isPropertyExcluded(e.property)) <
0
) {
if (entry.features.findIndex(e => f(e) && !isPropertyExcluded(e.property)) < 0) {
// given feature not found
return false;
}
Expand All @@ -166,21 +129,13 @@ export function exposesHasAllRequiredFeatures(
return true;
}

export function exposesGetOverlap(
first: ExposesEntry[],
second: ExposesEntry[],
): ExposesEntry[] {
export function exposesGetOverlap(first: ExposesEntry[], second: ExposesEntry[]): ExposesEntry[] {
const result: ExposesEntry[] = [];

const secondNormalized = normalizeExposes(second);

for (const entry of normalizeExposes(first)) {
const match = secondNormalized.find(
(x) =>
x.name === entry.name &&
x.property === entry.property &&
x.type === entry.type,
);
const match = secondNormalized.find((x) => x.name === entry.name && x.property === entry.property && x.type === entry.type);
if (match !== undefined) {
const merged = exposesGetMergedEntry(entry, match);
if (merged !== undefined) {
Expand Down Expand Up @@ -221,16 +176,13 @@ function exposesRemoveEndpoint(entry: ExposesEntry): ExposesEntry {
return result;
}

export function exposesGetMergedEntry(
first: ExposesEntry,
second: ExposesEntry,
): ExposesEntry | undefined {
export function exposesGetMergedEntry(first: ExposesEntry, second: ExposesEntry): ExposesEntry | undefined {
const result: ExposesEntry | ExposesEntryWithFeatures = {
type: first.type,
};
for (const member in first) {
if (!Array.isArray(first[member])) {
if (member in second && second[member] === first[member]) {
if ((member in second) && (second[member] === first[member])) {
result[member] = first[member];
}
}
Expand All @@ -254,10 +206,7 @@ export function exposesGetMergedEntry(
}
break;
case ExposesKnownTypes.BINARY:
if (
first.value_on !== second.value_on ||
first.value_off !== second.value_off
) {
if (first.value_on !== second.value_on || first.value_off !== second.value_off) {
return undefined;
}
break;
Expand All @@ -279,13 +228,9 @@ export function exposesGetMergedEntry(
if (exposesHasFeatures(first) && exposesHasFeatures(second)) {
result['features'] = [];
for (const feature of first.features) {
const match = second.features.find(
(x) =>
x.name === feature.name &&
x.property === feature.property &&
x.type === feature.type,
);
const match = second.features.find((x) => x.name === feature.name && x.property === feature.property && x.type === feature.type);
if (match !== undefined) {

const merged = exposesGetMergedEntry(feature, match);
if (merged !== undefined) {
result['features'].push(merged);
Expand All @@ -298,29 +243,22 @@ export function exposesGetMergedEntry(
return result;
}

export function exposesAreEqual(
first: ExposesEntry,
second: ExposesEntry,
): boolean {
if (
first.type !== second.type ||
first.name !== second.name ||
first.property !== second.property ||
first.access !== second.access ||
first.endpoint !== second.endpoint ||
first.value_min !== second.value_min ||
first.value_max !== second.value_max ||
first.value_off !== second.value_off ||
first.value_on !== second.value_on ||
first.values?.length !== second.values?.length
) {
export function exposesAreEqual(first: ExposesEntry, second: ExposesEntry): boolean {
if (first.type !== second.type
|| first.name !== second.name
|| first.property !== second.property
|| first.access !== second.access
|| first.endpoint !== second.endpoint
|| first.value_min !== second.value_min
|| first.value_max !== second.value_max
|| first.value_off !== second.value_off
|| first.value_on !== second.value_on
|| first.values?.length !== second.values?.length) {
return false;
}

if (first.values !== undefined && second?.values !== undefined) {
const missing = first.values.filter(
(v) => !(second.values?.includes(v) ?? false),
);
const missing = first.values.filter(v => !(second.values?.includes(v) ?? false));
if (missing.length > 0) {
return false;
}
Expand All @@ -337,16 +275,13 @@ export function exposesAreEqual(
return true;
}

export function exposesCollectionsAreEqual(
first: ExposesEntry[],
second: ExposesEntry[],
): boolean {
export function exposesCollectionsAreEqual(first: ExposesEntry[], second: ExposesEntry[]): boolean {
if (first.length !== second.length) {
return false;
}

for (const firstEntry of first) {
if (second.findIndex((e) => exposesAreEqual(firstEntry, e)) < 0) {
if (second.findIndex(e => exposesAreEqual(firstEntry, e)) < 0) {
return false;
}
}
Expand Down Expand Up @@ -377,49 +312,29 @@ export interface DeviceListEntryForGroup extends DeviceListEntry {

export const isDeviceListEntry = (x: any): x is DeviceListEntry =>
!isNullOrUndefined(x) && x.ieee_address && x.friendly_name && x.supported;
export const isDeviceListEntryForGroup = (
x: any,
): x is DeviceListEntryForGroup => {
return (
isDeviceListEntry(x) && 'group_id' in x && typeof x['group_id'] === 'number'
);
export const isDeviceListEntryForGroup = (x: any): x is DeviceListEntryForGroup => {
return (isDeviceListEntry(x) && 'group_id' in x && typeof x['group_id'] === 'number');
};
export function deviceListEntriesAreEqual(
first: DeviceListEntry | undefined,
second: DeviceListEntry | undefined,
): boolean {
export function deviceListEntriesAreEqual(first: DeviceListEntry | undefined, second: DeviceListEntry | undefined): boolean {
if (first === undefined || second === undefined) {
return first === undefined && second === undefined;
return (first === undefined && second === undefined);
}

if (
first.friendly_name !== second.friendly_name ||
first.ieee_address !== second.ieee_address ||
first.supported !== second.supported ||
first.software_build_id !== second.software_build_id ||
first.date_code !== second.date_code
) {
if (first.friendly_name !== second.friendly_name
|| first.ieee_address !== second.ieee_address
|| first.supported !== second.supported
|| first.software_build_id !== second.software_build_id
|| first.date_code !== second.date_code) {
return false;
}

if (
isNullOrUndefined(first.definition) ||
isNullOrUndefined(second.definition)
) {
return (
isNullOrUndefined(first.definition) &&
isNullOrUndefined(second.definition)
);
if (isNullOrUndefined(first.definition) || isNullOrUndefined(second.definition)) {
return isNullOrUndefined(first.definition) && isNullOrUndefined(second.definition);
}

return (
first.definition.model === second.definition.model &&
first.definition.vendor === second.definition.vendor &&
exposesCollectionsAreEqual(
first.definition.exposes,
second.definition.exposes,
)
);
return (first.definition.model === second.definition.model
&& first.definition.vendor === second.definition.vendor
&& exposesCollectionsAreEqual(first.definition.exposes, second.definition.exposes));
}

export interface GroupMember {
Expand Down

0 comments on commit 59512ae

Please sign in to comment.