import { actuatorCCs , CommandClasses , isZWaveError , TranslatedValueID , ValueID , valueIdToString , ValueMetadata , ValueMetadataNumeric , ZWaveError , ZWaveErrorCodes , } from "@zwave-js/core" ; import { distinct } from "alcalzone-shared/arrays" ; import type { CCAPI , SetValueAPIOptions } from "../commandclass/API" ; import type { Driver } from "../driver/Driver" ; import type { ZWaveNode } from "./Node" ; import { VirtualEndpoint } from "./VirtualEndpoint" ; export interface VirtualValueID extends TranslatedValueID { /** The metadata that belongs to this virtual value ID */ metadata : ValueMetadata ; /** The maximum supported CC version among all nodes targeted by this virtual value ID */ ccVersion : number ; } export class VirtualNode extends VirtualEndpoint { public constructor ( public readonly id , driver , /** The references to the physical node this virtual node abstracts */ physicalNodes ) { super ( undefined , driver , 0 ) ; super . setNode ( this ) ; this . physicalNodes = [ ... physicalNodes ] . filter ( ( n ) => n . id !== driver . controller . ownNodeId ) ; } public readonly physicalNodes : ZWaveNode [ ] ; /**
     * Updates a value for a given property of a given CommandClass.
     * This will communicate with the physical node(s) this virtual node represents!
     */ public async setValue ( valueId , value , options ? ) { try { const endpointInstance = this . getEndpoint ( valueId . endpoint || 0 ) ; if ( ! endpointInstance ) return false ; const api = ( endpointInstance . commandClasses as any ) [ valueId . commandClass ] as CCAPI ; if ( ! api . setValue ) return false ; await api . setValue ( { property : valueId . property , propertyKey : valueId . propertyKey } , value , options ) ; if ( api . isSetValueOptimistic ( valueId ) ) { const affectedNodes = endpointInstance . node . physicalNodes . filter ( ( node ) => node . getEndpoint ( endpointInstance . index ) ?. supportsCC ( valueId . commandClass ) ) ; for ( const node of affectedNodes ) { node . valueDB . setValue ( valueId , value ) ; } } return true ; } catch ( e ) { if ( isZWaveError ( e ) ) { let handled = false ; let emitErrorEvent = false ; switch ( e . code ) { case ZWaveErrorCodes . CC_NotImplemented : case ZWaveErrorCodes . CC_NoAPI : handled = true ; break ; case ZWaveErrorCodes . Argument_Invalid : handled = true ; emitErrorEvent = true ; break ; } if ( emitErrorEvent ) this . driver . emit ( "error" , e ) ; if ( handled ) return false ; } throw e ; } } /**
     * Returns a list of all value IDs and their metadata that can be used to
     * control the physical node(s) this virtual node represents.
     */ public getDefinedValueIDs ( ) { if ( this . physicalNodes . every ( ( n ) => n . isSecure === true ) ) return [ ] ; const ret = new Map < string , VirtualValueID > ( ) ; for ( const pNode of this . physicalNodes ) { if ( pNode . isSecure === true ) continue ; const valueIDs = pNode . getDefinedValueIDs ( ) . filter ( ( v ) => actuatorCCs . includes ( v . commandClass ) ) ; for ( const valueId of valueIDs ) { const mapKey = valueIdToString ( valueId ) ; const ccVersion = pNode . getCCVersion ( valueId . commandClass ) ; const metadata = pNode . getValueMetadata ( valueId ) ; if ( ! metadata . writeable ) continue ; const needsUpdate = ! ret . has ( mapKey ) || ret . get ( mapKey ) ! . ccVersion < ccVersion ; if ( needsUpdate ) { ret . set ( mapKey , { ... valueId , ccVersion , metadata : { ... metadata , readable : false } } ) ; } } } const exposedEndpoints = distinct ( [ ... ret . values ( ) ] . map ( ( v ) => v . endpoint ) . filter ( ( e ) : e is number => e !== undefined ) ) ; for ( const endpoint of exposedEndpoints ) { const valueId = { commandClass : CommandClasses . Basic , commandClassName : "Basic" , endpoint , property : "targetValue" , propertyName : "Target value" } ; const ccVersion = 1 ; const metadata = { ... ValueMetadata . WriteOnlyUInt8 , label : "Target value" } ; ret . set ( valueIdToString ( valueId ) , { ... valueId , ccVersion , metadata } ) ; } return [ ... ret . values ( ) ] ; } /** Cache for this node's endpoint instances */ private _endpointInstances = new Map < number , VirtualEndpoint > ( ) ; /**
     * Returns an endpoint of this node with the given index. 0 returns the node itself.
     */ public getEndpoint ( index ) ; public getEndpoint ( index ) ; public getEndpoint ( index ) { if ( index < 0 ) throw new ZWaveError ( "The endpoint index must be positive!" , ZWaveErrorCodes . Argument_Invalid ) ; if ( index === 0 ) return this ; if ( ! this . isMultiChannelInterviewComplete ) { this . driver . driverLog . print ( `Virtual node ${ this . id ?? "??" }, Endpoint ${ index }: Trying to access endpoint instance before the Multi Channel interview of all nodes was completed!` , "error" ) ; return undefined ; } if ( index > this . getEndpointCount ( ) ) return undefined ; if ( ! this . _endpointInstances . has ( index ) ) { this . _endpointInstances . set ( index , new VirtualEndpoint ( this , this . driver , index ) ) ; } return this . _endpointInstances . get ( index ) ! ; } /** Returns the current endpoint count of this virtual node (the maximum in the list of physical nodes) */ public getEndpointCount ( ) { let ret = 0 ; for ( const node of this . physicalNodes ) { const count = node . getEndpointCount ( ) ; ret = Math . max ( ret , count ) ; } return ret ; } private get isMultiChannelInterviewComplete ( ) : boolean { for ( const node of this . physicalNodes ) { if ( ! node [ "isMultiChannelInterviewComplete" ] ) return false ; } return true ; } }