import { CharacteristicEventTypes } from 'homebridge' ; import type { Service , PlatformConfig , PlatformAccessory , CharacteristicValue , CharacteristicSetCallback , CharacteristicGetCallback , } from 'homebridge' ; import { clamp , convertHSLtoRGB , convertRGBtoHSL } from './magichome-interface/utils' ; import { HomebridgeMagichomeDynamicPlatform } from './platform' ; import { Transport } from './magichome-interface/Transport' ; import { getLogs } from './logs' ; import { MagicHomeAccessory , IDeviceProps } from './magichome-interface/types' ; const COMMAND_POWER_ON = [ 0x71 , 0x23 , 0x0f ] ; const COMMAND_POWER_OFF = [ 0x71 , 0x24 , 0x0f ] ; const animations = { none : { name : 'none' , brightnessInterrupt : true , hueSaturationInterrupt : true } , } ; const INTRA_MESSAGE_TIME = 20 ; /**
 * Platform Accessory
 * An instance of this class is created for each accessory your platform registers
 * Each accessory may expose multiple services of different service types.
 */ export class HomebridgeMagichomeDynamicPlatformAccessory { protected service : Service ; protected myDevice : IDeviceProps = this . accessory . context . device ; protected transport = new Transport ( this . myDevice . cachedIPAddress , this . config ) ; protected colorWhiteThreshold = this . config . whiteEffects . colorWhiteThreshold ; protected colorWhiteThresholdSimultaniousDevices = this . config . whiteEffects . colorWhiteThresholdSimultaniousDevices ; protected colorOffThresholdSimultaniousDevices = this . config . whiteEffects . colorOffThresholdSimultaniousDevices ; protected simultaniousDevicesColorWhite = this . config . whiteEffects . simultaniousDevicesColorWhite ; public activeAnimation = animations . none ; protected setColortemp = false ; protected colorCommand = false ; protected deviceWriteInProgress = false ; protected deviceWriteRetry : any = null ; protected deviceUpdateInProgress = false ; protected deviceReadInProgress = false ; logs = getLogs ( ) ; public lightStateTemporary = { HSL : { hue : 255 , saturation : 100 , luminance : 50 } , RGB : { red : 0 , green : 0 , blue : 0 } , whiteValues : { warmWhite : 0 , coldWhite : 0 } , isOn : true , brightness : 100 , CCT : 0 , } ; protected lightState = { HSL : { hue : 255 , saturation : 100 , luminance : 50 } , RGB : { red : 0 , green : 0 , blue : 0 } , whiteValues : { warmWhite : 0 , coldWhite : 0 } , isOn : true , brightness : 100 , CCT : 0 , } ; constructor ( protected readonly platform , protected readonly accessory , public readonly config ) { this . accessory . getService ( this . platform . Service . AccessoryInformation ) ! . setCharacteristic ( this . platform . Characteristic . Manufacturer , 'MagicHome' ) . setCharacteristic ( this . platform . Characteristic . SerialNumber , this . myDevice . uniqueId ) . setCharacteristic ( this . platform . Characteristic . Model , this . myDevice . modelNumber ) . getCharacteristic ( this . platform . Characteristic . Identify ) . removeAllListeners ( CharacteristicEventTypes . SET ) . removeAllListeners ( CharacteristicEventTypes . GET ) . on ( CharacteristicEventTypes . SET , this . identifyLight . bind ( this ) ) ; this . accessory . getService ( this . platform . Service . AccessoryInformation ) ! . addOptionalCharacteristic ( this . platform . Characteristic . ConfiguredName ) ; if ( this . myDevice . lightParameters . hasBrightness || this . myDevice . lightParameters . hasBrightness == undefined ) { if ( this . accessory . getService ( this . platform . Service . Switch ) ) { this . accessory . removeService ( this . accessory . getService ( this . platform . Service . Switch ) ) ; } this . service = this . accessory . getService ( this . platform . Service . Lightbulb ) ?? this . accessory . addService ( this . platform . Service . Lightbulb ) ; this . myDevice . lightParameters . hasBrightness = true ; this . service . getCharacteristic ( this . platform . Characteristic . ConfiguredName ) . removeAllListeners ( CharacteristicEventTypes . SET ) . removeAllListeners ( CharacteristicEventTypes . GET ) . on ( CharacteristicEventTypes . SET , this . setConfiguredName . bind ( this ) ) ; this . service . getCharacteristic ( this . platform . Characteristic . Brightness ) . removeAllListeners ( CharacteristicEventTypes . SET ) . removeAllListeners ( CharacteristicEventTypes . GET ) . on ( CharacteristicEventTypes . SET , this . setBrightness . bind ( this ) ) . on ( CharacteristicEventTypes . GET , this . getBrightness . bind ( this ) ) ; if ( this . myDevice . lightParameters . hasColor ) { this . service . getCharacteristic ( this . platform . Characteristic . Hue ) . removeAllListeners ( CharacteristicEventTypes . SET ) . removeAllListeners ( CharacteristicEventTypes . GET ) . on ( CharacteristicEventTypes . SET , this . setHue . bind ( this ) ) . on ( CharacteristicEventTypes . GET , this . getHue . bind ( this ) ) ; this . service . getCharacteristic ( this . platform . Characteristic . Saturation ) . removeAllListeners ( CharacteristicEventTypes . SET ) . removeAllListeners ( CharacteristicEventTypes . GET ) . on ( CharacteristicEventTypes . SET , this . setSaturation . bind ( this ) ) ; } if ( this . myDevice . lightParameters . hasCCT ) { this . service . getCharacteristic ( this . platform . Characteristic . ColorTemperature ) . removeAllListeners ( CharacteristicEventTypes . SET ) . removeAllListeners ( CharacteristicEventTypes . GET ) . on ( CharacteristicEventTypes . SET , this . setColorTemperature . bind ( this ) ) . on ( CharacteristicEventTypes . GET , this . getColorTemperature . bind ( this ) ) ; } } else { this . service = this . accessory . getService ( this . platform . Service . Switch ) ?? this . accessory . addService ( this . platform . Service . Switch ) ; this . service . getCharacteristic ( this . platform . Characteristic . ConfiguredName ) . removeAllListeners ( CharacteristicEventTypes . SET ) . removeAllListeners ( CharacteristicEventTypes . GET ) . on ( CharacteristicEventTypes . SET , this . setConfiguredName . bind ( this ) ) ; } this . service . getCharacteristic ( this . platform . Characteristic . On ) . removeAllListeners ( CharacteristicEventTypes . SET ) . removeAllListeners ( CharacteristicEventTypes . GET ) . on ( CharacteristicEventTypes . SET , this . setOn . bind ( this ) ) . on ( CharacteristicEventTypes . GET , this . getOn . bind ( this ) ) ; this . updateLocalState ( ) ; this . service . setCharacteristic ( this . platform . Characteristic . Name , this . myDevice . displayName ) ; } setConfiguredName ( value , callback ) { const name = value . toString ( ) ; this . logs . debug ( 'Renaming device to %o' , name ) ; this . myDevice . displayName = name ; this . platform . api . updatePlatformAccessories ( [ this . accessory ] ) ; callback ( null ) ; } identifyLight ( ) { this . logs . info ( 'Identifying accessory: %o!' , this . myDevice . displayName ) ; this . flashEffect ( ) ; } setHue ( value , callback ) { this . setColortemp = false ; this . lightState . HSL . hue = value as number ; this . colorCommand = true ; this . processRequest ( ) ; callback ( null ) ; } setSaturation ( value , callback ) { this . setColortemp = false ; this . lightState . HSL . saturation = value as number ; this . colorCommand = true ; this . processRequest ( ) ; callback ( null ) ; } setBrightness ( value , callback ) { this . lightState . brightness = value as number ; this . colorCommand = true ; this . processRequest ( ) ; callback ( null ) ; } setColorTemperature ( value , callback ) { this . setColortemp = true ; this . lightState . CCT = value as number ; this . colorCommand = true ; this . processRequest ( ) ; callback ( null ) ; } setOn ( value , callback ) { this . lightState . isOn = value as boolean ; this . processRequest ( ) ; callback ( null ) ; } getHue ( callback ) { const hue = this . lightState . HSL . hue ; this . logs . debug ( 'Get Characteristic Hue -> %o for device: %o ' , hue , this . myDevice . displayName ) ; if ( ! this . setColortemp ) { this . updateLocalState ( ) ; } callback ( null , hue ) ; } getColorTemperature ( callback ) { const CCT = this . lightState . CCT ; this . logs . debug ( 'Get Characteristic Hue -> %o for device: %o ' , CCT , this . myDevice . displayName ) ; if ( this . setColortemp ) { this . updateLocalState ( ) ; } callback ( null , CCT ) ; } getBrightness ( callback ) { const brightness = this . lightState . brightness ; this . logs . debug ( 'Get Characteristic Brightness -> %o for device: %o ' , brightness , this . myDevice . displayName ) ; this . updateLocalState ( ) ; callback ( null , brightness ) ; } /**
     ** @getOn
     * instantly retrieve the current on/off state stored in our object
     * next call this.getState() which will update all values asynchronously as they are ready
     */ getOn ( callback ) { const isOn = this . lightState . isOn ; this . updateLocalState ( ) ; this . logs . debug ( 'Get Characteristic On -> %o for device: %o ' , isOn , this . myDevice . displayName ) ; callback ( null , isOn ) ; } /**
     ** @updateLocalState
     * retrieve light's state object from transport class
     * once values are available, update homekit with actual values
     */ async updateLocalState ( ) { if ( this . deviceWriteInProgress || this . deviceUpdateInProgress || this . deviceReadInProgress ) { return ; } this . deviceReadInProgress = true ; try { let state ; let scans = 0 ; while ( state == null && scans <= 5 ) { state = await this . transport . getState ( 1000 ) ; scans ++ ; } if ( state == null ) { const { ipAddress , uniqueId , displayName } = this . myDevice ; this . logs . debug ( `No response from device '${ displayName }' (${ uniqueId }) ${ ipAddress }` ) ; this . deviceReadInProgress = false ; return ; } this . myDevice . lastKnownState = state ; this . updateLocalRGB ( state . RGB ) ; this . updateLocalHSL ( convertRGBtoHSL ( this . lightState . RGB ) ) ; this . updateLocalWhiteValues ( state . whiteValues ) ; this . updateLocalIsOn ( state . isOn ) ; this . updateHomekitState ( ) ; } catch ( error ) { this . logs . error ( 'getState() error: ' , error ) ; } this . deviceReadInProgress = false ; } /**
     ** @updateHomekitState
     * send state to homekit
     */ async updateHomekitState ( ) { this . service . updateCharacteristic ( this . platform . Characteristic . On , this . lightState . isOn ) ; this . service . updateCharacteristic ( this . platform . Characteristic . Hue , this . lightState . HSL . hue ) ; this . service . updateCharacteristic ( this . platform . Characteristic . Saturation , this . lightState . HSL . saturation ) ; if ( this . lightState . HSL . luminance > 0 && this . lightState . isOn ) { this . updateLocalBrightness ( this . lightState . HSL . luminance * 2 ) ; } this . service . updateCharacteristic ( this . platform . Characteristic . Brightness , this . lightState . brightness ) ; } updateLocalHSL ( _hsl ) { this . lightState . HSL = _hsl ; } updateLocalRGB ( _rgb ) { this . lightState . RGB = _rgb ; } updateLocalWhiteValues ( _whiteValues ) { this . lightState . whiteValues = _whiteValues ; } updateLocalIsOn ( _isOn ) { this . lightState . isOn = _isOn ; } updateLocalBrightness ( _brightness ) { this . lightState . brightness = _brightness ; } /**
     ** @updateDeviceState
     *  determine RGB and warmWhite/coldWhite values  from homekit's HSL
     *  perform different logic based on light's capabilities, detimined by "this.myDevice.lightVersion"
     *
     */ async updateDeviceState ( _timeout = 200 ) { const hsl = this . lightState . HSL ; const [ red , green , blue ] = convertHSLtoRGB ( hsl ) ; const brightness = this . lightState . brightness ; const mask = 0xF0 ; const r = Math . round ( ( ( clamp ( red , 0 , 255 ) / 100 ) * brightness ) ) ; const g = Math . round ( ( ( clamp ( green , 0 , 255 ) / 100 ) * brightness ) ) ; const b = Math . round ( ( ( clamp ( blue , 0 , 255 ) / 100 ) * brightness ) ) ; await this . send ( [ 0x31 , r , g , b , 0x00 , mask , 0x0F ] , true , _timeout ) ; } /**
     ** @calculateWhiteColor
     *  determine warmWhite/coldWhite values from hue
     *  the closer to 0/360 the weaker coldWhite brightness becomes
     *  the closer to 180 the weaker warmWhite brightness becomes
     *  the closer to 90/270 the stronger both warmWhite and coldWhite become simultaniously
     */ hueToWhiteTemperature ( ) { const hsl = this . lightState . HSL ; let multiplier = 0 ; const whiteTemperature = { warmWhite : 0 , coldWhite : 0 } ; if ( hsl . hue <= 90 ) { whiteTemperature . warmWhite = 255 ; multiplier = ( ( hsl . hue / 90 ) ) ; whiteTemperature . coldWhite = Math . round ( ( 255 * multiplier ) ) ; } else if ( hsl . hue > 270 ) { whiteTemperature . warmWhite = 255 ; multiplier = ( 1 - ( hsl . hue - 270 ) / 90 ) ; whiteTemperature . coldWhite = Math . round ( ( 255 * multiplier ) ) ; } else if ( hsl . hue > 180 && hsl . hue <= 270 ) { whiteTemperature . coldWhite = 255 ; multiplier = ( ( hsl . hue - 180 ) / 90 ) ; whiteTemperature . warmWhite = Math . round ( ( 255 * multiplier ) ) ; } else if ( hsl . hue > 90 && hsl . hue <= 180 ) { whiteTemperature . coldWhite = 255 ; multiplier = ( 1 - ( hsl . hue - 90 ) / 90 ) ; whiteTemperature . warmWhite = Math . round ( ( 255 * multiplier ) ) ; } this . lightState . whiteValues = whiteTemperature ; return whiteTemperature ; } cctToWhiteTemperature ( ) { const CCT = this . lightState . CCT - 140 ; let multiplier = 0 ; const whiteTemperature = { warmWhite : 0 , coldWhite : 0 } ; const threshold = 110 ; if ( CCT >= threshold ) { whiteTemperature . warmWhite = 127 ; multiplier = ( 1 - ( ( CCT - threshold ) / ( 360 - threshold ) ) ) ; whiteTemperature . coldWhite = Math . round ( ( 127 * multiplier ) ) ; } else { whiteTemperature . coldWhite = 127 ; multiplier = ( CCT / threshold ) ; whiteTemperature . warmWhite = Math . round ( ( 127 * multiplier ) ) ; } this . lightState . whiteValues = whiteTemperature ; return whiteTemperature ; } async send ( command , useChecksum = true , _timeout = 200 ) { const buffer = Buffer . from ( command ) ; const output = await this . transport . send ( buffer , useChecksum , _timeout ) ; } cacheCurrentLightState ( ) { this . lightStateTemporary . HSL = this . lightState . HSL ; } async restoreCachedLightState ( ) { this . lightState . HSL = this . lightStateTemporary . HSL ; this . updateDeviceState ( ) ; } flashEffect ( ) { this . lightState . HSL . hue = 100 as number ; this . lightState . HSL . saturation = 100 as number ; let change = true ; let count = 0 ; const interval = setInterval ( ( ) => { if ( change ) { this . lightState . brightness = 0 ; } else { this . lightState . brightness = 100 ; } change = ! change ; count ++ ; this . updateDeviceState ( ) ; if ( count >= 20 ) { this . lightState . HSL . hue = 0 ; this . lightState . HSL . saturation = 5 ; this . lightState . brightness = 100 ; this . updateDeviceState ( ) ; clearInterval ( interval ) ; return ; } } , 300 ) ; } async stopAnimation ( ) { this . activeAnimation = animations . none ; } protected myTimer = null ; protected timestamps = [ ] ; protected timeOfLastRead = null ; protected timeOfLastWrite = null ; processRequest ( ) { if ( ! this . deviceUpdateInProgress ) { this . deviceUpdateInProgress = true ; setTimeout ( async ( ) => { if ( ( ! this . colorCommand ) || ! this . lightState . isOn ) { await this . send ( this . lightState . isOn ? COMMAND_POWER_ON : COMMAND_POWER_OFF ) ; } else { if ( ( this . myDevice . controllerFirmwareVersion <= 5 && this . myDevice . controllerFirmwareVersion > 1 ) || this . myDevice . controllerFirmwareVersion == 8 || ( this . myDevice . controllerFirmwareVersion == 1 && this . myDevice . modelNumber . includes ( 'HF-LPB100-ZJ200' ) ) ) { await this . send ( COMMAND_POWER_ON ) ; } setTimeout ( async ( ) => { await this . updateDeviceState ( ) ; } , 100 ) ; } this . colorCommand = false ; this . deviceUpdateInProgress = false ; } , INTRA_MESSAGE_TIME ) ; } return ; } /**
     *  This is a debug function to show the number of listeners for each .on event.
     */ logListeners ( ) { this . logs . warn ( 'On set Listener count: ' , this . service . getCharacteristic ( this . platform . Characteristic . On ) . listenerCount ( 'set' ) ) ; this . logs . warn ( 'Identify set Listener count: ' , this . service . getCharacteristic ( this . platform . Characteristic . Identify ) . listenerCount ( 'set' ) ) ; this . logs . warn ( 'Name set Listener count: ' , this . service . getCharacteristic ( this . platform . Characteristic . ConfiguredName ) . listenerCount ( 'set' ) ) ; this . logs . warn ( 'Brightness set Listener count: ' , this . service . getCharacteristic ( this . platform . Characteristic . Brightness ) . listenerCount ( 'set' ) ) ; this . logs . warn ( 'Hue set Listener count: ' , this . service . getCharacteristic ( this . platform . Characteristic . Hue ) . listenerCount ( 'set' ) ) ; this . logs . warn ( 'Sat set Listener count: ' , this . service . getCharacteristic ( this . platform . Characteristic . Saturation ) . listenerCount ( 'set' ) ) ; this . logs . warn ( 'Manufacturer set: Listener count: ' , this . service . setCharacteristic ( this . platform . Characteristic . Manufacturer , null ) . listenerCount ( 'set' ) ) ; this . logs . warn ( 'On get Listener count: ' , this . service . getCharacteristic ( this . platform . Characteristic . On ) . listenerCount ( 'get' ) ) ; this . logs . warn ( 'Identify get Listener count: ' , this . service . getCharacteristic ( this . platform . Characteristic . Identify ) . listenerCount ( 'get' ) ) ; this . logs . warn ( 'Name get Listener count: ' , this . service . getCharacteristic ( this . platform . Characteristic . ConfiguredName ) . listenerCount ( 'get' ) ) ; this . logs . warn ( 'Brightness get Listener count: ' , this . service . getCharacteristic ( this . platform . Characteristic . Brightness ) . listenerCount ( 'get' ) ) ; this . logs . warn ( 'Hue get Listener count: ' , this . service . getCharacteristic ( this . platform . Characteristic . Hue ) . listenerCount ( 'get' ) ) ; this . logs . warn ( 'Sat get Listener count: ' , this . service . getCharacteristic ( this . platform . Characteristic . Saturation ) . listenerCount ( 'get' ) ) ; this . logs . warn ( 'Manufacturer get: Listener count: ' , this . service . setCharacteristic ( this . platform . Characteristic . Manufacturer , null ) . listenerCount ( 'get' ) ) ; } }