import { actuatorCCs , CommandClasses , CommandClassInfo , GraphNode , ZWaveError , ZWaveErrorCodes , } from "@zwave-js/core" ; import { num2hex } from "@zwave-js/shared" ; import type { MultiChannelAssociationCC } from "../commandclass" ; import type { APIMethodsOf , CCAPI , CCAPIs , CCToAPI } from "../commandclass/API" ; import { AssociationCC , getHasLifelineValueId , getLifelineGroupIds , } from "../commandclass/AssociationCC" ; import { CommandClass , Constructable , getAPI , getCCConstructor , getCommandClassStatic , } from "../commandclass/CommandClass" ; import { AssociationAddress , EndpointAddress , getEndpointsValueId , getNodeIdsValueId , } from "../commandclass/MultiChannelAssociationCC" ; import { getInstallerIconValueId , getUserIconValueId , } from "../commandclass/ZWavePlusCC" ; import type { Driver } from "../driver/Driver" ; import type { DeviceClass } from "./DeviceClass" ; import type { ZWaveNode } from "./Node" ; /**
 * Represents a physical endpoint of a Z-Wave node. This can either be the root
 * device itself (index 0) or a more specific endpoint like a single plug.
 *
 * Each endpoint may have different capabilities (device class/supported CCs)
 */ export class Endpoint { public constructor ( /** The id of the node this endpoint belongs to */ public readonly nodeId , /** The driver instance this endpoint belongs to */ protected readonly driver , /** The index of this endpoint. 0 for the root device, 1+ otherwise */ public readonly index , deviceClass ? , supportedCCs ? ) { this . applyDeviceClass ( deviceClass ) ; if ( supportedCCs != undefined ) { for ( const cc of supportedCCs ) { this . addCC ( cc , { isSupported : true } ) ; } } } protected _deviceClass : DeviceClass | undefined ; public get deviceClass ( ) : DeviceClass | undefined { return this . _deviceClass ; } /** Resets all stored information of this endpoint */ protected reset ( ) { this . _implementedCommandClasses . clear ( ) ; this . _commandClassAPIs . clear ( ) ; } private _implementedCommandClasses = new Map < CommandClasses , CommandClassInfo > ( ) ; internal public get implementedCommandClasses ( ) : ReadonlyMap < CommandClasses , CommandClassInfo > { return this . _implementedCommandClasses ; } /**
     * Sets the device class of this endpoint and configures the mandatory CCs.
     * **Note:** This does nothing if the device class was already configured
     */ protected applyDeviceClass ( deviceClass ? ) { if ( this . _deviceClass ) return ; this . _deviceClass = deviceClass ; if ( deviceClass ) { for ( const cc of deviceClass . mandatorySupportedCCs ) { this . addMandatoryCC ( cc , { isSupported : true } ) ; } for ( const cc of deviceClass . mandatoryControlledCCs ) { this . addMandatoryCC ( cc , { isControlled : true } ) ; } } } param info public addCC ( cc , info ) { if ( this . index > 0 && cc === CommandClasses [ "Multi Channel" ] ) return ; let ccInfo = this . _implementedCommandClasses . get ( cc ) ?? { isSupported : false , isControlled : false , secure : false , version : 0 } ; ccInfo = Object . assign ( ccInfo , info ) ; this . _implementedCommandClasses . set ( cc , ccInfo ) ; } /**
     * Adds a mandatory CC to the list of command classes implemented by the endpoint or updates the information.
     * Performs some sanity checks before adding so the behavior is in compliance with the specifications
     */ protected addMandatoryCC ( cc , info ) { if ( this . getNodeUnsafe ( ) ?. isListening && ( cc === CommandClasses . Battery || cc === CommandClasses [ "Wake Up" ] ) ) { return ; } else if ( this . index > 0 && [ CommandClasses [ "CRC-16 Encapsulation" ] , CommandClasses [ "Device Reset Locally" ] , CommandClasses [ "Manufacturer Specific" ] , CommandClasses . Powerlevel , CommandClasses . Version , CommandClasses [ "Transport Service" ] , ] . includes ( cc ) ) { return ; } this . addCC ( cc , info ) ; } /** Removes a CC from the list of command classes implemented by the endpoint */ public removeCC ( cc ) { this . _implementedCommandClasses . delete ( cc ) ; } /** Tests if this endpoint supports the given CommandClass */ public supportsCC ( cc ) { return ! ! this . _implementedCommandClasses . get ( cc ) ?. isSupported ; } /** Tests if this endpoint supports or controls the given CC only securely */ public isCCSecure ( cc ) { return ! ! this . _implementedCommandClasses . get ( cc ) ?. secure ; } /** Tests if this endpoint controls the given CommandClass */ public controlsCC ( cc ) { return ! ! this . _implementedCommandClasses . get ( cc ) ?. isControlled ; } /** Removes the BasicCC from the supported CCs if any other actuator CCs are supported */ public hideBasicCCInFavorOfActuatorCCs ( ) { if ( this . supportsCC ( CommandClasses . Basic ) && actuatorCCs . some ( ( cc ) => this . supportsCC ( cc ) ) ) { this . addCC ( CommandClasses . Basic , { isSupported : false } ) ; if ( ! this . supportsCC ( CommandClasses . Basic ) && ! this . controlsCC ( CommandClasses . Basic ) ) { this . removeCC ( CommandClasses . Basic ) ; } } } /**
     * Retrieves the version of the given CommandClass this endpoint implements.
     * Returns 0 if the CC is not supported.
     */ public getCCVersion ( cc ) { const ccInfo = this . _implementedCommandClasses . get ( cc ) ; const ret = ccInfo ?. version ?? 0 ; if ( ret === 0 && this . index > 0 ) { return this . getNodeUnsafe ( ) ! . getCCVersion ( cc ) ; } return ret ; } /**
     * Creates an instance of the given CC and links it to this endpoint.
     * Throws if the CC is neither supported nor controlled by the endpoint.
     */ public createCCInstance < T extends CommandClass > ( cc ) { const ccId = typeof cc === "number" ? cc : getCommandClassStatic ( cc ) ; if ( ! this . supportsCC ( ccId ) && ! this . controlsCC ( ccId ) ) { throw new ZWaveError ( `Cannot create an instance of the unsupported CC ${ CommandClasses [ ccId ] } (${ num2hex ( ccId ) })` , ZWaveErrorCodes . CC_NotSupported ) ; } return this . createCCInstanceInternal ( cc ) ; } /**
     * Creates an instance of the given CC and links it to this endpoint.
     * Returns undefined if the CC is neither supported nor controlled by the endpoint.
     */ public createCCInstanceUnsafe < T extends CommandClass > ( cc ) { const ccId = typeof cc === "number" ? cc : getCommandClassStatic ( cc ) ; if ( this . supportsCC ( ccId ) || this . controlsCC ( ccId ) ) { return this . createCCInstanceInternal ( cc ) ; } } internal public createCCInstanceInternal < T extends CommandClass > ( cc ) { const Constructor = typeof cc === "number" ? getCCConstructor ( cc ) : cc ; if ( Constructor ) { return new Constructor ( this . driver , { nodeId : this . nodeId , endpoint : this . index } ) as T ; } } /** Returns instances for all CCs this endpoint supports, that should be interviewed, and that are implemented in this library */ public getSupportedCCInstances ( ) { let supportedCCInstances = [ ... this . implementedCommandClasses . keys ( ) ] . filter ( ( cc ) => this . supportsCC ( cc ) ) . map ( ( cc ) => this . createCCInstance ( cc ) ) . filter ( ( instance ) => ! ! instance ) as CommandClass [ ] ; if ( this . index > 0 ) { supportedCCInstances = supportedCCInstances . filter ( ( instance ) => ! instance . skipEndpointInterview ( ) ) ; } return supportedCCInstances ; } /** Builds the dependency graph used to automatically determine the order of CC interviews */ public buildCCInterviewGraph ( skipCCs ) { const supportedCCs = this . getSupportedCCInstances ( ) . map ( ( instance ) => instance . ccId ) . filter ( ( ccId ) => ! skipCCs . includes ( ccId ) ) ; const ret = supportedCCs . map ( ( cc ) => new GraphNode ( cc ) ) ; for ( const node of ret ) { const instance = this . createCCInstance ( node . value ) ! ; for ( const requiredCCId of instance . determineRequiredCCInterviews ( ) ) { const requiredCC = ret . find ( ( instance ) => instance . value === requiredCCId ) ; if ( requiredCC ) node . edges . add ( requiredCC ) ; } } return ret ; } internal param ccId param requireSupport public createAPI < T extends CommandClasses > ( ccId , requireSupport = true ) { const APIConstructor = getAPI ( ccId ) ; const ccName = CommandClasses [ ccId ] ; if ( APIConstructor == undefined ) { throw new ZWaveError ( `Command Class ${ ccName } (${ num2hex ( ccId ) }) has no associated API!` , ZWaveErrorCodes . CC_NoAPI ) ; } const apiInstance = new APIConstructor ( this . driver , this ) ; if ( requireSupport ) { return new Proxy ( apiInstance , { get : ( target , property ) => { if ( property !== "ccId" && property !== "endpoint" && property !== "isSupported" && property !== "withOptions" && property !== "commandOptions" && ! target . isSupported ( ) ) { throw new ZWaveError ( `Node ${ this . nodeId }${ this . index === 0 ? "" : ` (endpoint ${ this . index })` } does not support the Command Class ${ ccName }!` , ZWaveErrorCodes . CC_NotSupported ) ; } return target [ property as keyof CCAPI ] ; } } ) ; } else { return apiInstance ; } } private _commandClassAPIs = new Map < CommandClasses , CCAPI > ( ) ; private _commandClassAPIsProxy = new Proxy ( this . _commandClassAPIs , { get : ( target , ccNameOrId ) => { if ( process . env . NODE_ENV === "test" && typeof ccNameOrId === "string" && ( ccNameOrId === "$$typeof" || ccNameOrId === "constructor" || ccNameOrId . includes ( "@@__IMMUTABLE" ) ) ) { return undefined ; } if ( typeof ccNameOrId === "symbol" ) { if ( ccNameOrId === Symbol . iterator ) { return this . commandClassesIterator ; } else if ( ccNameOrId === Symbol . toStringTag ) { return "[object Object]" ; } return undefined ; } else { let ccId ; if ( /^\d+$/ . test ( ccNameOrId ) ) { ccId = + ccNameOrId ; } else { ccId = CommandClasses [ ccNameOrId as any ] as unknown as CommandClasses | undefined ; if ( ccId == undefined ) { throw new ZWaveError ( `Command Class ${ ccNameOrId } is not implemented! If you are sure that the name/id is correct, consider opening an issue at https://github.com/AlCalzone/node-zwave-js` , ZWaveErrorCodes . CC_NotImplemented ) ; } } if ( ! target . has ( ccId ) ) { const api = this . createAPI ( ccId ) ; target . set ( ccId , api ) ; } return target . get ( ccId ) ; } } } ) ; /**
     * Used to iterate over the commandClasses API without throwing errors by accessing unsupported CCs
     */ private readonly commandClassesIterator : ( ) => Iterator < CCAPI > = function * ( this ) { for ( const cc of this . implementedCommandClasses . keys ( ) ) { if ( this . supportsCC ( cc ) ) yield ( this . commandClasses as any ) [ cc ] ; } } . bind ( this ) ; /**
     * Provides access to simplified APIs that are taylored to specific CCs.
     * Make sure to check support of each API using `API.isSupported()` since
     * all other API calls will throw if the API is not supported
     */ public get commandClasses ( ) : CCAPIs { return this . _commandClassAPIsProxy as unknown as CCAPIs ; } /** Allows checking whether a CC API is supported before calling it with  Endpoint . invokeCCAPI public supportsCCAPI ( cc ) { return ( ( this . commandClasses as any ) [ cc ] as CCAPI ) . isSupported ( ) ; } /**
     * Allows dynamically calling any CC API method on this endpoint by CC ID and method name.
     * Use  Endpoint . supportsCCAPI to check support first.
      public invokeCCAPI < CC extends CommandClasses , TMethod extends keyof TAPI , TAPI extends Record < string , ( ... args : any [ ] ) => any > = CommandClasses extends CC ? any : APIMethodsOf < CC > > ( cc , method , ... args ) { const CCAPI = ( this . commandClasses as any ) [ cc ] ; return CCAPI [ method ] ( ... args ) ; } /**
     * Returns the node this endpoint belongs to (or undefined if the node doesn't exist)
     */ public getNodeUnsafe ( ) { return this . driver . controller . nodes . get ( this . nodeId ) ; } /** Z-Wave+ Icon (for management) */ public get installerIcon ( ) : number | undefined { return this . getNodeUnsafe ( ) ?. getValue ( getInstallerIconValueId ( this . index ) ) ; } /** Z-Wave+ Icon (for end users) */ public get userIcon ( ) : number | undefined { return this . getNodeUnsafe ( ) ?. getValue ( getUserIconValueId ( this . index ) ) ; } internal public async configureLifelineAssociations ( ) { const ownNodeId = this . driver . controller . ownNodeId ! ; const node = this . getNodeUnsafe ( ) ! ; const valueDB = node . valueDB ; const nodeSupportsMultiChannel = node . supportsCC ( CommandClasses [ "Multi Channel" ] ) ; let assocInstance ; const assocAPI = this . commandClasses . Association ; if ( this . supportsCC ( CommandClasses . Association ) ) { assocInstance = this . createCCInstanceUnsafe ( CommandClasses . Association ) ; } let mcInstance ; let mcGroupCount = 0 ; const mcAPI = this . commandClasses [ "Multi Channel Association" ] ; if ( this . supportsCC ( CommandClasses [ "Multi Channel Association" ] ) ) { mcInstance = this . createCCInstanceUnsafe ( CommandClasses [ "Multi Channel Association" ] ) ; mcGroupCount = mcInstance ?. getGroupCountCached ( ) ?? 0 ; } const lifelineGroups = getLifelineGroupIds ( node ) ; if ( lifelineGroups . length === 0 ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : "No information about Lifeline associations, cannot assign ourselves!" , level : "warn" } ) ; valueDB . setValue ( getHasLifelineValueId ( this . index ) , false ) ; return ; } this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Checking/assigning lifeline groups: ${ lifelineGroups . join ( ", " ) }
