import { EventEmitter } from "events" ; import { Socket , SocketConnectOpts } from "net" ; import { Discover } from "./discover" ; import { defaultLogger } from "./logger" ; import { AdjustType , Color , Command , CommandType , DevicePropery , FlowState , ICommandResult , IConfig , IDevice , IEventResult , Scene , StartFlowAction , } from "./models" ; import { ILogger } from "./models/logger" ; const DEFAULT_PORT = 55443 ; /**
 * The client to connect and control the light
 */ export class Yeelight extends EventEmitter { public connected : boolean ; public autoReconnect = false ; public disablePing = false ; public autoReconnectTime = 1000 ; public connectTimeout = 1000 ; private client : Socket ; private commandId = 1 ; private sentCommands : { [ commandId ] : Command ; } = { } ; private resultCommands : ICommandResult [ ] ; private isConnecting = false ; private isClosing = false ; private isReconnecting = false ; private reconnectTimeout : NodeJS . Timer | null = null ; private pingTimeout : NodeJS . Timer | null = null ; private readonly EVENT_NAME = "command_result" ; constructor param { IConfig } options constructor ( private options , private logger ? ) { super ( ) ; this . logger = logger || defaultLogger ; this . resultCommands = new Array < ICommandResult > ( ) ; this . emit ( "ready" , this ) ; this . options . timeout = this . options . timeout || 5000 ; } public onData ( data ) { const messages = data . toString ( ) ; messages . split ( "\n" ) . forEach ( ( message ) => { const json = message . toString ( ) ; if ( json ) { const result = JSON . parse ( json ) ; this . onMessage ( result ) ; } } ) ; } public onMessage ( result ) { this . resultCommands . push ( result ) ; const commandId = "" + result . id ; const originalCommand = this . sentCommands [ commandId ] ; if ( ! originalCommand ) { return ; } const eventData = { action : originalCommand . method , command : originalCommand , result , success : true } ; this . logger . info ( "Light data recieved: " , result ) ; this . emit ( `${ this . EVENT_NAME }_${ result . id }` , eventData ) ; this . emit ( originalCommand . method , eventData ) ; delete this . sentCommands [ commandId ] ; if ( result . id && result . result ) { this . emit ( "commandSuccess" , eventData ) ; } if ( result && result . error ) { eventData . success = false ; this . emit ( "commandError" , eventData ) ; } } /**
     * Drop connection/listerners and clean up resources.
     */ public disconnect ( ) { this . removeAllListeners ( ) ; this . emit ( "end" ) ; this . client . removeAllListeners ( "data" ) ; this . isClosing = true ; return new Promise ( ( resolve ) => this . client . end ( resolve as any ) ) . then ( ( ) => { return this . closeConnection ( ) ; } ) ; } returns public async connect ( ) { if ( this . isConnecting ) { throw new Error ( "Already connecting" ) ; } if ( this . options . lightIp ) { return this . connectToIp ( this . options . lightIp , this . options . lightPort ) ; } else if ( this . options . lightId ) { const discover = new Discover ( { filter : ( d ) => d . id === this . options . lightId , limit : 1 , timeout : this . connectTimeout } ) ; const devices = await discover . start ( ) ; await discover . destroy ( ) ; const device = devices [ 0 ] ; if ( device ) { return this . connectToIp ( device . host , device . port ) ; } else { throw new Error ( "Unable to connect, no device with id '" + this . options . lightId + "' found" ) ; } } else { throw new Error ( "Unable to connect, neither config.lightIp or options.lightId is set" ) ; } } public setPower ( turnOn = true , effect = "sudden" , duration = 500 ) { return this . sendCommand ( new Command ( 1 , CommandType . SET_POWER , [ ( turnOn ? "on" : "off" ) , effect , duration ] ) ) ; } param type param time returns { Promise < IEventResult > } public cronAdd ( type , time ) { return this . sendCommand ( new Command ( 1 , CommandType . CRON_ADD , [ 0 , time ] ) ) ; } param type returns { Promise < IEventResult > } public cronGet ( type ) { return this . sendCommand ( new Command ( 1 , CommandType . CRON_GET , [ type ] ) ) ; } param type returns { Promise < IEventResult > } public cronDelete ( type ) { return this . sendCommand ( new Command ( 1 , CommandType . CRON_DEL , [ type ] ) ) ; } returns { Promise < ICommandResult > } returns { Promise < IEventResult > } public toggle ( ) { return this . sendCommand ( new Command ( 1 , CommandType . TOGGLE , [ ] ) ) ; } returns { Promise < IEventResult > } public setDefault ( ) { return this . sendCommand ( new Command ( 1 , CommandType . SET_DEFAULT , [ ] ) ) ; } param { FlowState [ ] } states param { StarFlowAction } action param { number } repeat default returns { Promise < IEventResult > } public startColorFlow ( states , action = StartFlowAction . LED_STAY , repeat = 0 ) { const values = states . reduce ( ( a , b ) => [ ... a , ... b . getState ( ) ] , [ ] ) ; return this . sendCommand ( new Command ( 1 , CommandType . START_COLOR_FLOW , [ repeat , action , values . join ( "," ) ] ) ) ; } /**
     * This method is used to stop a running color flow.
     */ public stopColorFlow ( ) { return this . sendCommand ( new Command ( 1 , CommandType . STOP_COLOR_FLOW , [ ] ) ) ; } param scene returns { Promise < IEventResult > } public setScene < T extends Scene > ( scene ) { return this . sendCommand ( new Command ( 1 , CommandType . SET_SCENE , scene . getData ( ) ) ) ; } param { string [ ] } params returns { Promise < IEventResult > } public getProperty ( params ) { return this . sendCommand ( new Command ( 1 , CommandType . GET_PROPS , params ) ) ; } param { number } ct param { "smooth" | "sudden" } effect param { number } duration returns { Promise < IEventResult > } public setCtAbx ( ct , effect = "sudden" , duration = 500 ) { return this . sendCommand ( new Command ( 1 , CommandType . SET_CT_ABX , [ ct , effect , duration ] ) ) ; } param color param { "smooth" | "sudden" } effect param { number } duration returns { Promise < IEventResult > } public setRGB ( color , effect , duration = 500 ) { return this . sendCommand ( new Command ( 1 , CommandType . SET_RGB , [ color . getValue ( ) , effect , duration ] ) ) ; } param hue param sat param { "smooth" | "sudden" } effect param { number } duration returns { Promise < IEventResult > } public setHSV ( hue , sat , effect = "sudden" , duration = 500 ) { return this . sendCommand ( new Command ( 1 , CommandType . SET_HSV , [ hue , sat , effect , duration ] ) ) ; } param brightness param { "smooth" | "sudden" } effect param { number } duration returns { Promise < IEventResult > } public setBright ( brightness , effect = "sudden" , duration = 500 ) { return this . sendCommand ( new Command ( 1 , CommandType . SET_BRIGHT , [ brightness , effect , duration ] ) ) ; } param command param { AdjustType } adjustType param { string } prop returns { Promise < IEventResult > } public setAdjust ( adjustType , prop ) { return this . sendCommand ( new Command ( 1 , CommandType . SET_ADJUST , [ adjustType , prop ] ) ) ; } param action param { string } host param { number } port returns { Promise < IEventResult > } public setMusic ( action , host , port ) { return this . sendCommand ( new Command ( 1 , CommandType . SET_MUSIC , [ action , host , port ] ) ) ; } param { string } name returns { Promise < IEventResult > } public setName ( name ) { return this . sendCommand ( new Command ( 1 , CommandType . SET_NAME , [ name ] ) ) ; } param { number } percentage param { number } duration returns { Promise < IEventResult > } public adjust ( type , percentage = 0 , duration = 500 ) { return this . sendCommand ( new Command ( 1 , type , [ percentage , duration ] ) ) ; } /**
     * This method is used to just check if the connection is alive
     */ public ping ( ) { return this . sendCommand ( new Command ( 1 , CommandType . PING , [ ] ) ) . catch ( ( command ) => { const result = command . result ; if ( ! result || ! result . error || result . error . code !== - 1 || ! ( result . error . message + "" ) . match ( /not supported/i ) ) { throw command ; } } ) . then ( ( ) => { return null ; } ) ; } param { Command } command returns { Promise < IEventResult > } public sendCommand ( command ) { if ( ! this . connected ) { return Promise . reject ( "Connection is closed" ) ; } return new Promise ( ( resolve , reject ) => { const commandId = this . commandId ++ ; command . id = commandId ; this . sentCommands [ "" + commandId ] = command ; const timer = setTimeout ( ( ) => { this . removeAllListeners ( `${ this . EVENT_NAME }_${ command . id }` ) ; this . emit ( "commandTimedout" , command ) ; this . wasDisconnected ( ) ; reject ( "Command timedout, no response from server." ) ; } , this . options . timeout ) ; this . once ( `${ this . EVENT_NAME }_${ command . id }` , ( commandResult ) => { clearTimeout ( timer ) ; const result = commandResult . result ; if ( result . id && result . result ) { resolve ( commandResult ) ; } else if ( result . error ) { reject ( commandResult ) ; } } ) ; const msg = command . getString ( ) ; this . client . write ( msg + "\r\n" , ( ) => { this . emit ( command . method + "_sent" , command ) ; } ) ; } ) ; } private connectToIp ( host , port ) { return new Promise ( ( resolve , reject ) => { this . isConnecting = true ; this . isClosing = false ; if ( this . client ) { this . closeConnection ( ) ; } this . client = new Socket ( ) ; this . client . on ( "data" , this . onData . bind ( this ) ) ; this . client . on ( "connect" , ( ) => { this . wasConnected ( ) ; } ) ; this . client . on ( "close" , ( ) => { this . wasDisconnected ( ) ; } ) ; this . client . on ( "end" , ( ) => { this . wasDisconnected ( ) ; } ) ; this . client . on ( "error" , ( err ) => { this . emit ( "error" , err ) ; this . wasDisconnected ( ) ; } ) ; this . client . connect ( port || DEFAULT_PORT , host , ( ) => { this . isConnecting = false ; this . wasConnected ( ) ; resolve ( this ) ; } ) ; this . client . once ( "error" , ( err ) => { this . isConnecting = false ; reject ( err ) ; } ) ; setTimeout ( ( ) => { this . isConnecting = false ; reject ( "Connection timeout" ) ; } , this . connectTimeout ) ; } ) ; } private wasConnected ( ) { this . isReconnecting = false ; if ( ! this . connected ) { this . connected = true ; this . emit ( "connected" ) ; this . triggerPing ( ) ; } } private wasDisconnected ( ) { if ( this . connected ) { this . connected = false ; this . emit ( "disconnected" ) ; } this . _recoverNetworkError ( ) ; } private _recoverNetworkError ( ) { if ( this . autoReconnect && ! this . isClosing ) { this . isReconnecting = true ; if ( ! this . reconnectTimeout ) { this . reconnectTimeout = setTimeout ( ( ) => { this . reconnectTimeout = null ; if ( ! this . connected ) { if ( ! this . isConnecting ) { this . connect ( ) . catch ( ( err ) => { if ( ! err . toString ( ) . match ( /timeout/ ) ) { this . emit ( "error" , "Erorr during reconnect: " + err ) ; } this . _recoverNetworkError ( ) ; } ) ; } else { this . _recoverNetworkError ( ) ; } } } , this . autoReconnectTime ) ; } } } private triggerPing ( ) { if ( this . connected && ! this . disablePing ) { this . ping ( ) . then ( ( ) => { if ( ! this . pingTimeout ) { this . pingTimeout = setTimeout ( ( ) => { this . pingTimeout = null ; this . triggerPing ( ) ; } , this . connectTimeout ) ; } } ) . catch ( ( ) => { } ) ; } } private closeConnection ( ) { this . wasDisconnected ( ) ; if ( this . client ) { this . client . removeAllListeners ( ) ; this . client . destroy ( ) ; } } }