From 87cd04ac29c1e6864bfca5b7dd0d337c8106dac0 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Sat, 16 Nov 2024 23:15:32 +0100 Subject: [PATCH] Controller energy (#128) * Add generic Polling logic ... this allows to define which attributes are polled and do this in an optimized way and init the polling interval and expose config * Define Polling for custom energy states * Adjust to new constructor * Use Bridged device reachability over node connected status * inspec matter,js errors for more infos ... because errors can be nested and are not included in default toString() * Optimize device updates * Improve infos and device config * make linter happy * adjust logging --- src/lib/DeviceManagement.ts | 139 ++++++++--- src/main.ts | 2 +- src/matter/ControllerNode.ts | 63 ++--- src/matter/DeviceNode.ts | 63 +++-- src/matter/GeneralMatterNode.ts | 32 ++- .../to-iobroker/ContactSensorToIoBroker.ts | 6 +- src/matter/to-iobroker/DimmableToIobroker.ts | 6 +- src/matter/to-iobroker/DoorLockToIoBroker.ts | 6 +- .../to-iobroker/GenericDeviceToIoBroker.ts | 234 +++++++++++++++++- .../GenericElectricityDataDeviceToIoBroker.ts | 11 +- .../to-iobroker/HumiditySensorToIoBroker.ts | 6 +- src/matter/to-iobroker/OccupancyToIoBroker.ts | 6 +- .../to-iobroker/OnOffLightToIoBroker.ts | 6 +- .../to-iobroker/OnOffPlugInUnitToIoBroker.ts | 6 +- .../TemperatureSensorToIoBroker.ts | 6 +- .../to-iobroker/UtilityOnlyToIoBroker.ts | 6 +- .../WaterLeakDetectorToIoBroker.ts | 6 +- src/matter/to-iobroker/ioBrokerFactory.ts | 47 +++- 18 files changed, 516 insertions(+), 135 deletions(-) diff --git a/src/lib/DeviceManagement.ts b/src/lib/DeviceManagement.ts index 01f0b41f..0c46a645 100644 --- a/src/lib/DeviceManagement.ts +++ b/src/lib/DeviceManagement.ts @@ -10,9 +10,10 @@ import { type DeviceRefresh, type DeviceStatus, type InstanceDetails, + type JsonFormData, } from '@iobroker/dm-utils'; import type { GeneralMatterNode, NodeDetails } from '../matter/GeneralMatterNode'; -import type { GenericDeviceToIoBroker } from '../matter/to-iobroker/GenericDeviceToIoBroker'; +import { GenericDeviceToIoBroker } from '../matter/to-iobroker/GenericDeviceToIoBroker'; import { getText, t } from './i18n'; import { decamelize } from './utils'; @@ -155,6 +156,12 @@ class MatterAdapterDeviceManagement extends DeviceManagement { description: t('Configure this node'), handler: (id, context) => this.#handleConfigureNode(ioNode, context), }, + { + id: 'logNodeDebug', + icon: 'fa-solid fa-file-lines', // Why icon does not work?? + description: t('Output Debug details this node'), + handler: (id, context) => this.#handleLogDebugNode(ioNode, context), + }, ], }); @@ -279,61 +286,114 @@ class MatterAdapterDeviceManagement extends DeviceManagement { return Promise.resolve({ refresh: false }); } + async #handleLogDebugNode(node: GeneralMatterNode, context: ActionContext): Promise<{ refresh: DeviceRefresh }> { + const debugInfos = 'TODO'; + + await context.showForm( + { + type: 'panel', + items: { + debugInfos: { + type: 'text', + label: t('Debug Infos'), + minRows: 30, + sm: 12, + disabled: true, + }, + }, + style: { + minWidth: 200, + }, + }, + { + data: { debugInfos }, + title: t('Debug Infos'), + }, + ); + + return { refresh: false }; + } + async #handleConfigureNodeOrDevice( title: string, baseId: string, context: ActionContext, + nodeOrDevice: GeneralMatterNode | GenericDeviceToIoBroker, ): Promise<{ refresh: DeviceRefresh }> { const obj = await this.adapter.getObjectAsync(baseId); + //const node = nodeOrDevice instanceof GeneralMatterNode ? nodeOrDevice : undefined; + const device = nodeOrDevice instanceof GenericDeviceToIoBroker ? nodeOrDevice : undefined; + + const items: Record = { + exposeMatterApplicationClusterData: { + type: 'select', + label: t('Expose Matter Application Cluster Data'), + options: [ + { label: 'Yes', value: 'true' }, + { label: 'No', value: 'false' }, + { label: 'Default', value: '' }, + ], + sm: 12, + }, + exposeMatterSystemClusterData: { + type: 'select', + label: t('Expose Matter System Cluster Data'), + options: [ + { label: 'Yes', value: 'true' }, + { label: 'No', value: 'false' }, + { label: 'Default', value: '' }, + ], + sm: 12, + }, + }; + const data: JsonFormData = { + exposeMatterApplicationClusterData: + typeof obj?.native.exposeMatterApplicationClusterData !== 'boolean' + ? '' + : obj?.native.exposeMatterApplicationClusterData + ? 'true' + : 'false', + exposeMatterSystemClusterData: + typeof obj?.native.exposeMatterSystemClusterData !== 'boolean' + ? '' + : obj?.native.exposeMatterSystemClusterData + ? 'true' + : 'false', + }; + + if (device !== undefined) { + const deviceConfig = device.deviceConfiguration; + if (deviceConfig.pollInterval !== undefined) { + items.pollInterval = { + type: 'number', + label: t('Energy Attribute Polling Interval (s)'), + min: 30, + max: 2147482, + sm: 12, + }; + data.pollInterval = deviceConfig.pollInterval; + } + } + const result = await context.showForm( { type: 'panel', - items: { - exposeMatterApplicationClusterData: { - type: 'select', - label: t('Expose Matter Application Cluster Data'), - options: [ - { label: 'Yes', value: 'true' }, - { label: 'No', value: 'false' }, - { label: 'Default', value: '' }, - ], - sm: 12, - }, - exposeMatterSystemClusterData: { - type: 'select', - label: t('Expose Matter System Cluster Data'), - options: [ - { label: 'Yes', value: 'true' }, - { label: 'No', value: 'false' }, - { label: 'Default', value: '' }, - ], - sm: 12, - }, - }, + items, style: { minWidth: 200, }, }, { - data: { - exposeMatterApplicationClusterData: - typeof obj?.native.exposeMatterApplicationClusterData !== 'boolean' - ? '' - : obj?.native.exposeMatterApplicationClusterData - ? 'true' - : 'false', - exposeMatterSystemClusterData: - typeof obj?.native.exposeMatterSystemClusterData !== 'boolean' - ? '' - : obj?.native.exposeMatterSystemClusterData - ? 'true' - : 'false', - }, + data, title: t(title), }, ); + if (result?.pollInterval !== undefined && device !== undefined) { + device.setDeviceConfiguration({ pollInterval: result.pollInterval }); + } + if ( result?.exposeMatterApplicationClusterData !== undefined && result?.exposeMatterSystemClusterData !== undefined @@ -342,6 +402,7 @@ class MatterAdapterDeviceManagement extends DeviceManagement { native: { exposeMatterApplicationClusterData: strToBool(result.exposeMatterApplicationClusterData), exposeMatterSystemClusterData: strToBool(result.exposeMatterSystemClusterData), + ...(device !== undefined ? device.deviceConfiguration : {}), }, }); } @@ -351,7 +412,7 @@ class MatterAdapterDeviceManagement extends DeviceManagement { async #handleConfigureNode(node: GeneralMatterNode, context: ActionContext): Promise<{ refresh: DeviceRefresh }> { this.adapter.log.info(`Configure node ${node.nodeId}`); - return await this.#handleConfigureNodeOrDevice('Configure node', node.nodeBaseId, context); + return await this.#handleConfigureNodeOrDevice('Configure node', node.nodeBaseId, context, node); } async #handleConfigureDevice( @@ -360,7 +421,7 @@ class MatterAdapterDeviceManagement extends DeviceManagement { ): Promise<{ refresh: DeviceRefresh }> { this.adapter.log.info(`Configure device ${device.name}`); - return await this.#handleConfigureNodeOrDevice('Configure device', device.baseId, context); + return await this.#handleConfigureNodeOrDevice('Configure device', device.baseId, context, device); } async #handleIdentifyDevice( diff --git a/src/main.ts b/src/main.ts index da081473..c73ec2db 100644 --- a/src/main.ts +++ b/src/main.ts @@ -462,7 +462,7 @@ export class MatterAdapter extends utils.Adapter { // controller sub node changed const nodeId = objParts[1]; await this.syncControllerNode(nodeId, obj as ioBroker.FolderObject); - } else if (objParts[0] === 'controller' && objPartsLength === 3 && obj?.type === 'device') { + } else if (objParts[0] === 'controller' && objPartsLength > 2 && obj?.type === 'device') { // controller node device sub node changed const nodeId = objParts[1]; const nodeObj = await this.getObjectAsync(`controller.${nodeId}`); diff --git a/src/matter/ControllerNode.ts b/src/matter/ControllerNode.ts index 4f734b05..4953941f 100644 --- a/src/matter/ControllerNode.ts +++ b/src/matter/ControllerNode.ts @@ -14,6 +14,7 @@ import type { MatterControllerConfig } from '../../src-admin/src/types'; import type { MatterAdapter } from '../main'; import { GeneralMatterNode, type PairedNodeConfig } from './GeneralMatterNode'; import type { GeneralNode, MessageResponse } from './GeneralNode'; +import { inspect } from 'util'; export interface ControllerCreateOptions { adapter: MatterAdapter; @@ -184,8 +185,9 @@ class Controller implements GeneralNode { return await this.completeCommissioningForNode(message.peerNodeId, message.discoveryData); } } catch (error) { - this.#adapter.log.warn(`Error while executing command "${command}": ${error.stack}`); - return { error: `Error while executing command "${command}": ${error.message}` }; + const errorText = inspect(error, { depth: 10 }); + this.#adapter.log.warn(`Error while executing command "${command}": ${errorText}`); + return { error: `Error while executing command "${command}": ${error.message}`, result: false }; } return { error: `Unknown command "${command}"` }; @@ -265,7 +267,13 @@ class Controller implements GeneralNode { await this.#adapter.setState('controller.info.discovering', false, true); - await this.#commissioningController.start(); + try { + await this.#commissioningController.start(); + } catch (error) { + const errorText = inspect(error, { depth: 10 }); + this.#adapter.log.error(`Failed to start the controller: ${errorText}`); + return; + } // get nodes const nodes = this.#commissioningController.getCommissionedNodes(); @@ -369,37 +377,32 @@ class Controller implements GeneralNode { } // this.#adapter.log.debug(`Commissioning ... ${JSON.stringify(options)}`); - try { - if (passcode === undefined) { - throw new Error('Passcode is missing'); - } + if (passcode === undefined) { + throw new Error('Passcode is missing'); + } - const options: NodeCommissioningOptions = { - ...this.nodeConnectSettings, - commissioning: commissioningOptions, - discovery: { - commissionableDevice: device || undefined, - identifierData: - longDiscriminator !== undefined - ? { longDiscriminator } - : shortDiscriminator !== undefined - ? { shortDiscriminator } - : vendorId !== undefined - ? { vendorId, productId } - : undefined, - }, - passcode, - }; + const options: NodeCommissioningOptions = { + ...this.nodeConnectSettings, + commissioning: commissioningOptions, + discovery: { + commissionableDevice: device || undefined, + identifierData: + longDiscriminator !== undefined + ? { longDiscriminator } + : shortDiscriminator !== undefined + ? { shortDiscriminator } + : vendorId !== undefined + ? { vendorId, productId } + : undefined, + }, + passcode, + }; - const nodeId = await this.#commissioningController.commissionNode(options); + const nodeId = await this.#commissioningController.commissionNode(options); - await this.registerCommissionedNode(nodeId); + await this.registerCommissionedNode(nodeId); - return { result: true, nodeId: nodeId.toString() }; - } catch (e) { - this.#adapter.log.info(`Commissioning failed: ${e.stack}`); - return { error: e.message, result: false }; - } + return { result: true, nodeId: nodeId.toString() }; } async completeCommissioningForNode(nodeId: NodeId, discoveryData?: DiscoveryData): Promise { diff --git a/src/matter/DeviceNode.ts b/src/matter/DeviceNode.ts index 45306773..96f53844 100644 --- a/src/matter/DeviceNode.ts +++ b/src/matter/DeviceNode.ts @@ -85,26 +85,32 @@ class Device extends BaseServerNode { // The device type to announce we use from the first returned endpoint of the device const deviceType = endpoints[0].type.deviceType; - this.serverNode = await ServerNode.create({ - id: this.#parameters.uuid, - network: { - port: this.#parameters.port, - }, - productDescription: { - name: deviceName, - deviceType, - }, - basicInformation: { - vendorName, - vendorId: VendorId(vendorId), - nodeLabel: productName, - productName, - productLabel: productName, - productId, - serialNumber: uniqueId, - uniqueId: md5(uniqueId), - }, - }); + try { + this.serverNode = await ServerNode.create({ + id: this.#parameters.uuid, + network: { + port: this.#parameters.port, + }, + productDescription: { + name: deviceName, + deviceType, + }, + basicInformation: { + vendorName, + vendorId: VendorId(vendorId), + nodeLabel: productName, + productName, + productLabel: productName, + productId, + serialNumber: uniqueId, + uniqueId: md5(uniqueId), + }, + }); + } catch (error) { + const errorText = inspect(error, { depth: 10 }); + this.adapter.log.error(`Error creating device ${this.#parameters.uuid}: ${errorText}`); + return; + } if (this.#deviceOptions?.noComposed) { // No composed means we remove all beside first returned endpoint @@ -158,13 +164,24 @@ class Device extends BaseServerNode { if (!this.serverNode) { return; } - await this.serverNode.start(); - this.#started = true; + try { + await this.serverNode.start(); + this.#started = true; + } catch (error) { + const errorText = inspect(error, { depth: 10 }); + this.adapter.log.error(`Error starting device ${this.#parameters.uuid}: ${errorText}`); + return; + } await this.updateUiState(); } async stop(): Promise { - await this.serverNode?.close(); + try { + await this.serverNode?.close(); + } catch (error) { + const errorText = inspect(error, { depth: 10 }); + this.adapter.log.error(`Error stopping device ${this.#parameters.uuid}: ${errorText}`); + } await this.#device.destroy(); this.serverNode = undefined; this.#started = false; diff --git a/src/matter/GeneralMatterNode.ts b/src/matter/GeneralMatterNode.ts index d4772b83..96f3e6d7 100644 --- a/src/matter/GeneralMatterNode.ts +++ b/src/matter/GeneralMatterNode.ts @@ -277,6 +277,7 @@ export class GeneralMatterNode { options?: { exposeMatterSystemClusterData?: boolean; exposeMatterApplicationClusterData?: boolean; + connectionStateId?: string; }, ): Promise { const id = endpoint.number; @@ -314,6 +315,7 @@ export class GeneralMatterNode { let exposeMatterApplicationClusterData = customExposeMatterApplicationClusterData ?? this.exposeMatterApplicationClusterData; + let connectionStateId = options?.connectionStateId ?? `${this.adapter.namespace}.${this.connectionStateId}`; if (primaryDeviceType === undefined) { this.adapter.log.warn( `Node ${this.node.nodeId}: Unknown device type: ${serialize(endpoint.deviceType)}. Please report this issue.`, @@ -336,18 +338,33 @@ export class GeneralMatterNode { endpointId: id, }, }); + + if (primaryDeviceType.deviceType.name === 'BridgedNode') { + const ioBrokerDevice = await ioBrokerDeviceFabric( + this.node, + endpoint, + rootEndpoint, + this.adapter, + endpointDeviceBaseId, + connectionStateId, + ); + if (ioBrokerDevice !== null) { + connectionStateId = ioBrokerDevice.connectionStateId; + this.#deviceMap.set(id, ioBrokerDevice); + } + } + for (const childEndpoint of endpoint.getChildEndpoints()) { // Recursive call to process all sub endpoints for raw states - await this.#endpointToIoBrokerDevices(childEndpoint, rootEndpoint, endpointDeviceBaseId); + await this.#endpointToIoBrokerDevices(childEndpoint, rootEndpoint, endpointDeviceBaseId, { + connectionStateId, + }); } } else { await this.adapter.extendObjectAsync(endpointDeviceBaseId, { type: 'device', common: { name: existingObject ? undefined : deviceTypeName, - statusStates: { - onlineId: `${this.adapter.namespace}.${this.connectionStateId}`, - }, }, native: { nodeId: this.nodeId, @@ -358,11 +375,12 @@ export class GeneralMatterNode { if (id !== 0) { // Ignore the root endpoint const ioBrokerDevice = await ioBrokerDeviceFabric( - this.node.nodeId, + this.node, endpoint, rootEndpoint, this.adapter, endpointDeviceBaseId, + connectionStateId, ); if (ioBrokerDevice !== null) { this.#deviceMap.set(id, ioBrokerDevice); @@ -758,7 +776,7 @@ export class GeneralMatterNode { value, } = data; this.adapter.log.debug( - `attributeChangedCallback "${this.nodeId}": Attribute ${nodeId}/${endpointId}/${toHex(clusterId)}/${attributeName} changed to ${Logger.toJSON( + `handleChangedAttribute "${this.nodeId}": Attribute ${nodeId}/${endpointId}/${toHex(clusterId)}/${attributeName} changed to ${Logger.toJSON( value, )}`, ); @@ -800,7 +818,7 @@ export class GeneralMatterNode { events, } = data; this.adapter.log.debug( - `eventTriggeredCallback "${this.nodeId}": Event ${nodeId}/${endpointId}/${toHex(clusterId)}/${eventName} triggered with ${Logger.toJSON( + `handleTriggeredEvent "${this.nodeId}": Event ${nodeId}/${endpointId}/${toHex(clusterId)}/${eventName} triggered with ${Logger.toJSON( events, )}`, ); diff --git a/src/matter/to-iobroker/ContactSensorToIoBroker.ts b/src/matter/to-iobroker/ContactSensorToIoBroker.ts index fe11ff3d..5552f0bf 100644 --- a/src/matter/to-iobroker/ContactSensorToIoBroker.ts +++ b/src/matter/to-iobroker/ContactSensorToIoBroker.ts @@ -1,6 +1,6 @@ import ChannelDetector from '@iobroker/type-detector'; import { BooleanState } from '@matter/main/clusters'; -import type { Endpoint } from '@project-chip/matter.js/device'; +import type { Endpoint, PairedNode } from '@project-chip/matter.js/device'; import type { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { DetectedDevice, DeviceOptions } from '../../lib/devices/GenericDevice'; @@ -12,13 +12,15 @@ export class ContactSensorToIoBroker extends GenericDeviceToIoBroker { readonly #ioBrokerDevice: Window; // TODO That's a hack for now, could also be Door or Generic? constructor( + node: PairedNode, endpoint: Endpoint, rootEndpoint: Endpoint, adapter: ioBroker.Adapter, endpointDeviceBaseId: string, deviceTypeName: string, + defaultConnectionStateId: string, ) { - super(adapter, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName); + super(adapter, node, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName, defaultConnectionStateId); this.#ioBrokerDevice = new Window( { ...ChannelDetector.getPatterns().window, isIoBrokerDevice: false } as DetectedDevice, diff --git a/src/matter/to-iobroker/DimmableToIobroker.ts b/src/matter/to-iobroker/DimmableToIobroker.ts index eceaed69..a2394de9 100644 --- a/src/matter/to-iobroker/DimmableToIobroker.ts +++ b/src/matter/to-iobroker/DimmableToIobroker.ts @@ -1,6 +1,6 @@ import ChannelDetector from '@iobroker/type-detector'; import { LevelControl, OnOff } from '@matter/main/clusters'; -import type { Endpoint } from '@project-chip/matter.js/device'; +import type { Endpoint, PairedNode } from '@project-chip/matter.js/device'; import type { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import Dimmer from '../../lib/devices/Dimmer'; @@ -15,13 +15,15 @@ export class DimmableToIobroker extends GenericElectricityDataDeviceToIoBroker { #maxLevel = 254; constructor( + node: PairedNode, endpoint: Endpoint, rootEndpoint: Endpoint, adapter: ioBroker.Adapter, endpointDeviceBaseId: string, deviceTypeName: string, + defaultConnectionStateId: string, ) { - super(adapter, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName); + super(adapter, node, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName, defaultConnectionStateId); this.#ioBrokerDevice = new Dimmer( { ...ChannelDetector.getPatterns().dimmer, isIoBrokerDevice: false } as DetectedDevice, diff --git a/src/matter/to-iobroker/DoorLockToIoBroker.ts b/src/matter/to-iobroker/DoorLockToIoBroker.ts index d72d428d..8603ecdb 100644 --- a/src/matter/to-iobroker/DoorLockToIoBroker.ts +++ b/src/matter/to-iobroker/DoorLockToIoBroker.ts @@ -1,6 +1,6 @@ import ChannelDetector from '@iobroker/type-detector'; import { DoorLock } from '@matter/main/clusters'; -import type { Endpoint } from '@project-chip/matter.js/device'; +import type { Endpoint, PairedNode } from '@project-chip/matter.js/device'; import type { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { DetectedDevice, DeviceOptions } from '../../lib/devices/GenericDevice'; @@ -13,13 +13,15 @@ export class DoorLockToIoBroker extends GenericElectricityDataDeviceToIoBroker { readonly #unboltingSupported: boolean; constructor( + node: PairedNode, endpoint: Endpoint, rootEndpoint: Endpoint, adapter: ioBroker.Adapter, endpointDeviceBaseId: string, deviceTypeName: string, + defaultConnectionStateId: string, ) { - super(adapter, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName); + super(adapter, node, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName, defaultConnectionStateId); this.#unboltingSupported = this.appEndpoint.getClusterClient(DoorLock.Complete)?.supportedFeatures.unbolting ?? false; diff --git a/src/matter/to-iobroker/GenericDeviceToIoBroker.ts b/src/matter/to-iobroker/GenericDeviceToIoBroker.ts index 7633f258..680c5076 100644 --- a/src/matter/to-iobroker/GenericDeviceToIoBroker.ts +++ b/src/matter/to-iobroker/GenericDeviceToIoBroker.ts @@ -1,7 +1,7 @@ import { type AttributeId, type ClusterId, Diagnostic, EndpointNumber, type EventId } from '@matter/main'; -import { BasicInformation, Identify } from '@matter/main/clusters'; +import { BasicInformation, BridgedDeviceBasicInformation, Identify } from '@matter/main/clusters'; import type { DecodedEventData } from '@matter/main/protocol'; -import type { Endpoint } from '@project-chip/matter.js/device'; +import type { Endpoint, PairedNode, DeviceBasicInformation } from '@project-chip/matter.js/device'; import type { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { DeviceOptions } from '../../lib/devices/GenericDevice'; @@ -15,32 +15,49 @@ export type EnabledProperty = { attributeName?: string; convertValue?: (value: any) => any; changeHandler?: (value: any) => Promise; + pollAttribute?: boolean; +}; + +export type GenericDeviceConfiguration = { + pollInterval?: number; }; /** Base class to map an ioBroker device to a matter device. */ export abstract class GenericDeviceToIoBroker { readonly #adapter: ioBroker.Adapter; readonly baseId: string; + readonly #node: PairedNode; protected readonly appEndpoint: Endpoint; readonly #rootEndpoint: Endpoint; #name: string; readonly deviceType: string; readonly #deviceOptions: DeviceOptions; #enabledProperties = new Map(); + #connectionStateId: string; + #hasBridgedReachabilityAttribute = false; + #pollTimeout?: ioBroker.Timeout; + #destroyed = false; + #initialized = false; + #pollInterval = 60_000; + #hasAttributesToPoll = false; protected constructor( adapter: ioBroker.Adapter, + node: PairedNode, endpoint: Endpoint, rootEndpoint: Endpoint, endpointDeviceBaseId: string, deviceTypeName: string, + defaultConnectionStateId: string, ) { this.#adapter = adapter; + this.#node = node; this.appEndpoint = endpoint; this.#rootEndpoint = rootEndpoint; this.baseId = endpointDeviceBaseId; this.#name = deviceTypeName; this.deviceType = deviceTypeName; + this.#connectionStateId = defaultConnectionStateId; this.#deviceOptions = { additionalStateData: {}, @@ -65,6 +82,14 @@ export abstract class GenericDeviceToIoBroker { return this.appEndpoint.number!; } + get connectionStateId(): string { + return this.#connectionStateId; + } + + get nodeBasicInformation(): Partial { + return this.#node.basicInformation ?? {}; + } + /** * Method to override to add own states to the device. * This method is called by the constructor. @@ -76,6 +101,17 @@ export abstract class GenericDeviceToIoBroker { attributeName: 'reachable', convertValue: value => !value, }); + if (!this.#enabledProperties.has(PropertyType.Unreachable)) { + this.enableDeviceTypeState(PropertyType.Unreachable, { + endpointId: this.appEndpoint.number, + clusterId: BridgedDeviceBasicInformation.Cluster.id, + attributeName: 'reachable', + convertValue: value => !value, + }); + if (this.#enabledProperties.has(PropertyType.Unreachable)) { + this.#hasBridgedReachabilityAttribute = true; + } + } return this.#deviceOptions; } @@ -90,9 +126,10 @@ export abstract class GenericDeviceToIoBroker { clusterId?: ClusterId; convertValue?: (value: any) => any; changeHandler?: (value: any) => Promise; + pollAttribute?: boolean; } & ({ vendorSpecificAttributeId: AttributeId } | { attributeName?: string }), ): void { - const { endpointId, clusterId, convertValue, changeHandler } = data; + const { endpointId, clusterId, convertValue, changeHandler, pollAttribute } = data; const stateData = this.#deviceOptions.additionalStateData![type] ?? {}; if (stateData.id !== undefined) { console.log(`State ${type} already enabled`); @@ -125,6 +162,7 @@ export abstract class GenericDeviceToIoBroker { attributeName, convertValue, changeHandler, + pollAttribute, }); } @@ -161,6 +199,10 @@ export abstract class GenericDeviceToIoBroker { } if (value !== undefined) { await this.ioBrokerDevice.updatePropertyValue(property, value); + + if (property === PropertyType.Unreachable && this.#hasBridgedReachabilityAttribute) { + await this.#adapter.setStateAsync(this.#connectionStateId, { val: !value, ack: true }); + } } } @@ -194,14 +236,63 @@ export abstract class GenericDeviceToIoBroker { /** Initialization Logic for the device. makes sure all handlers are registered for both sides. */ async init(): Promise { - const obj = await this.#adapter.getObjectAsync(this.baseId); - if (obj && obj.common.name) { - this.#name = typeof obj.common.name === 'string' ? obj.common.name : obj.common.name.en; + const existingObject = await this.#adapter.getObjectAsync(this.baseId); + if (existingObject) { + if (existingObject.common.name) { + this.#name = + typeof existingObject.common.name === 'string' + ? existingObject.common.name + : existingObject.common.name.en; + } + this.setDeviceConfiguration({ + pollInterval: existingObject.native?.pollInterval, + }); } await this.ioBrokerDevice.init(); this.#registerIoBrokerHandlersAndInitialize(); await this.#initializeStates(); + + if (this.#hasBridgedReachabilityAttribute) { + await this.#adapter.setObjectNotExists(`${this.baseId}.info`, { + type: 'channel', + common: { + name: 'Bridged Device connection info', + }, + native: {}, + }); + + this.#connectionStateId = `${this.baseId}.info.connection`; + await this.#adapter.setObjectNotExists(`${this.#connectionStateId}`, { + type: 'state', + common: { + name: 'Connected', + role: 'indicator.connected', + type: 'boolean', + read: true, + write: false, + }, + native: {}, + }); + + await this.#adapter.setState(this.#connectionStateId, { + val: + (await this.appEndpoint + .getClusterClient(BridgedDeviceBasicInformation.Cluster) + ?.getReachableAttribute()) ?? false, + ack: true, + }); + } + + await this.#adapter.extendObjectAsync(this.baseId, { + common: { + statusStates: { + onlineId: `${this.#connectionStateId}`, + }, + }, + }); + + this.#initialized = true; } #registerIoBrokerHandlersAndInitialize(): void { @@ -229,9 +320,117 @@ export abstract class GenericDeviceToIoBroker { await this.updateIoBrokerState(property, value); } } + this.#initAttributePolling(); + } + + #initAttributePolling(): void { + if (this.#pollTimeout !== undefined) { + this.#adapter.clearTimeout(this.#pollTimeout); + this.#pollTimeout = undefined; + } + const pollingAttributes = new Array<{ + endpointId: EndpointNumber; + clusterId: ClusterId; + attributeId: AttributeId; + }>(); + for (const { endpointId, clusterId, attributeId, pollAttribute } of this.#enabledProperties.values()) { + if (pollAttribute) { + if (endpointId !== undefined && clusterId !== undefined && attributeId !== undefined) { + pollingAttributes.push({ endpointId, clusterId, attributeId }); + } + } + } + if (pollingAttributes.length) { + this.#hasAttributesToPoll = true; + this.#pollTimeout = this.#adapter.setTimeout( + () => this.#pollAttributes(pollingAttributes), + this.#pollInterval, + ); + } + } + + async #pollAttributes( + attributes: { + endpointId: EndpointNumber; + clusterId: ClusterId; + attributeId: AttributeId; + }[], + ): Promise { + this.#pollTimeout = undefined; + if (this.#destroyed || attributes.length === 0) { + return; + } + + if (this.#node.isConnected) { + // Split in chunks of maximum 9 attributes and get an interactionClient from node + const client = await this.#node.getInteractionClient(); + + for (let i = 0; i < attributes.length; i += 9) { + if (this.#destroyed) { + return; + } + // Maximum read for 9 attribute paths is allowed, so split in chunks of 9 + const chunk = attributes.slice(i, i + 9); + + // Collect the endpoints and clusters and get the last known data version for each to use as filter + const endpointClusters = new Map(); + for (const { endpointId, clusterId } of chunk) { + const key = `${endpointId}-${clusterId}`; + endpointClusters.set(key, { endpointId, clusterId }); + } + const dataVersionFilters = new Array<{ + endpointId: EndpointNumber; + clusterId: ClusterId; + dataVersion: number; + }>(); + for (const data of endpointClusters.values()) { + const filter = client.getCachedClusterDataVersions(data); + if (filter.length) { + dataVersionFilters.push(filter[0]); + } + } + + try { + // Query the attributes + const result = await client.getMultipleAttributes({ + attributes: chunk.map(({ endpointId, clusterId, attributeId }) => ({ + endpointId, + clusterId, + attributeId, + })), + dataVersionFilters, + }); + + // Handle the results as if they would have come as subscription update + for (const { + path: { endpointId, clusterId, attributeId, attributeName }, + value, + } of result) { + await this.handleChangedAttribute({ + clusterId, + endpointId, + attributeId, + attributeName, + value, + }); + } + } catch (e) { + this.#adapter.log.info(`Error polling attributes for node ${this.#node.nodeId}: ${e}`); + } + } + } else { + this.#adapter.log.debug(`Node ${this.#node.nodeId} is not connected, do not poll attributes`); + } + + this.#pollTimeout = this.#adapter.setTimeout(() => this.#pollAttributes(attributes), this.#pollInterval); } destroy(): Promise { + this.#destroyed = true; + if (this.#pollTimeout !== undefined) { + this.#adapter.clearTimeout(this.#pollTimeout); + this.#pollTimeout = undefined; + } return this.ioBrokerDevice.destroy(); } @@ -274,4 +473,27 @@ export abstract class GenericDeviceToIoBroker { return Promise.resolve(result); } + + get deviceConfiguration(): { pollInterval?: number } { + return { + pollInterval: this.#hasAttributesToPoll ? Math.round(this.#pollInterval / 1000) : undefined, + }; + } + + setDeviceConfiguration(config: GenericDeviceConfiguration): void { + const { pollInterval } = config; + if (pollInterval !== undefined) { + if (isNaN(pollInterval) || pollInterval < 30 || pollInterval > 2_147_482) { + this.#adapter.log.warn( + `Invalid polling interval ${pollInterval} seconds, use former value of ${Math.round(this.#pollInterval / 1000)}.`, + ); + return; + } + this.#pollInterval = pollInterval * 1000; + if (this.#initialized) { + // If already initialized, restart polling + this.#initAttributePolling(); + } + } + } } diff --git a/src/matter/to-iobroker/GenericElectricityDataDeviceToIoBroker.ts b/src/matter/to-iobroker/GenericElectricityDataDeviceToIoBroker.ts index 00bf4d09..e736983e 100644 --- a/src/matter/to-iobroker/GenericElectricityDataDeviceToIoBroker.ts +++ b/src/matter/to-iobroker/GenericElectricityDataDeviceToIoBroker.ts @@ -84,7 +84,8 @@ export abstract class GenericElectricityDataDeviceToIoBroker extends GenericDevi #enableCustomEveMeasurementStates(): boolean { const endpointId = this.appEndpoint.getNumber(); - // TODO Add polling when this is present nd with the Eve vendor id 4874 (0x130a) + // TODO Add polling when this is present and with the Eve vendor id 4874 (0x130a) + const isEveDevice = this.nodeBasicInformation.vendorId === 0x130a; // Only poll real Eve devices const clusterId = ClusterId(0x130afc01); const eveCluster = this.appEndpoint.getClusterClientById(clusterId); if (eveCluster !== undefined) { @@ -99,22 +100,26 @@ export abstract class GenericElectricityDataDeviceToIoBroker extends GenericDevi endpointId, clusterId, vendorSpecificAttributeId: AttributeId(0x130a000a), + pollAttribute: isEveDevice, }); this.enableDeviceTypeState(PropertyType.Consumption, { endpointId, clusterId, vendorSpecificAttributeId: AttributeId(0x130a000b), + pollAttribute: isEveDevice, }); this.enableDeviceTypeState(PropertyType.Current, { endpointId, clusterId, vendorSpecificAttributeId: AttributeId(0x130a0009), convertValue: value => value * 1000, // let's assume we have A? + pollAttribute: isEveDevice, }); this.enableDeviceTypeState(PropertyType.Voltage, { endpointId, clusterId, vendorSpecificAttributeId: AttributeId(0x130a0008), + pollAttribute: isEveDevice, }); return true; } @@ -131,22 +136,26 @@ export abstract class GenericElectricityDataDeviceToIoBroker extends GenericDevi endpointId, clusterId, vendorSpecificAttributeId: AttributeId(0x00125d0023), + pollAttribute: true, }); // Watt as Float this.enableDeviceTypeState(PropertyType.Consumption, { endpointId, clusterId, vendorSpecificAttributeId: AttributeId(0x00125d0021), + pollAttribute: true, }); // Accumulated Watt as Float this.enableDeviceTypeState(PropertyType.Current, { endpointId, clusterId, vendorSpecificAttributeId: AttributeId(0x00125d0022), convertValue: value => value * 1000, // let's assume we have A? + pollAttribute: true, }); // Current as float 32 this.enableDeviceTypeState(PropertyType.Voltage, { endpointId, clusterId, vendorSpecificAttributeId: AttributeId(0x00125d0024), + pollAttribute: true, }); // Voltage as float 32 return true; } diff --git a/src/matter/to-iobroker/HumiditySensorToIoBroker.ts b/src/matter/to-iobroker/HumiditySensorToIoBroker.ts index 125fbd03..a2495d49 100644 --- a/src/matter/to-iobroker/HumiditySensorToIoBroker.ts +++ b/src/matter/to-iobroker/HumiditySensorToIoBroker.ts @@ -1,6 +1,6 @@ import ChannelDetector from '@iobroker/type-detector'; import { RelativeHumidityMeasurement } from '@matter/main/clusters'; -import type { Endpoint } from '@project-chip/matter.js/device'; +import type { Endpoint, PairedNode } from '@project-chip/matter.js/device'; import type { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { DetectedDevice, DeviceOptions } from '../../lib/devices/GenericDevice'; @@ -12,13 +12,15 @@ export class HumiditySensorToIoBroker extends GenericElectricityDataDeviceToIoBr readonly #ioBrokerDevice: Humidity; constructor( + node: PairedNode, endpoint: Endpoint, rootEndpoint: Endpoint, adapter: ioBroker.Adapter, endpointDeviceBaseId: string, deviceTypeName: string, + defaultConnectionStateId: string, ) { - super(adapter, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName); + super(adapter, node, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName, defaultConnectionStateId); this.#ioBrokerDevice = new Humidity( { ...ChannelDetector.getPatterns().humidity, isIoBrokerDevice: false } as DetectedDevice, diff --git a/src/matter/to-iobroker/OccupancyToIoBroker.ts b/src/matter/to-iobroker/OccupancyToIoBroker.ts index 9d84bb28..e2c8394c 100644 --- a/src/matter/to-iobroker/OccupancyToIoBroker.ts +++ b/src/matter/to-iobroker/OccupancyToIoBroker.ts @@ -1,7 +1,7 @@ import ChannelDetector from '@iobroker/type-detector'; import { OccupancySensing } from '@matter/main/clusters'; import type { TypeFromBitSchema } from '@matter/main/types'; -import type { Endpoint } from '@project-chip/matter.js/device'; +import type { Endpoint, PairedNode } from '@project-chip/matter.js/device'; import type { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { DetectedDevice, DeviceOptions } from '../../lib/devices/GenericDevice'; @@ -13,13 +13,15 @@ export class OccupancyToIoBroker extends GenericDeviceToIoBroker { readonly #ioBrokerDevice: Motion; constructor( + node: PairedNode, endpoint: Endpoint, rootEndpoint: Endpoint, adapter: ioBroker.Adapter, endpointDeviceBaseId: string, deviceTypeName: string, + defaultConnectionStateId: string, ) { - super(adapter, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName); + super(adapter, node, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName, defaultConnectionStateId); this.#ioBrokerDevice = new Motion( { ...ChannelDetector.getPatterns().motion, isIoBrokerDevice: false } as DetectedDevice, diff --git a/src/matter/to-iobroker/OnOffLightToIoBroker.ts b/src/matter/to-iobroker/OnOffLightToIoBroker.ts index bdc6b687..83bc4a1a 100644 --- a/src/matter/to-iobroker/OnOffLightToIoBroker.ts +++ b/src/matter/to-iobroker/OnOffLightToIoBroker.ts @@ -1,6 +1,6 @@ import ChannelDetector from '@iobroker/type-detector'; import { OnOff } from '@matter/main/clusters'; -import type { Endpoint } from '@project-chip/matter.js/device'; +import type { Endpoint, PairedNode } from '@project-chip/matter.js/device'; import type { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { DetectedDevice, DeviceOptions } from '../../lib/devices/GenericDevice'; @@ -12,13 +12,15 @@ export class OnOffLightToIoBroker extends GenericElectricityDataDeviceToIoBroker readonly #ioBrokerDevice: Light; constructor( + node: PairedNode, endpoint: Endpoint, rootEndpoint: Endpoint, adapter: ioBroker.Adapter, endpointDeviceBaseId: string, deviceTypeName: string, + defaultConnectionStateId: string, ) { - super(adapter, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName); + super(adapter, node, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName, defaultConnectionStateId); this.#ioBrokerDevice = new Light( { ...ChannelDetector.getPatterns().light, isIoBrokerDevice: false } as DetectedDevice, diff --git a/src/matter/to-iobroker/OnOffPlugInUnitToIoBroker.ts b/src/matter/to-iobroker/OnOffPlugInUnitToIoBroker.ts index 0f9045cf..5f3a46f8 100644 --- a/src/matter/to-iobroker/OnOffPlugInUnitToIoBroker.ts +++ b/src/matter/to-iobroker/OnOffPlugInUnitToIoBroker.ts @@ -1,6 +1,6 @@ import ChannelDetector from '@iobroker/type-detector'; import { OnOff } from '@matter/main/clusters'; -import type { Endpoint } from '@project-chip/matter.js/device'; +import type { Endpoint, PairedNode } from '@project-chip/matter.js/device'; import type { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { DetectedDevice, DeviceOptions } from '../../lib/devices/GenericDevice'; @@ -12,13 +12,15 @@ export class OnOffPlugInUnitToIoBroker extends GenericElectricityDataDeviceToIoB readonly #ioBrokerDevice: Socket; constructor( + node: PairedNode, endpoint: Endpoint, rootEndpoint: Endpoint, adapter: ioBroker.Adapter, endpointDeviceBaseId: string, deviceTypeName: string, + defaultConnectionStateId: string, ) { - super(adapter, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName); + super(adapter, node, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName, defaultConnectionStateId); this.#ioBrokerDevice = new Socket( { ...ChannelDetector.getPatterns().socket, isIoBrokerDevice: false } as DetectedDevice, diff --git a/src/matter/to-iobroker/TemperatureSensorToIoBroker.ts b/src/matter/to-iobroker/TemperatureSensorToIoBroker.ts index 323bad37..d3baf34d 100644 --- a/src/matter/to-iobroker/TemperatureSensorToIoBroker.ts +++ b/src/matter/to-iobroker/TemperatureSensorToIoBroker.ts @@ -1,6 +1,6 @@ import ChannelDetector from '@iobroker/type-detector'; import { TemperatureMeasurement } from '@matter/main/clusters'; -import type { Endpoint } from '@project-chip/matter.js/device'; +import type { Endpoint, PairedNode } from '@project-chip/matter.js/device'; import type { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { DetectedDevice, DeviceOptions } from '../../lib/devices/GenericDevice'; @@ -12,13 +12,15 @@ export class TemperatureSensorToIoBroker extends GenericElectricityDataDeviceToI readonly #ioBrokerDevice: Temperature; constructor( + node: PairedNode, endpoint: Endpoint, rootEndpoint: Endpoint, adapter: ioBroker.Adapter, endpointDeviceBaseId: string, deviceTypeName: string, + defaultConnectionStateId: string, ) { - super(adapter, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName); + super(adapter, node, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName, defaultConnectionStateId); this.#ioBrokerDevice = new Temperature( { ...ChannelDetector.getPatterns().temperature, isIoBrokerDevice: false } as DetectedDevice, diff --git a/src/matter/to-iobroker/UtilityOnlyToIoBroker.ts b/src/matter/to-iobroker/UtilityOnlyToIoBroker.ts index d07b6ba4..ec429d5c 100644 --- a/src/matter/to-iobroker/UtilityOnlyToIoBroker.ts +++ b/src/matter/to-iobroker/UtilityOnlyToIoBroker.ts @@ -1,5 +1,5 @@ import ChannelDetector from '@iobroker/type-detector'; -import type { Endpoint } from '@project-chip/matter.js/device'; +import type { Endpoint, PairedNode } from '@project-chip/matter.js/device'; import type { GenericDevice } from '../../lib'; import type ElectricityDataDevice from '../../lib/devices/ElectricityDataDevice'; import type { DetectedDevice } from '../../lib/devices/GenericDevice'; @@ -11,13 +11,15 @@ export class UtilityOnlyToIoBroker extends GenericElectricityDataDeviceToIoBroke readonly #ioBrokerDevice: ElectricityDataDevice; constructor( + node: PairedNode, endpoint: Endpoint, rootEndpoint: Endpoint, adapter: ioBroker.Adapter, endpointDeviceBaseId: string, deviceTypeName: string, + defaultConnectionStateId: string, ) { - super(adapter, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName); + super(adapter, node, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName, defaultConnectionStateId); this.#ioBrokerDevice = new Socket( // TODO: Change to something generic like ElectricityDataDevice that we need to define first diff --git a/src/matter/to-iobroker/WaterLeakDetectorToIoBroker.ts b/src/matter/to-iobroker/WaterLeakDetectorToIoBroker.ts index de8a545d..26f2a7c5 100644 --- a/src/matter/to-iobroker/WaterLeakDetectorToIoBroker.ts +++ b/src/matter/to-iobroker/WaterLeakDetectorToIoBroker.ts @@ -1,6 +1,6 @@ import ChannelDetector from '@iobroker/type-detector'; import { BooleanState } from '@matter/main/clusters'; -import type { Endpoint } from '@project-chip/matter.js/device'; +import type { Endpoint, PairedNode } from '@project-chip/matter.js/device'; import type { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import FloodAlarm from '../../lib/devices/FloodAlarm'; @@ -12,13 +12,15 @@ export class WaterLeakDetectorToIoBroker extends GenericDeviceToIoBroker { readonly #ioBrokerDevice: FloodAlarm; constructor( + node: PairedNode, endpoint: Endpoint, rootEndpoint: Endpoint, adapter: ioBroker.Adapter, endpointDeviceBaseId: string, deviceTypeName: string, + defaultConnectionStateId: string, ) { - super(adapter, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName); + super(adapter, node, endpoint, rootEndpoint, endpointDeviceBaseId, deviceTypeName, defaultConnectionStateId); this.#ioBrokerDevice = new FloodAlarm( { ...ChannelDetector.getPatterns().floodAlarm, isIoBrokerDevice: false } as DetectedDevice, diff --git a/src/matter/to-iobroker/ioBrokerFactory.ts b/src/matter/to-iobroker/ioBrokerFactory.ts index 13126efb..37c3eb85 100644 --- a/src/matter/to-iobroker/ioBrokerFactory.ts +++ b/src/matter/to-iobroker/ioBrokerFactory.ts @@ -1,6 +1,5 @@ -import type { NodeId } from '@matter/main'; import { DeviceClassification, DeviceTypeModel, MatterModel } from '@matter/main/model'; -import type { Endpoint } from '@project-chip/matter.js/device'; +import type { Endpoint, PairedNode } from '@project-chip/matter.js/device'; import { ContactSensorToIoBroker } from './ContactSensorToIoBroker'; import { DimmableToIobroker } from './DimmableToIobroker'; import { DoorLockToIoBroker } from './DoorLockToIoBroker'; @@ -43,116 +42,148 @@ export function identifyDeviceTypes(endpoint: Endpoint): { * Factory function to create an ioBroker device from a Matter device type. */ async function ioBrokerDeviceFabric( - nodeId: NodeId, + node: PairedNode, endpoint: Endpoint, rootEndpoint: Endpoint, adapter: ioBroker.Adapter, endpointDeviceBaseId: string, + defaultConnectionStateId: string, ): Promise { - const { primaryDeviceType } = identifyDeviceTypes(endpoint); + const { primaryDeviceType, utilityTypes } = identifyDeviceTypes(endpoint); const fullEndpointDeviceBaseId = `${adapter.namespace}.${endpointDeviceBaseId}`; const mainDeviceTypeName = primaryDeviceType?.deviceType.name ?? 'Unknown'; - adapter.log.info(`Node ${nodeId}: Creating device for ${mainDeviceTypeName}`); + adapter.log.info(`Node ${node.nodeId}: Creating device for ${mainDeviceTypeName}`); let device: GenericDeviceToIoBroker; switch (mainDeviceTypeName) { case 'DimmablePlugInUnit': case 'DimmableLight': device = new DimmableToIobroker( + node, endpoint, rootEndpoint, adapter, fullEndpointDeviceBaseId, mainDeviceTypeName, + defaultConnectionStateId, ); break; case 'ContactSensor': device = new ContactSensorToIoBroker( + node, endpoint, rootEndpoint, adapter, fullEndpointDeviceBaseId, mainDeviceTypeName, + defaultConnectionStateId, ); break; case 'DoorLock': device = new DoorLockToIoBroker( + node, endpoint, rootEndpoint, adapter, fullEndpointDeviceBaseId, mainDeviceTypeName, + defaultConnectionStateId, ); break; case 'ElectricalSensor': case 'PowerSource': device = new UtilityOnlyToIoBroker( + node, endpoint, rootEndpoint, adapter, fullEndpointDeviceBaseId, mainDeviceTypeName, + defaultConnectionStateId, ); break; case 'HumiditySensor': device = new HumiditySensorToIoBroker( + node, endpoint, rootEndpoint, adapter, fullEndpointDeviceBaseId, mainDeviceTypeName, + defaultConnectionStateId, ); break; case 'OccupancySensor': device = new OccupancyToIoBroker( + node, endpoint, rootEndpoint, adapter, fullEndpointDeviceBaseId, mainDeviceTypeName, + defaultConnectionStateId, ); break; case 'OnOffLight': device = new OnOffLightToIoBroker( + node, endpoint, rootEndpoint, adapter, fullEndpointDeviceBaseId, mainDeviceTypeName, + defaultConnectionStateId, ); break; case 'OnOffPlugInUnit': device = new OnOffPlugInUnitToIoBroker( + node, endpoint, rootEndpoint, adapter, fullEndpointDeviceBaseId, mainDeviceTypeName, + defaultConnectionStateId, ); break; case 'TemperatureSensor': device = new TemperatureSensorToIoBroker( + node, endpoint, rootEndpoint, adapter, fullEndpointDeviceBaseId, mainDeviceTypeName, + defaultConnectionStateId, ); break; case 'WaterLeakDetector': device = new WaterLeakDetectorToIoBroker( + node, endpoint, rootEndpoint, adapter, fullEndpointDeviceBaseId, mainDeviceTypeName, + defaultConnectionStateId, ); break; default: - adapter.log.info( - `Node ${nodeId}: Unknown device type: ${mainDeviceTypeName}. We enabled exposing of the application clusters for this node if you need this device type.`, + if (utilityTypes.length === 0) { + adapter.log.info( + `Node ${node.nodeId}: Unknown device type: ${mainDeviceTypeName}. We enabled exposing of the application clusters for this node if you need this device type.`, + ); + } + // ... but device has a utility type, so we can expose it + device = new UtilityOnlyToIoBroker( + node, + endpoint, + rootEndpoint, + adapter, + fullEndpointDeviceBaseId, + mainDeviceTypeName, + defaultConnectionStateId, ); - return null; } await device.init(); return device;