supports classic associations:       ${ ! ! assocInstance }
supports multi channel associations: ${ ! ! mcInstance }` } ) ; for ( const group of lifelineGroups ) { const groupSupportsMultiChannelAssociation = group <= mcGroupCount ; const assocConfig = node . deviceConfig ?. getAssociationConfigForEndpoint ( this . index , group ) ; const mustUseNodeAssociation = ! groupSupportsMultiChannelAssociation || ! nodeSupportsMultiChannel || assocConfig ?. multiChannel === false ; let mustUseMultiChannelAssociation = false ; if ( groupSupportsMultiChannelAssociation && nodeSupportsMultiChannel ) { if ( assocConfig ?. multiChannel === true ) { mustUseMultiChannelAssociation = true ; } else if ( this . index === 0 ) { const allEndpoints = node . getAllEndpoints ( ) ; if ( allEndpoints . length > 1 && allEndpoints . filter ( ( e ) => e . index !== this . index ) . every ( ( e ) => ! e . supportsCC ( CommandClasses . Association ) && ! e . supportsCC ( CommandClasses [ "Multi Channel Association" ] ) ) ) { mustUseMultiChannelAssociation = true ; } } } this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Configuring lifeline group #${ group }:
group supports multi channel:  ${ groupSupportsMultiChannelAssociation }
configured strategy:           ${ assocConfig ?. multiChannel ?? "auto" }
must use node association:     ${ mustUseNodeAssociation }
must use endpoint association: ${ mustUseMultiChannelAssociation }` } ) ; const isAssignedAsNodeAssociation = ( ) : boolean => { if ( groupSupportsMultiChannelAssociation && mcInstance ) { if ( mcInstance . getMaxNodesCached ( group ) > 0 && ! ! mcInstance . getAllDestinationsCached ( ) . get ( group ) ?. some ( ( addr ) => addr . nodeId === ownNodeId && addr . endpoint == undefined ) ) { return true ; } } if ( assocInstance ) { if ( assocInstance . getMaxNodesCached ( group ) > 0 && ! ! assocInstance . getAllDestinationsCached ( ) . get ( group ) ?. some ( ( addr ) => addr . nodeId === ownNodeId ) ) { return true ; } } return false ; } ; const isAssignedAsEndpointAssociation = ( ) : boolean => { if ( mcInstance ) { if ( mcInstance . getMaxNodesCached ( group ) > 0 && mcInstance . getAllDestinationsCached ( ) . get ( group ) ?. some ( ( addr ) => addr . nodeId === ownNodeId && addr . endpoint === 0 ) ) { return true ; } } return false ; } ; const invalidEndpointAssociations = mcInstance ?. getAllDestinationsCached ( ) . get ( group ) ?. filter ( ( addr ) : addr is AssociationAddress & EndpointAddress => addr . nodeId === ownNodeId && addr . endpoint != undefined && addr . endpoint !== 0 ) ?? [ ] ; if ( invalidEndpointAssociations . length > 0 && mcAPI . isSupported ( ) && groupSupportsMultiChannelAssociation ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Found invalid lifeline associations in group #${ group }, removing them...` , direction : "outbound" } ) ; await mcAPI . removeDestinations ( { groupId : group , endpoints : invalidEndpointAssociations } ) ; await mcAPI . getGroup ( group ) ; } let hasLifeline = false ; if ( ! mustUseMultiChannelAssociation ) { if ( isAssignedAsNodeAssociation ( ) ) { hasLifeline = true ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Lifeline group #${ group } is already assigned with a node association` , direction : "none" } ) ; } else if ( assocAPI . isSupported ( ) && assocInstance ! . getMaxNodesCached ( group ) > 0 ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Assigning lifeline group #${ group } with a node association via Association CC...` , direction : "outbound" } ) ; if ( isAssignedAsEndpointAssociation ( ) && mcAPI . isSupported ( ) ) { await mcAPI . removeDestinations ( { groupId : group , endpoints : [ { nodeId : ownNodeId , endpoint : 0 } ] } ) ; await mcAPI . getGroup ( group ) ; } await assocAPI . addNodeIds ( group , ownNodeId ) ; const groupReport = await assocAPI . getGroup ( group ) ; hasLifeline = ! ! groupReport ?. nodeIds . includes ( ownNodeId ) ; if ( hasLifeline ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Lifeline group #${ group } was assigned with a node association via Association CC` , direction : "none" } ) ; } else { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Assigning lifeline group #${ group } with a node association via Association CC did not work` , direction : "none" } ) ; } } if ( ! hasLifeline && mcAPI . isSupported ( ) && mcInstance ! . getMaxNodesCached ( group ) > 0 ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Assigning lifeline group #${ group } with a node association via Multi Channel Association CC...` , direction : "outbound" } ) ; if ( isAssignedAsEndpointAssociation ( ) ) { await mcAPI . removeDestinations ( { groupId : group , endpoints : [ { nodeId : ownNodeId , endpoint : 0 } ] } ) ; } await mcAPI . addDestinations ( { groupId : group , nodeIds : [ ownNodeId ] } ) ; const groupReport = await mcAPI . getGroup ( group ) ; hasLifeline = ! ! groupReport ?. nodeIds . includes ( ownNodeId ) ; if ( hasLifeline ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Lifeline group #${ group } was assigned with a node association via Multi Channel Association CC` , direction : "none" } ) ; } else { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Assigning lifeline group #${ group } with a node association via Multi Channel Association CC did not work` , direction : "none" } ) ; } } } if ( ! hasLifeline && ! mustUseNodeAssociation ) { if ( isAssignedAsEndpointAssociation ( ) ) { hasLifeline = true ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Lifeline group #${ group } is already assigned with an endpoint association` , direction : "none" } ) ; } else if ( mcAPI . isSupported ( ) && mcAPI . version >= 3 && mcInstance ! . getMaxNodesCached ( group ) > 0 ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Assigning lifeline group #${ group } with a multi channel association...` , direction : "outbound" } ) ; if ( isAssignedAsNodeAssociation ( ) ) { await mcAPI . removeDestinations ( { groupId : group , nodeIds : [ ownNodeId ] } ) ; if ( assocAPI . isSupported ( ) ) { await assocAPI . removeNodeIds ( { groupId : group , nodeIds : [ ownNodeId ] } ) ; await assocAPI . getGroup ( group ) ; } } await mcAPI . addDestinations ( { groupId : group , endpoints : [ { nodeId : ownNodeId , endpoint : 0 } ] } ) ; const groupReport = await mcAPI . getGroup ( group ) ; hasLifeline = ! ! groupReport ?. endpoints . some ( ( a ) => a . nodeId === ownNodeId && a . endpoint === 0 ) ; if ( hasLifeline ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Lifeline group #${ group } was assigned with a multi channel association` , direction : "none" } ) ; } else { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Assigning lifeline group #${ group } with a multi channel association did not work` , direction : "none" } ) ; } } } if ( ! hasLifeline && group === 1 && node . supportsCC ( CommandClasses [ "Z-Wave Plus Info" ] ) && this . index > 0 ) { const rootAssocConfig = node . deviceConfig ?. getAssociationConfigForEndpoint ( 0 , group ) ; const rootMustUseNodeAssociation = ! nodeSupportsMultiChannel || rootAssocConfig ?. multiChannel === false ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Checking root device for fallback assignment of lifeline group #${ group }:
root supports multi channel:  ${ nodeSupportsMultiChannel }
configured strategy:           ${ rootAssocConfig ?. multiChannel ?? "auto" }
must use node association:     ${ rootMustUseNodeAssociation }` } ) ; if ( ! rootMustUseNodeAssociation ) { const rootNodesValueId = getNodeIdsValueId ( 0 , group ) ; const rootHasNodeAssociation = ! ! valueDB . getValue < number [ ] > ( rootNodesValueId ) ?. some ( ( a ) => a === ownNodeId ) ; const rootEndpointsValueId = getEndpointsValueId ( 0 , group ) ; const rootHasEndpointAssociation = ! ! valueDB . getValue < EndpointAddress [ ] > ( rootEndpointsValueId ) ?. some ( ( a ) => a . nodeId === ownNodeId && a . endpoint === 0 ) ; if ( rootHasEndpointAssociation ) { hasLifeline = true ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Lifeline group #${ group } is already assigned with a multi channel association on the root device` , direction : "none" } ) ; } else { const rootMCAPI = node . commandClasses [ "Multi Channel Association" ] ; if ( rootMCAPI . isSupported ( ) ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `Assigning lifeline group #${ group } with a multi channel association on the root device...` , direction : "outbound" } ) ; if ( rootHasNodeAssociation ) { await rootMCAPI . removeDestinations ( { groupId : group , nodeIds : [ ownNodeId ] } ) ; } await rootMCAPI . addDestinations ( { groupId : group , endpoints : [ { nodeId : ownNodeId , endpoint : 0 } ] } ) ; const groupReport = await rootMCAPI . getGroup ( group ) ; hasLifeline = ! ! groupReport ?. endpoints . some ( ( a ) => a . nodeId === ownNodeId && a . endpoint === 0 ) ; } } } } if ( ! hasLifeline ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . index , message : `All attempts to assign lifeline group #${ group } failed, skipping...` , direction : "none" , level : "warn" } ) ; } } valueDB . setValue ( getHasLifelineValueId ( this . index ) , true ) ; } }