import { APIEvent } from 'homebridge' ; import type { API , DynamicPlatformPlugin , Logger , PlatformAccessory , PlatformConfig } from 'homebridge' ; import { PLATFORM_NAME , PLUGIN_NAME } from './settings' ; import { join } from 'path' ; import { loadJson } from './magichome-interface/utils' ; import { Switch } from './accessories/Switch' ; import { DimmerStrip } from './accessories/DimmerStrip' ; import { RGBStrip } from './accessories/RGBStrip' ; import { GRBStrip } from './accessories/GRBStrip' ; import { RGBWBulb } from './accessories/RGBWBulb' ; import { RGBWWBulb } from './accessories/RGBWWBulb' ; import { RGBWStrip } from './accessories/RGBWStrip' ; import { RGBWWStrip } from './accessories/RGBWWStrip' ; import { CCTStrip } from './accessories/CCTStrip' ; import { cloneDeep } from 'lodash' ; import { Logs } from './logs' ; import { Discover } from './magichome-interface/Discover' ; import { Transport } from './magichome-interface/Transport' ; import { HomebridgeMagichomeDynamicPlatformAccessory } from './platformAccessory' ; import { IDeviceProps , IDeviceDiscoveredProps , IDeviceQueriedProps , ILightParameters } from './magichome-interface/types' ; import { getPrettyName as getUniqueIdName , lightTypesMap } from './magichome-interface/LightMap' ; import { MagicHomeAccessory , ControllerTypes } from './magichome-interface/types' ; const accessoryType = { Switch , DimmerStrip , GRBStrip , RGBStrip , RGBWBulb , RGBWWBulb , RGBWStrip , RGBWWStrip , CCTStrip , } ; /**
 * HomebridgePlatform
 * This class is the main constructor for your plugin, this is where you should
 * parse the user config and discover/register accessories with Homebridge.
 */ export class HomebridgeMagichomeDynamicPlatform implements DynamicPlatformPlugin { public readonly Service = this . api . hap . Service ; public readonly Characteristic = this . api . hap . Characteristic ; public readonly accessories : MagicHomeAccessory [ ] = [ ] ; public count = 1 ; private periodicDiscovery : NodeJS . Timeout | null = null ; private logs : Logs ; constructor ( public readonly hbLogger , public readonly config , public readonly api ) { if ( this . config . advancedOptions . logLevel ) { this . logs = new Logs ( hbLogger , this . config . advancedOptions . logLevel ) ; } else { this . logs = new Logs ( hbLogger ) ; } this . logs . warn ( 'Finished initializing homebridge-magichome-dynamic-platform %o' , loadJson < any > ( join ( __dirname , '../package.json' ) , { } ) . version ) ; this . logs . info ( 'If this plugin brings you joy, consider visiting GitHub and giving it a ⭐.' ) ; this . api . on ( APIEvent . DID_FINISH_LAUNCHING , ( ) => { this . logs . debug ( 'Executed didFinishLaunching callback' ) ; this . count = 1 ; this . discoverDevices ( true ) ; const shouldRediscover = this . config . advancedOptions ?. periodicDiscovery ?? false ; if ( shouldRediscover ) { this . periodicDiscovery = setInterval ( ( ) => this . discoverDevices ( false ) , 30000 ) ; } } ) ; } /**
     * This function is invoked when homebridge restores cached accessories from disk at startup.
     * It should be used to setup event handlers for characteristics and update respective values.
     */ configureAccessory ( accessory ) { this . logs . debug ( '%o - Loading accessory from cache...' , this . count ++ , accessory . context . device . displayName ) ; accessory . context . device . restartsSinceSeen ++ ; accessory . context . pendingRegistration = true ; this . accessories . push ( accessory ) ; } /**
     * Accessories are added by one of three Methods:
     * Method One: New devices that were seen after scanning the network and are registered for the first time
     * Method Two: Cached devices that were seen after scanning the network and are added while checking for ip discrepancies
     * Method Three: Cached devices that were not seen after scanning the network but are still added with a warning to the user
     */ async discoverDevices ( dgb ) { const { isValidDeviceModel } = HomebridgeMagichomeDynamicPlatform ; const pendingUpdate = new Set ( ) ; const recentlyRegisteredDevices = new Set ( ) ; let registeredDevices = 0 , newDevices = 0 , unseenDevices = 0 , scans = 0 ; let discover = new Discover ( this . logs , this . config ) ; let devicesDiscovered = await discover . scan ( 2000 ) ; while ( devicesDiscovered . length === 0 && scans < 5 ) { this . logs . debug ( '( Scan: %o ) Discovered zero devices... rescanning...' , scans + 1 ) ; devicesDiscovered = await discover . scan ( 2000 ) ; scans ++ ; } discover = null ; if ( devicesDiscovered . length === 0 ) { this . logs . debug ( '\nDiscovered zero devices!\n' ) ; } else { this . logs . debug ( '\nDiscovered %o devices.\n' , devicesDiscovered . length ) ; } for ( const deviceDiscovered of devicesDiscovered ) { let existingAccessory = null ; try { const generatedUUID = this . api . hap . uuid . generate ( deviceDiscovered . uniqueId ) ; existingAccessory = this . accessories . find ( accessory => accessory . UUID === generatedUUID ) ; if ( ! existingAccessory ) { if ( ! this . createNewAccessory ( deviceDiscovered , generatedUUID ) ) { continue ; } recentlyRegisteredDevices . add ( deviceDiscovered . uniqueId ) ; registeredDevices ++ ; newDevices ++ ; } else { if ( ! isValidDeviceModel ( existingAccessory . context . device , null ) ) { this . logs . debug ( `The known device "${ deviceDiscovered . uniqueId }" seen during discovery has outdated data model (pre v1.8.6). Rebuilding device. ` , deviceDiscovered ) ; const initialState = await this . getInitialState ( deviceDiscovered . ipAddress , 10000 ) ; const deviceQueryData = await this . determineController ( deviceDiscovered ) ; if ( ! initialState || ! deviceQueryData ) { this . logs . error ( 'Warning! Device type could not be determined for device: %o, this is usually due to an unresponsive device.\n Please restart homebridge. If the problem persists, ensure the device works in the "Magichome Pro" app.\n file an issue on github with an uploaded log\n' , deviceDiscovered . uniqueId ) ; continue ; } const oldName = existingAccessory . context . displayName || existingAccessory . context . device ?. lightParameters ?. convenientName || deviceQueryData . lightParameters . convenientName ; const rootProps = { UUID : generatedUUID , cachedIPAddress : deviceDiscovered . ipAddress , restartsSinceSeen : 0 , displayName : oldName , lastKnownState : initialState } ; const deviceData = Object . assign ( rootProps , deviceDiscovered , deviceQueryData ) ; existingAccessory . context . device = deviceData ; if ( isValidDeviceModel ( existingAccessory . context . device , this . logs ) ) { this . logs . debug ( `[discovered+cached] Device "${ deviceDiscovered . uniqueId }" successfully repaired!` ) ; } else { this . logs . error ( `[discovered+cached] Device "${ deviceDiscovered . uniqueId }" was not repaired successfully. Ensure it can be controlled in the MagicHome app then restart homebridge to try again while it is online.` ) ; continue ; } } if ( ! this . registerExistingAccessory ( deviceDiscovered , existingAccessory ) ) { continue ; } recentlyRegisteredDevices . add ( deviceDiscovered . uniqueId ) ; registeredDevices ++ ; } } catch ( error ) { this . logs . debug ( '[discovered+cached] platform.ts discoverDevices() accessory creation has thrown the following error: %o' , error ) ; this . logs . debug ( '[discovered+cached] The existingAccessory object is: ' , existingAccessory ) ; this . logs . debug ( '[discovered+cached] The deviceDiscovered object is: ' , deviceDiscovered ) ; } } for ( const accessory of this . accessories ) { try { if ( ! isValidDeviceModel ( accessory . context . device , null ) ) { const { uniqueId } = accessory . context . device ; this . logs . debug ( `Device "${ uniqueId }" was not seen during discovery. Ensure it can be controlled in the MagicHome app. Rescan in 30 seconds...` ) ; pendingUpdate . add ( uniqueId ) ; continue ; } if ( accessory . context . device ?. displayName && accessory . context . device . displayName . toString ( ) . toLowerCase ( ) . includes ( 'delete' ) ) { this . api . unregisterPlatformAccessories ( PLUGIN_NAME , PLATFORM_NAME , [ accessory ] ) ; this . logs . warn ( 'Successfully pruned accessory: ' , accessory . context . device . displayName , 'due to being marked for deletion\n' ) ; continue ; } else if ( this . config . pruning . pruneMissingCachedAccessories || this . config . pruning . pruneAllAccessoriesNextRestart ) { if ( accessory . context . device . restartsSinceSeen >= this . config . pruning . restartsBeforeMissingAccessoriesPruned || this . config . pruning . pruneAllAccessoriesNextRestart ) { this . api . unregisterPlatformAccessories ( PLUGIN_NAME , PLATFORM_NAME , [ accessory ] ) ; this . logs . warn ( 'Successfully pruned accessory:' , accessory . context . device . displayName , 'which had not being seen for (' , accessory . context . device . restartsSinceSeen , ') restart(s).\n' ) ; continue ; } } if ( accessory . context . device . restartsSinceSeen > 0 ) { if ( ! this . isAllowed ( accessory . context . device . uniqueId ) ) { this . logs . warn ( 'Warning! Accessory: %o will be pruned as its Unique ID: %o is blacklisted or is not whitelisted.\n' , accessory . context . device . displayName , accessory . context . device . uniqueId ) ; try { this . api . unregisterPlatformAccessories ( PLUGIN_NAME , PLATFORM_NAME , [ accessory ] ) ; } catch ( err ) { this . logs . debug ( 'Accessory: %o count not be pruned. Likely it had never been registered.\n' , accessory . context . device . displayName , accessory . context . device . uniqueId ) ; } continue ; } this . logs . debug ( `Warning! Continuing to register cached accessory "${ accessory . context . device . uniqueId }" despite not being seen for ${ accessory . context . device . restartsSinceSeen } restarts.` ) ; let lightAccessory = null ; try { lightAccessory = new accessoryType [ accessory . context . device . lightParameters . controllerLogicType ] ( this , accessory , this . config ) ; } catch ( error ) { this . logs . error ( '[1] The controllerLogicType does not exist in accessoryType list. Did you migrate this? controllerLogicType=' , accessory . context . device ?. lightParameters ?. controllerLogicType ) ; this . logs . error ( 'device object: ' , accessory . context . device ) ; continue ; } this . api . updatePlatformAccessories ( [ accessory ] ) ; registeredDevices ++ ; unseenDevices ++ ; } } catch ( error ) { this . logs . error ( 'platform.ts discoverDevices() accessory pruning has thrown the following error: %o' , error ) ; this . logs . error ( 'The context object is: ' , accessory . context ) ; } } this . logs . debug ( '\nRegistered %o Magichome device(s). \nNew devices: %o \nCached devices that were seen this restart: %o' + '\nCached devices that were not seen this restart: %o\n' , registeredDevices , newDevices , registeredDevices - newDevices - unseenDevices , unseenDevices ) ; if ( recentlyRegisteredDevices . size > 0 ) { const found = recentlyRegisteredDevices . size ; const pending = Array . from ( pendingUpdate ) . length ; const pendingStr = pending > 0 ? ` Pending update: ${ pending } devices` : '' ; this . logs . debug ( `Discovery summary:  Found ${ found } devices.${ pendingStr }` ) ; } this . count = 1 ; } isAllowed ( uniqueId ) { const blacklistedUniqueIDs = this . config . deviceManagement . blacklistedUniqueIDs ; let isAllowed = true ; try { if ( blacklistedUniqueIDs !== undefined && this . config . deviceManagement . blacklistOrWhitelist !== undefined ) { if ( ( ( blacklistedUniqueIDs ) . includes ( uniqueId ) && ( this . config . deviceManagement . blacklistOrWhitelist ) . includes ( 'blacklist' ) ) || ( ! ( blacklistedUniqueIDs ) . includes ( uniqueId ) ) && ( this . config . deviceManagement . blacklistOrWhitelist ) . includes ( 'whitelist' ) ) { isAllowed = false ; } } } catch ( error ) { this . logs . debug ( error ) ; } return isAllowed ; } async getInitialState ( ipAddress , _timeout = 500 ) { const transport = new Transport ( ipAddress , this . config ) ; try { let scans = 0 , data ; while ( data == null && scans < 5 ) { data = await transport . getState ( _timeout ) ; scans ++ ; } return data ; } catch ( error ) { this . logs . debug ( error ) ; } } async determineController ( discoveredDevice ) { const { ipAddress } = discoveredDevice || { } ; if ( typeof ipAddress !== 'string' ) { this . logs . error ( 'Cannot determine controller because invalid IP address. Device:' , discoveredDevice ) ; return null ; } const initialState = await this . getInitialState ( ipAddress , 10000 ) ; if ( initialState == undefined ) { this . logs . debug ( 'Cannot determine controller. Device unreacheable.' , discoveredDevice ) ; return null ; } let lightParameters ; const controllerHardwareVersion = initialState . controllerHardwareVersion ; const controllerFirmwareVersion = initialState . controllerFirmwareVersion ; this . logs . debug ( 'Attempting to assign controller to new device: UniqueId: %o \nIpAddress %o \nModel: %o\nHardware Version: %o \nDevice Type: %o\n' , discoveredDevice . uniqueId , discoveredDevice . ipAddress , discoveredDevice . modelNumber , initialState . controllerHardwareVersion . toString ( 16 ) , initialState . controllerFirmwareVersion . toString ( 16 ) ) ; if ( lightTypesMap . has ( controllerHardwareVersion ) ) { this . logs . debug ( 'Device %o: Hardware Version: %o with Firmware Version: %o matches known device type records' , discoveredDevice . uniqueId , controllerHardwareVersion . toString ( 16 ) , controllerFirmwareVersion . toString ( 16 ) ) ; lightParameters = lightTypesMap . get ( controllerHardwareVersion ) ; } else { this . logs . warn ( 'Unknown device version number: %o... unable to create accessory.' , controllerHardwareVersion . toString ( 16 ) ) ; this . logs . warn ( 'Please create an issue at https://github.com/Zacknetic/HomebridgeMagicHome-DynamicPlatform/issues and upload your homebridge.log' ) ; return null ; } this . logs . debug ( 'Controller Logic Type assigned to %o' , lightParameters . controllerLogicType ) ; return { lightParameters , controllerHardwareVersion : controllerHardwareVersion , controllerFirmwareVersion : controllerFirmwareVersion , } ; } param deviceDiscovered param generatedUUID async createNewAccessory ( deviceDiscovered , generatedUUID ) { const unsupportedModels = [ '000-0000' ] ; const deviceQueryData = await this . determineController ( deviceDiscovered ) ; if ( deviceQueryData == null ) { if ( unsupportedModels . includes ( deviceDiscovered . modelNumber ) ) { this . logs . warn ( 'Warning! Discovered device did not respond to query. Device is in the unsupported device list.\nFile an issue on github requesting support. Details:' , deviceDiscovered ) ; } else { this . logs . warn ( 'Warning! Discovered device did not respond to query. This is usually due to an unresponsive device.\nPlease restart homebridge. If the problem persists, ensure the device works in the "Magichome Pro" app.\nFile an issue on github with an uploaded log.' , deviceDiscovered ) ; } return false ; } if ( ! this . isAllowed ( deviceDiscovered . uniqueId ) ) { this . logs . warn ( 'Warning! New device with Unique ID: %o is blacklisted or is not whitelisted.\n' , deviceDiscovered . uniqueId ) ; return false ; } if ( this . config . advancedOptions && this . config . advancedOptions . namesWithMacAddress ) { const uniqueIdName = getUniqueIdName ( deviceDiscovered . uniqueId , deviceQueryData . lightParameters . controllerLogicType ) ; deviceQueryData . lightParameters . convenientName = uniqueIdName ; } const accessory = new this . api . platformAccessory ( deviceQueryData . lightParameters . convenientName , generatedUUID ) as MagicHomeAccessory ; const deviceData = Object . assign ( { UUID : generatedUUID , cachedIPAddress : deviceDiscovered . ipAddress , restartsSinceSeen : 0 , displayName : deviceQueryData . lightParameters . convenientName } , deviceDiscovered , deviceQueryData ) ; accessory . context . device = deviceData ; this . printDeviceInfo ( 'Registering new accessory...!' , accessory ) ; this . api . registerPlatformAccessories ( PLUGIN_NAME , PLATFORM_NAME , [ accessory ] ) ; let lightAccessory = null ; try { lightAccessory = new accessoryType [ accessory . context . device . lightParameters . controllerLogicType ] ( this , accessory , this . config ) ; } catch ( error ) { this . logs . error ( '[2] The controllerLogicType does not exist in accessoryType list. controllerLogicType=' , accessory . context . device ?. lightParameters ?. controllerLogicType ) ; this . logs . error ( 'device object: ' , accessory . context . device ) ; return false ; } this . accessories . push ( accessory ) ; return true ; } param deviceDiscovered param existingAccessory registerExistingAccessory ( deviceDiscovered , existingAccessory ) { if ( ! this . isAllowed ( existingAccessory . context . device . uniqueId ) ) { this . logs . warn ( 'Warning! Accessory: %o will be pruned as its Unique ID: %o is blacklisted or is not whitelisted.\n' , existingAccessory . context . device . displayName , existingAccessory . context . device . uniqueId ) ; this . api . unregisterPlatformAccessories ( PLUGIN_NAME , PLATFORM_NAME , [ existingAccessory ] ) ; return false ; } existingAccessory . context . device . restartsSinceSeen = 0 ; const ipHasNotChanged = existingAccessory . context . device . cachedIPAddress === deviceDiscovered . ipAddress ; const { pendingRegistration } = existingAccessory . context ; const registrationComplete = ! pendingRegistration ; if ( registrationComplete && ipHasNotChanged ) { this . logs . debug ( `Device ${ existingAccessory . context . device . uniqueId } already registered. Registration update not required` ) ; return false ; } if ( ! ipHasNotChanged ) { this . logs . warn ( 'Ip address discrepancy found for accessory: %o\n Expected ip address: %o\n Discovered ip address: %o' , existingAccessory . context . device . displayName , existingAccessory . context . device . cachedIPAddress , deviceDiscovered . ipAddress ) ; existingAccessory . context . device . cachedIPAddress = deviceDiscovered . ipAddress ; this . logs . warn ( 'Ip address successfully reassigned to: %o\n ' , existingAccessory . context . device . cachedIPAddress ) ; } this . printDeviceInfo ( 'Registering existing accessory: ' , existingAccessory ) ; let lightAccessory = null ; try { if ( ! existingAccessory . context ?. device ?. lightParameters ?. controllerLogicType || accessoryType [ existingAccessory . context ?. device ?. lightParameters ?. controllerLogicType ] === undefined ) { this . logs . error ( '[registerExistingAccessory] The controllerLogicType does not exist in accessoryType list. controllerLogicType=' , existingAccessory . context . device ?. lightParameters ?. controllerLogicType ) ; this . logs . error ( '[registerExistingAccessory] device object: ' , existingAccessory . context . device ) ; return false ; } lightAccessory = new accessoryType [ existingAccessory . context . device . lightParameters . controllerLogicType ] ( this , existingAccessory , this . config ) ; } catch ( error ) { this . logs . error ( '[registerExistingAccessory] The controllerLogicType does not exist in accessoryType list. controllerLogicType=' , existingAccessory . context . device ?. lightParameters ?. controllerLogicType ) ; this . logs . error ( '[registerExistingAccessory] device object: ' , existingAccessory . context . device ) ; this . logs . error ( error ) ; return false ; } this . api . updatePlatformAccessories ( [ existingAccessory ] ) ; existingAccessory . context . pendingRegistration = false ; return true ; } printDeviceInfo ( message , accessory ) { const device = accessory . context . device ; this . logs . info ( `${ this . count ++ } - ${ message }[ ${ device . displayName } ]` ) ; this . logs . debug ( `
    Controller Logic Type: ${ device . lightParameters ?. controllerLogicType }
    Model: ${ device . modelNumber }
    Unique ID: ${ device . uniqueId } 
    IP-Address: ${ device . ipAddress } 
    Hardware Version: ${ device . controllerHardwareVersion ?. toString ( 16 ) }
    Firmware Version: ${ device . controllerHardwareVersion ?. toString ( 16 ) }\n` ) ; } async send ( transport , command , useChecksum = true , _timeout = 200 ) { const buffer = Buffer . from ( command ) ; const output = await transport . send ( buffer , useChecksum , _timeout ) ; this . logs . debug ( 'Recived the following response' , output ) ; } static isValidDeviceModel ( _device , logger ) { const device = cloneDeep ( _device ) ; try { const { lightParameters } = device || { } ; const rootProps = [ 'UUID' , 'cachedIPAddress' , 'restartsSinceSeen' , 'displayName' , 'ipAddress' , 'uniqueId' , 'modelNumber' , 'lightParameters' , 'controllerHardwareVersion' , 'controllerFirmwareVersion' ] ; const lightProps = [ 'controllerLogicType' , 'convenientName' , 'simultaneousCCT' , 'hasColor' , 'hasBrightness' ] ; const missingRootProps = rootProps . filter ( k => device [ k ] === undefined || device [ k ] == null ) ; const missingLightProps = lightProps . filter ( k => lightParameters [ k ] === undefined || lightParameters [ k ] == null ) ; const missingProps = [ ... missingRootProps , ... missingLightProps ] ; if ( device . lastKnownState === undefined ) { missingProps . push ( 'lastKnownState' ) ; } if ( ! Object . values ( ControllerTypes ) . includes ( lightParameters . controllerLogicType ) ) { if ( logger ) { logger . error ( `[isValidDeviceModel] The ContollerLogicType "${ lightParameters . controllerLogicType }" is unknown.` ) ; } return false ; } if ( missingProps . length > 0 ) { if ( logger ) { logger . error ( '[isValidDeviceModel] unable to validate device model. Missing properties: ' , missingProps ) ; logger . debug ( '\nThree things are certain:\nDeath, taxes and lost data.\nGuess which has occurred.' ) ; } return false ; } return true ; } catch ( err ) { return false ; } } }