import { EventEmitter } from "events" ; import { CoapClient as coap , CoapResponse , ConnectionResult , RequestMethod } from "node-coap-client" ; import { wait } from "alcalzone-shared/async" ; import { isObject } from "alcalzone-shared/typeguards" ; import { createDeferredPromise , DeferredPromise } from "alcalzone-shared/deferred-promise" ; import { Accessory , AccessoryTypes } from "./lib/accessory" ; import { except } from "./lib/array-extensions" ; import { BlindOperation } from "./lib/blind" ; import { endpoints as coapEndpoints , GatewayEndpoints } from "./lib/endpoints" ; import { AllEventCallbacks , AllEvents , ConnectionWatcherEvents } from "./lib/events" ; import { GatewayDetails , UpdatePriority } from "./lib/gatewayDetails" ; import { Group , GroupInfo , GroupOperation } from "./lib/group" ; import { IPSOObject , IPSOOptions } from "./lib/ipsoObject" ; import { LightOperation } from "./lib/light" ; import { log , LoggerFunction , setCustomLogger } from "./lib/logger" ; import { FirmwareUpdateNotification , GatewayRebootReason , Notification , NotificationTypes , RebootNotification } from "./lib/notification" ; import { OperationProvider } from "./lib/operation-provider" ; import { PlugOperation } from "./lib/plug" ; import { Scene } from "./lib/scene" ; import { TradfriError , TradfriErrorCodes } from "./lib/tradfri-error" ; import { invertOperation } from "./lib/utils" ; import { ConnectionWatcher , ConnectionWatcherOptions } from "./lib/watcher" ; import { AirPurifierOperation } from "./lib/airPurifier" ; export type ObserveResourceCallback = ( resp : CoapResponse ) => void ; export type ObserveDevicesCallback = ( addedDevices : Accessory [ ] , removedDevices : Accessory [ ] ) => void ; export interface TradfriClient { on < TEvent extends AllEvents > ( event , callback ) : this ; removeListener < TEvent extends AllEvents > ( event , callback ) : this ; removeAllListeners ( event ? ) : this ; } export interface TradfriOptions { /** Callback for a custom logger function. */ customLogger : LoggerFunction ; /** Whether to use raw CoAP values or the simplified scale */ useRawCoAPValues : boolean ; /** Whether the connection should be automatically watched */ watchConnection : boolean | Partial < ConnectionWatcherOptions > ; } export class TradfriClient extends EventEmitter implements OperationProvider { /** dictionary of CoAP observers */ public observedPaths : string [ ] = [ ] ; /** dictionary of known devices */ public devices : Record < string , Accessory > = { } ; /** dictionary of known groups */ public groups : Record < string , GroupInfo > = { } ; /** Base URL for all CoAP requests */ private requestBase : string ; /** Options regarding IPSO objects and serialization */ private ipsoOptions : IPSOOptions = { } ; private securityCode : string | undefined ; private identity : string | undefined ; private psk : string | undefined ; /** Automatic connection watching */ private watcher : ConnectionWatcher | undefined ; /** A dictionary of the observer callbacks. Used to restore it after a soft reset */ private rememberedObserveCallbacks = new Map < string , ( resp : CoapResponse ) => void > ( ) ; constructor ( hostname ) ; constructor ( hostname , customLogger ) ; constructor ( hostname , options ) ; constructor ( public readonly hostname , optionsOrLogger ? ) { super ( ) ; if ( typeof hostname !== "string" ) throw new Error ( "The hostname must be a string." ) ; this . requestBase = `coaps://${ hostname }:5684/` ; if ( typeof optionsOrLogger === "function" ) { setCustomLogger ( optionsOrLogger ) ; } else if ( typeof optionsOrLogger === "object" ) { if ( optionsOrLogger . customLogger != null ) setCustomLogger ( optionsOrLogger . customLogger ) ; if ( optionsOrLogger . useRawCoAPValues === true ) this . ipsoOptions . skipValueSerializers = true ; if ( optionsOrLogger . watchConnection != null && optionsOrLogger . watchConnection !== false ) { const watcherOptions = optionsOrLogger . watchConnection === true ? undefined : optionsOrLogger . watchConnection ; this . watcher = new ConnectionWatcher ( this , watcherOptions ) ; const eventNames = [ "ping succeeded" , "ping failed" , "connection alive" , "connection lost" , "gateway offline" , "reconnecting" , "give up" , ] ; for ( const event of eventNames ) { this . watcher . on ( event , ( ... args ) => this . emit ( event , ... args ) ) ; } } } } param identity param psk public async connect ( identity , psk ) { const maxAttempts = ( this . watcher != null && this . watcher . options . reconnectionEnabled ) ? this . watcher . options . maximumConnectionAttempts : 1 ; const interval = this . watcher && this . watcher . options . connectionInterval ; const backoffFactor = this . watcher && this . watcher . options . failedConnectionBackoffFactor ; let lastFailureReason ; for ( let attempt = 0 ; attempt < maxAttempts ; attempt ++ ) { if ( attempt > 0 ) { const nextTimeout = Math . round ( interval ! * backoffFactor ! ** Math . min ( 5 , attempt - 1 ) ) ; log ( `retrying connection in ${ nextTimeout } ms` , "debug" ) ; await wait ( nextTimeout ) ; } const connectionResult = await this . tryToConnect ( identity , psk ) ; switch ( connectionResult ) { case true : { if ( this . watcher != null ) this . watcher . start ( ) ; return true ; } case "auth failed" : throw new TradfriError ( "The provided credentials are not valid. Please re-authenticate!" , TradfriErrorCodes . AuthenticationFailed ) ; case "timeout" : { this . emit ( "connection failed" , attempt + 1 , maxAttempts ) ; lastFailureReason = "timeout" ; continue ; } default : { if ( connectionResult instanceof Error ) { this . emit ( "connection failed" , attempt + 1 , maxAttempts ) ; lastFailureReason = new TradfriError ( `An unexpected error occured while connecting to the gateway: ${ connectionResult . message }` , TradfriErrorCodes . ConnectionFailed ) ; lastFailureReason . stack = connectionResult . stack ; continue ; } else { throw new TradfriError ( `An unexpected response was received while trying to connect to the gateway: ${ connectionResult }` , TradfriErrorCodes . ConnectionFailed ) ; } } } } lastFailureReason = lastFailureReason ! ; if ( lastFailureReason === "timeout" ) { throw new TradfriError ( `The gateway did not respond ${ maxAttempts === 1 ? "in time" : `after ${ maxAttempts } tries` }.` , TradfriErrorCodes . ConnectionTimedOut ) ; } else { lastFailureReason . message = `Could not connect to the gateway${ maxAttempts === 1 ? "" : ` after ${ maxAttempts } tries` }:\n` + lastFailureReason . message ; throw lastFailureReason ; } } param identity param psk returns private async tryToConnect ( identity , psk ) { coap . reset ( ) ; coap . setSecurityParams ( this . hostname , { psk : { [ identity ] : psk } , } ) ; coap . setCompatOptions ( this . hostname , { resetAntiReplayWindowBeforeServerHello : true , } ) ; log ( `Attempting connection. Identity = ${ identity }, psk = ${ psk }` , "debug" ) ; const result = await coap . tryToConnect ( this . requestBase ) ; if ( result === true ) { log ( "Connection successful" , "debug" ) ; this . identity = identity ; this . psk = psk ; } else { log ( "Connection failed. Reason: " + result , "debug" ) ; } return result ; } param securityCode returns throws public async authenticate ( securityCode ) { log ( "authenticate() > trying to connect with the security code" , "debug" ) ; switch ( await this . tryToConnect ( "Client_identity" , securityCode ) ) { case true : break ; case "auth failed" : throw new TradfriError ( "The security code is wrong" , TradfriErrorCodes . AuthenticationFailed ) ; case "timeout" : throw new TradfriError ( "The gateway did not respond in time." , TradfriErrorCodes . ConnectionTimedOut ) ; case "error" : throw new TradfriError ( "An unknown error occured while connecting to the gateway" , TradfriErrorCodes . ConnectionFailed ) ; } const identity = `tradfri_${ Date . now ( ) }` ; log ( `authenticating with identity "${ identity }"` , "debug" ) ; let payload = JSON . stringify ( { 9090 : identity } ) ; payload = Buffer . from ( payload ) ; const response = await this . swallowInternalCoapRejections ( coap . request ( `${ this . requestBase }${ coapEndpoints . gateway ( GatewayEndpoints . Authenticate ) }` , "post" , payload ) ) ; if ( response . code . toString ( ) !== "2.01" ) { throw new TradfriError ( `unexpected response (${ response . code . toString ( ) }) to getPSK().` , TradfriErrorCodes . AuthenticationFailed ) ; } const pskResponse = JSON . parse ( response . payload ! . toString ( "utf8" ) ) ; const psk = pskResponse [ "9091" ] ; this . securityCode = securityCode ; return { identity , psk } ; } internal public async reconnectHandler ( ) { this . reset ( true ) ; log ( `trying to reconnect` , "debug" ) ; const result = await this . tryToConnect ( this . identity ! , this . psk ! ) ; if ( result === "auth failed" ) { if ( this . securityCode ) { log ( `invalid credentials, trying to re-authenticate` , "debug" ) ; ( { identity : this . identity , psk : this . psk } = await this . authenticate ( this . securityCode ) ) ; await this . tryToConnect ( this . identity , this . psk ) ; } else { log ( `invalid credentials, cannot reconnect` , "debug" ) ; return false ; } } return true ; } param path param callback returns public async observeResource ( path , callback ) { const observerUrl = this . getObserverUrl ( path ) ; if ( this . observedPaths . indexOf ( observerUrl ) > - 1 ) return false ; this . observedPaths . push ( observerUrl ) ; this . rememberedObserveCallbacks . set ( observerUrl , callback ) ; await this . swallowInternalCoapRejections ( coap . observe ( observerUrl , "get" , callback ) ) ; return true ; } private getObserverUrl ( path ) { path = normalizeResourcePath ( path ) ; return path . startsWith ( this . requestBase ) ? path : `${ this . requestBase }${ path }` ; } param path public isObserving ( path ) { const observerUrl = this . getObserverUrl ( path ) ; return this . observedPaths . indexOf ( observerUrl ) > - 1 ; } param path public stopObservingResource ( path ) { const observerUrl = this . getObserverUrl ( path ) ; const index = this . observedPaths . indexOf ( observerUrl ) ; if ( index === - 1 ) return ; coap . stopObserving ( observerUrl ) ; this . observedPaths . splice ( index , 1 ) ; this . rememberedObserveCallbacks . delete ( observerUrl ) ; } param preserveObservers public reset ( preserveObservers = false ) { coap . reset ( ) ; this . observedPaths = [ ] ; if ( ! preserveObservers ) this . rememberedObserveCallbacks . clear ( ) ; } /**
     * Closes the underlying CoAP client and clears all observers.
     */ public destroy ( ) { if ( this . watcher != null ) this . watcher . stop ( ) ; this . reset ( ) ; } /**
     * Restores all previously remembered observers with their original callbacks
     * Call this AFTER a dead connection was restored
     */ public async restoreObservers ( ) { log ( "restoring previously used observers" , "debug" ) ; let devicesRestored = false ; const devicesPath = this . getObserverUrl ( coapEndpoints . devices ) ; let groupsAndScenesRestored = false ; const groupsPath = this . getObserverUrl ( coapEndpoints . groups ) ; const scenesPath = this . getObserverUrl ( coapEndpoints . scenes ) ; for ( const [ path , callback ] of this . rememberedObserveCallbacks . entries ( ) ) { if ( path . indexOf ( devicesPath ) > - 1 ) { if ( ! devicesRestored ) { log ( "restoring device observers" , "debug" ) ; await this . observeDevices ( ) ; devicesRestored = true ; } } else if ( path . indexOf ( groupsPath ) > - 1 || path . indexOf ( scenesPath ) > - 1 ) { if ( ! groupsAndScenesRestored ) { log ( "restoring groups and scene observers" , "debug" ) ; await this . observeGroupsAndScenes ( ) ; groupsAndScenesRestored = true ; } } else { log ( `restoring custom observer for path "${ path }"` , "debug" ) ; await this . observeResource ( path , callback ) ; } } } private observeDevicesPromise : DeferredPromise < void > | undefined ; returns public async observeDevices ( ) { if ( this . isObserving ( coapEndpoints . devices ) ) return ; this . observeDevicesPromise = createDeferredPromise < void > ( ) ; void this . observeResource ( coapEndpoints . devices , ( resp ) => void this . observeDevices_callback ( resp ) ) . catch ( e => { if ( ! ! this . observeDevicesPromise ) this . observeDevicesPromise . reject ( e ) ; } ) ; return this . observeDevicesPromise ; } private async observeDevices_callback ( response ) { if ( response . code . toString ( ) !== "2.05" ) { if ( ! this . handleNonSuccessfulResponse ( response , `observeDevices()` , false ) ) return ; } const newDevices = parsePayload ( response ) ; log ( `got all devices: ${ JSON . stringify ( newDevices ) }` ) ; const oldKeys = Object . keys ( this . devices ) . map ( k => + k ) . sort ( ) ; const newKeys = newDevices . sort ( ) ; const addedKeys = except ( newKeys , oldKeys ) ; log ( `adding devices with keys ${ JSON . stringify ( addedKeys ) }` , "debug" ) ; const observeDevicePromises = newKeys . map ( id => { const handleResponse = ( resp ) => { const result = this . observeDevice_callback ( id , resp ) ; if ( this . observeDevicesPromise != null ) { if ( result ) { if ( newKeys . every ( k => k in this . devices ) ) { this . observeDevicesPromise . resolve ( ) ; this . observeDevicesPromise = undefined ; } } else { this . observeDevicesPromise . reject ( new Error ( `The device with the id ${ id } could not be observed` ) ) ; this . observeDevicesPromise = undefined ; } } } ; return this . observeResource ( `${ coapEndpoints . devices }/${ id }` , handleResponse ) ; } ) ; await Promise . all ( observeDevicePromises ) ; const removedKeys = except ( oldKeys , newKeys ) ; log ( `removing devices with keys ${ JSON . stringify ( removedKeys ) }` , "debug" ) ; for ( const id of removedKeys ) { delete this . devices [ id ] ; this . stopObservingResource ( `${ coapEndpoints . devices }/${ id }` ) ; this . emit ( "device removed" , id ) ; } } public stopObservingDevices ( ) { const pathPrefix = `${ this . requestBase }${ coapEndpoints . devices }` ; this . observedPaths . filter ( p => p . startsWith ( pathPrefix ) ) . forEach ( p => this . stopObservingResource ( p ) ) ; } private observeDevice_callback ( instanceId , response ) { if ( response . code . toString ( ) !== "2.05" ) { if ( ! this . handleNonSuccessfulResponse ( response , `observeDevice(${ instanceId })` ) ) return false ; } const result = parsePayload ( response ) ; log ( `observeDevice > ` + JSON . stringify ( result ) , "debug" ) ; const accessory = new Accessory ( this . ipsoOptions ) . parse ( result ) . fixBuggedProperties ( ) . createProxy ( ) ; this . devices [ instanceId ] = accessory . clone ( ) ; this . emit ( "device updated" , accessory . link ( this ) ) ; return true ; } private observeGroupsPromise : DeferredPromise < void > | undefined ; private observeScenesPromises : Map < number , DeferredPromise < void > > | undefined ; returns public async observeGroupsAndScenes ( ) { if ( this . isObserving ( coapEndpoints . groups ) ) return ; this . observeGroupsPromise = createDeferredPromise < void > ( ) ; void this . observeResource ( coapEndpoints . groups , ( resp ) => void this . observeGroups_callback ( resp ) ) . catch ( e => { if ( ! ! this . observeGroupsPromise ) this . observeGroupsPromise . reject ( e ) ; } ) ; return this . observeGroupsPromise ; } private async observeGroups_callback ( response ) { if ( response . code . toString ( ) !== "2.05" ) { if ( ! this . handleNonSuccessfulResponse ( response , `observeGroups()` , false ) ) return ; } const newGroups = parsePayload ( response ) ; log ( `got all groups: ${ JSON . stringify ( newGroups ) }` ) ; const oldKeys = Object . keys ( this . groups ) . map ( k => + k ) . sort ( ) ; const newKeys = newGroups . sort ( ) ; const addedKeys = except ( newKeys , oldKeys ) ; log ( `adding groups with keys ${ JSON . stringify ( addedKeys ) }` , "debug" ) ; if ( this . observeGroupsPromise != null && this . observeScenesPromises == null ) { this . observeScenesPromises = new Map ( newKeys . map ( id => [ id , createDeferredPromise < void > ( ) ] as [ number , DeferredPromise < void > ] ) ) ; } const observeGroupPromises = newKeys . map ( id => { const handleResponse = ( resp ) => { const result = this . observeGroup_callback ( id , resp ) ; if ( this . observeGroupsPromise != null ) { if ( result ) { if ( newKeys . every ( k => k in this . groups ) ) { Promise . all ( this . observeScenesPromises ! . values ( ) ) . then ( ( ) => { this . observeGroupsPromise ! . resolve ( ) ; this . observeGroupsPromise = undefined ; this . observeScenesPromises = undefined ; } ) . catch ( reason => { if ( this . observeGroupsPromise != null ) { this . observeGroupsPromise . reject ( reason ) ; } this . observeGroupsPromise = undefined ; this . observeScenesPromises = undefined ; } ) ; } } else { this . observeGroupsPromise . reject ( new Error ( `The group with the id ${ id } could not be observed` ) ) ; this . observeGroupsPromise = undefined ; } } } ; return this . observeResource ( `${ coapEndpoints . groups }/${ id }` , handleResponse ) ; } ) ; await Promise . all ( observeGroupPromises ) ; const removedKeys = except ( oldKeys , newKeys ) ; log ( `removing groups with keys ${ JSON . stringify ( removedKeys ) }` , "debug" ) ; removedKeys . forEach ( ( id ) => { delete this . groups [ id ] ; this . stopObservingGroup ( id ) ; this . emit ( "group removed" , id ) ; } ) ; } public stopObservingGroups ( ) { for ( const id of Object . keys ( this . groups ) ) { this . stopObservingGroup ( + id ) ; } this . stopObservingResource ( coapEndpoints . groups ) ; } private stopObservingGroup ( instanceId ) { this . stopObservingResource ( `${ coapEndpoints . groups }/${ instanceId }` ) ; const scenesPrefix = this . getObserverUrl ( `${ coapEndpoints . scenes }/${ instanceId }` ) ; const pathsToDelete = this . observedPaths . filter ( path => path . startsWith ( scenesPrefix ) ) ; for ( const path of pathsToDelete ) { this . stopObservingResource ( path ) ; } } private observeGroup_callback ( instanceId , response ) { if ( response . code . toString ( ) !== "2.05" ) { if ( ! this . handleNonSuccessfulResponse ( response , `observeGroup(${ instanceId })` ) ) return false ; } const result = parsePayload ( response ) ; const group = new Group ( this . ipsoOptions ) . parse ( result ) . fixBuggedProperties ( ) . createProxy ( ) ; let groupInfo ; if ( ! ( instanceId in this . groups ) ) { this . groups [ instanceId ] = { group : undefined ! , scenes : { } , } ; } groupInfo = this . groups [ instanceId ] ; groupInfo . group = group . clone ( ) ; this . emit ( "group updated" , group . link ( this ) ) ; this . observeResource ( `${ coapEndpoints . scenes }/${ instanceId }` , ( resp ) => void this . observeScenes_callback ( instanceId , resp ) ) ; return true ; } private async observeScenes_callback ( groupId , response ) { if ( response . code . toString ( ) !== "2.05" ) { if ( ! this . handleNonSuccessfulResponse ( response , `observeScenes(${ groupId })` , false ) ) return ; } const groupInfo = this . groups [ groupId ] ; const newScenes = parsePayload ( response ) ; log ( `got all scenes in group ${ groupId }: ${ JSON . stringify ( newScenes ) }` ) ; const oldKeys = Object . keys ( groupInfo . scenes ) . map ( k => + k ) . sort ( ) ; const newKeys = newScenes . sort ( ) ; const addedKeys = except ( newKeys , oldKeys ) ; log ( `adding scenes with keys ${ JSON . stringify ( addedKeys ) } to group ${ groupId }` , "debug" ) ; if ( newKeys . length > 0 ) { const observeScenePromises = newKeys . map ( id => { const handleResponse = ( resp ) => { const result = this . observeScene_callback ( groupId , id , resp ) ; if ( this . observeScenesPromises != null ) { const scenePromise = this . observeScenesPromises . get ( groupId ) ; if ( result ) { if ( newKeys . every ( k => k in groupInfo . scenes ) ) { if ( ! ! scenePromise ) scenePromise . resolve ( ) ; } } else { if ( ! ! scenePromise ) scenePromise . reject ( new Error ( `The scene with the id ${ id } could not be observed` ) ) ; } } } ; return this . observeResource ( `${ coapEndpoints . scenes }/${ groupId }/${ id }` , handleResponse ) ; } ) ; await Promise . all ( observeScenePromises ) ; } else { this . observeScenesPromises ?. get ( groupId ) ?. resolve ( ) ; } const removedKeys = except ( oldKeys , newKeys ) ; log ( `removing scenes with keys ${ JSON . stringify ( removedKeys ) } from group ${ groupId }` , "debug" ) ; removedKeys . forEach ( id => { delete groupInfo . scenes [ id ] ; this . stopObservingResource ( `${ coapEndpoints . scenes }/${ groupId }/${ id }` ) ; this . emit ( "scene removed" , groupId , id ) ; } ) ; } private observeScene_callback ( groupId , instanceId , response ) { if ( response . code . toString ( ) !== "2.05" ) { if ( ! this . handleNonSuccessfulResponse ( response , `observeScene(${ groupId }, ${ instanceId })` ) ) return false ; } const result = parsePayload ( response ) ; const scene = new Scene ( this . ipsoOptions ) . parse ( result ) . fixBuggedProperties ( ) . createProxy ( ) ; this . groups [ groupId ] . scenes [ instanceId ] = scene . clone ( ) ; this . emit ( "scene updated" , groupId , scene . link ( this ) ) ; return true ; } private observeGatewayPromise : DeferredPromise < void > | undefined ; returns public async observeGateway ( ) { if ( this . isObserving ( coapEndpoints . gateway ( GatewayEndpoints . Details ) ) ) return ; this . observeGatewayPromise = createDeferredPromise < void > ( ) ; void this . observeResource ( coapEndpoints . gateway ( GatewayEndpoints . Details ) , ( resp ) => void this . observeGateway_callback ( resp ) ) . catch ( e => { if ( ! ! this . observeGatewayPromise ) this . observeGatewayPromise . reject ( e ) ; } ) ; return this . observeGatewayPromise ; } private async observeGateway_callback ( response ) { log ( `received response to observeGateway(): ${ JSON . stringify ( response , null , 4 ) }` ) ; if ( response . code . toString ( ) !== "2.05" ) { if ( ! this . handleNonSuccessfulResponse ( response , `observeGateway()` , false ) ) { log ( `  => not successful` ) ; if ( this . observeGatewayPromise != null ) { this . observeGatewayPromise . reject ( new Error ( `The gateway could not be observed` ) ) ; this . observeGatewayPromise = undefined ; } return ; } } log ( `got gateway information` ) ; const result = parsePayload ( response ) ; const gateway = new GatewayDetails ( this . ipsoOptions ) . parse ( result ) . fixBuggedProperties ( ) . createProxy ( ) ; this . emit ( "gateway updated" , gateway . link ( this ) ) ; if ( this . observeGatewayPromise != null ) { this . observeGatewayPromise . resolve ( ) ; this . observeGatewayPromise = undefined ; } } public stopObservingGateway ( ) { this . stopObservingResource ( `${ this . requestBase }${ coapEndpoints . gateway ( GatewayEndpoints . Details ) }` ) ; } private observeNotificationsPromise : DeferredPromise < void > | undefined ; returns public async observeNotifications ( ) { if ( this . isObserving ( coapEndpoints . notifications ) ) return ; this . observeNotificationsPromise = createDeferredPromise < void > ( ) ; void this . observeResource ( coapEndpoints . notifications , ( resp ) => void this . observeNotifications_callback ( resp ) ) . catch ( e => { if ( ! ! this . observeNotificationsPromise ) this . observeNotificationsPromise . reject ( e ) ; } ) ; return this . observeNotificationsPromise ; } private async observeNotifications_callback ( response ) { log ( `received response to observeNotifications(): ${ JSON . stringify ( response , null , 4 ) }` ) ; if ( response . code . toString ( ) !== "2.05" ) { if ( ! this . handleNonSuccessfulResponse ( response , `observeNotifications()` , false ) ) { log ( `  => not successful` ) ; if ( this . observeNotificationsPromise != null ) { this . observeNotificationsPromise . reject ( new Error ( `The notifications could not be observed` ) ) ; this . observeNotificationsPromise = undefined ; } return ; } } const notifications = parsePayload ( response ) as Record < string , any > [ ] ; for ( const not of notifications ) { const notification = new Notification ( ) . parse ( not ) ; switch ( notification . event ) { case NotificationTypes . Reboot : this . emit ( "rebooting" , GatewayRebootReason [ ( notification . details as RebootNotification ) . reason ] ) ; break ; case NotificationTypes . LossOfInternetConnectivity : this . emit ( "internet connectivity changed" , ! notification . isActive ) ; break ; case NotificationTypes . NewFirmwareAvailable : { const details = notification . details as FirmwareUpdateNotification ; this . emit ( "firmware update available" , details . releaseNotes , UpdatePriority [ details . priority ] ) ; break ; } } } if ( this . observeNotificationsPromise != null ) { this . observeNotificationsPromise . resolve ( ) ; this . observeNotificationsPromise = undefined ; } } public stopObservingNotifications ( ) { this . stopObservingResource ( `${ this . requestBase }${ coapEndpoints . notifications }` ) ; } param resp param context returns private handleNonSuccessfulResponse ( resp , context , ignore404 = true ) { const code = resp . code . toString ( ) ; const payload = parsePayload ( resp ) || "" ; if ( code === "4.04" && ignore404 ) { return false ; } else { this . emit ( "error" , new Error ( `unexpected response (${ code }) to ${ context }: ${ payload }` ) ) ; return false ; } } param timeout public ping ( timeout ? ) { return coap . ping ( this . requestBase , timeout ) ; } param accessory returns public updateDevice ( accessory ) { if ( ! ( accessory . instanceId in this . devices ) ) { throw new Error ( `The device with id ${ accessory . instanceId } is not known and cannot be update!` ) ; } const original = this . devices [ accessory . instanceId ] ; return this . updateResource ( `${ coapEndpoints . devices }/${ accessory . instanceId }` , accessory , original ) ; } param group returns public updateGroup ( group ) { if ( ! ( group . instanceId in this . groups ) ) { throw new Error ( `The group with id ${ group . instanceId } is not known and cannot be update!` ) ; } const original = this . groups [ group . instanceId ] . group ; return this . updateResource ( `${ coapEndpoints . groups }/${ group . instanceId }` , group , original ) ; } param path param newObj param reference returns private async updateResource ( path , newObj , reference ) { newObj . options = this . ipsoOptions ; log ( `updateResource(${ path }) > comparing ${ JSON . stringify ( newObj ) } with the reference ${ JSON . stringify ( reference ) }` , "debug" ) ; const serializedObj = newObj . serialize ( reference ) ; if ( ! serializedObj || Object . keys ( serializedObj ) . length === 0 ) { log ( `updateResource(${ path }) > empty object, not sending any payload` , "debug" ) ; return false ; } let payload = JSON . stringify ( serializedObj ) ; log ( `updateResource(${ path }) > sending payload: ${ payload }` , "debug" ) ; payload = Buffer . from ( payload ) ; await this . swallowInternalCoapRejections ( coap . request ( `${ this . requestBase }${ path }` , "put" , payload ) ) ; return true ; } param group param operation param force returns public operateGroup ( group , operation , force = false ) { if ( ! isObject ( operation ) ) { throw new Error ( `The parameter "operation" must be an object!` ) ; } const newGroup = group . clone ( ) . merge ( operation , true ) ; const reference = group . clone ( ) ; if ( force ) { reference . merge ( invertOperation ( operation ) , true ) ; } return this . updateResource ( `${ coapEndpoints . groups }/${ group . instanceId }` , newGroup , reference ) ; } param accessory param operation param force returns public operateLight ( accessory , operation , force = false ) { if ( accessory . type !== AccessoryTypes . lightbulb ) { throw new Error ( `The parameter "accessory" must be a lightbulb!` ) ; } if ( ! isObject ( operation ) ) { throw new Error ( `The parameter "operation" must be an object!` ) ; } const newAccessory = accessory . clone ( ) ; newAccessory . lightList [ 0 ] . merge ( operation ) ; const reference = accessory . clone ( ) ; if ( force ) { reference . lightList [ 0 ] . merge ( invertOperation ( operation ) , true ) ; } return this . updateResource ( `${ coapEndpoints . devices }/${ accessory . instanceId }` , newAccessory , reference ) ; } param accessory param operation param force returns public operatePlug ( accessory , operation , force = false ) { if ( accessory . type !== AccessoryTypes . plug ) { throw new Error ( `The parameter "accessory" must be a plug!` ) ; } if ( ! isObject ( operation ) ) { throw new Error ( `The parameter "operation" must be an object!` ) ; } const newAccessory = accessory . clone ( ) ; newAccessory . plugList [ 0 ] . merge ( operation ) ; const reference = accessory . clone ( ) ; if ( force ) { reference . plugList [ 0 ] . merge ( invertOperation ( operation ) , true ) ; } return this . updateResource ( `${ coapEndpoints . devices }/${ accessory . instanceId }` , newAccessory , reference ) ; } param accessory param operation param force returns public operateBlind ( accessory , operation , force = false ) { if ( accessory . type !== AccessoryTypes . blind ) { throw new Error ( `The parameter "accessory" must be a blind!` ) ; } if ( ! isObject ( operation ) ) { throw new Error ( `The parameter "operation" must be an object!` ) ; } const newAccessory = accessory . clone ( ) ; newAccessory . blindList [ 0 ] . merge ( operation , true ) ; const reference = accessory . clone ( ) ; if ( force ) { reference . blindList [ 0 ] . merge ( invertOperation ( operation ) , true ) ; } return this . updateResource ( `${ coapEndpoints . devices }/${ accessory . instanceId }` , newAccessory , reference ) ; } param accessory param operation param force returns public operateAirPurifier ( accessory , operation , force = false ) { if ( accessory . type !== AccessoryTypes . airPurifier ) { throw new Error ( `The parameter "accessory" must be an air purifier!` ) ; } if ( ! isObject ( operation ) ) { throw new Error ( `The parameter "operation" must be an object!` ) ; } const newAccessory = accessory . clone ( ) ; newAccessory . airPurifierList [ 0 ] . merge ( operation , true ) ; const reference = accessory . clone ( ) ; if ( force ) { reference . airPurifierList [ 0 ] . merge ( invertOperation ( operation ) , true ) ; } return this . updateResource ( `${ coapEndpoints . devices }/${ accessory . instanceId }` , newAccessory , reference ) ; } param path param method param payload public async request ( path , method , payload ? ) { let jsonPayload ; if ( payload != null ) { jsonPayload = JSON . stringify ( payload ) ; log ( "sending custom payload: " + jsonPayload , "debug" ) ; jsonPayload = Buffer . from ( jsonPayload ) ; } const resp = await this . swallowInternalCoapRejections ( coap . request ( `${ this . requestBase }${ path }` , method , jsonPayload ! as Buffer ) ) ; return { code : resp . code . toString ( ) , payload : parsePayload ( resp ) , } ; } private swallowInternalCoapRejections < T > ( promise ) { return new Promise ( async ( resolve , reject ) => { try { resolve ( await promise ) ; } catch ( e ) { if ( /coap\s?client was reset/i . test ( e . message ) ) { this . emit ( "error" , new TradfriError ( "The network stack was reset. Pending promises will not be fulfilled." , TradfriErrorCodes . NetworkReset ) ) ; } else if ( /dtls handshake timed out/i . test ( e . message ) ) { this . emit ( "error" , new TradfriError ( "Could not establish a secure connection in time. Pending promises will not be fulfilled." , TradfriErrorCodes . ConnectionTimedOut ) ) ; } else { reject ( e ) ; } } } ) ; } /** Reboots the gateway. This operation is additionally acknowledged with a reboot notification. */ public async rebootGateway ( ) { const { code } = await this . request ( coapEndpoints . gateway ( GatewayEndpoints . Reboot ) , "post" ) ; return code === "2.01" ; } /** Factory resets the gateway. WARNING: All configuration will be wiped! */ public async resetGateway ( ) { const { code } = await this . request ( coapEndpoints . gateway ( GatewayEndpoints . Reset ) , "post" ) ; return code === "2.01" ; } } /** Normalizes the path to a resource, so it can be used for storing the observer */ function normalizeResourcePath ( path ) { path = path || "" ; while ( path . startsWith ( "/" ) ) path = path . slice ( 1 ) ; while ( path . endsWith ( "/" ) ) path = path . slice ( 0 , - 1 ) ; return path ; } function parsePayload ( response ) { if ( response . payload == null ) return null ; switch ( response . format ) { case 0 : case null : return response . payload . toString ( "utf-8" ) ; case 50 : const json = response . payload . toString ( "utf-8" ) ; try { return JSON . parse ( json ) ; } catch ( e ) { return null ; } default : log ( `unknown CoAP response format ${ response . format }` , "warn" ) ; return response . payload ; } }