UNPKG

@switchbot/homebridge-switchbot

Version:

The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.

1,001 lines (916 loc) • 166 kB
/* Copyright(C) 2017-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. * * platform.ts: @switchbot/homebridge-switchbot platform class. */ import type { Server } from 'node:http' import type { API, DynamicPlatformPlugin, Logging, PlatformAccessory } from 'homebridge' import type { MqttClient } from 'mqtt' /* * For Testing Locally: * import type { blindTilt, curtain, curtain3, device, irdevice } from '/Users/Shared/GitHub/OpenWonderLabs/node-switchbot/dist/index.js'; * import { LogLevel, SwitchBotBLE, SwitchBotModel, SwitchBotOpenAPI } from '/Users/Shared/GitHub/OpenWonderLabs/node-switchbot/dist/index.js'; */ import type { blindTilt, bodyChange, curtain, curtain3, device, deviceStatusRequest, irdevice } from 'node-switchbot' import type { blindTiltConfig, curtainConfig, devicesConfig, irDevicesConfig, options, SwitchBotPlatformConfig } from './settings.js' import { readFileSync } from 'node:fs' import { argv } from 'node:process' import asyncmqtt from 'async-mqtt' import fakegato from 'fakegato-history' import { EveHomeKitTypes } from 'homebridge-lib/EveHomeKitTypes' import { LogLevel, SwitchBotBLE, SwitchBotModel, SwitchBotOpenAPI } from 'node-switchbot' import { queueScheduler } from 'rxjs' import { BlindTilt } from './device/blindtilt.js' import { Bot } from './device/bot.js' import { CeilingLight } from './device/ceilinglight.js' import { ColorBulb } from './device/colorbulb.js' import { Contact } from './device/contact.js' import { Curtain } from './device/curtain.js' import { Fan } from './device/fan.js' import { Hub } from './device/hub.js' import { Humidifier } from './device/humidifier.js' import { IOSensor } from './device/iosensor.js' import { StripLight } from './device/lightstrip.js' import { Lock } from './device/lock.js' import { Meter } from './device/meter.js' import { MeterPlus } from './device/meterplus.js' import { MeterPro } from './device/meterpro.js' import { Motion } from './device/motion.js' import { Plug } from './device/plug.js' import { RelaySwitch } from './device/relayswitch.js' import { RobotVacuumCleaner } from './device/robotvacuumcleaner.js' import { WaterDetector } from './device/waterdetector.js' import { AirConditioner } from './irdevice/airconditioner.js' import { AirPurifier } from './irdevice/airpurifier.js' import { Camera } from './irdevice/camera.js' import { IRFan } from './irdevice/fan.js' import { Light } from './irdevice/light.js' import { Others } from './irdevice/other.js' import { TV } from './irdevice/tv.js' import { VacuumCleaner } from './irdevice/vacuumcleaner.js' import { WaterHeater } from './irdevice/waterheater.js' import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js' import { formatDeviceIdAsMac, isBlindTiltDevice, isCurtainDevice, safeStringify, sleep } from './utils.js' /** * 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 SwitchBotPlatform implements DynamicPlatformPlugin { // Platform properties public accessories: PlatformAccessory[] = [] public readonly api: API public readonly log: Logging // Configuration properties platformConfig!: SwitchBotPlatformConfig platformLogging!: options['logging'] platformRefreshRate!: options['refreshRate'] platformPushRate!: options['pushRate'] platformUpdateRate!: options['updateRate'] platformMaxRetries!: options['maxRetries'] platformDelayBetweenRetries!: options['delayBetweenRetries'] config!: SwitchBotPlatformConfig debugMode!: boolean version!: string // MQTT and Webhook properties mqttClient: MqttClient | null = null webhookEventListener: Server | null = null // SwitchBot APIs switchBotAPI!: SwitchBotOpenAPI switchBotBLE!: SwitchBotBLE // External APIs public readonly eve: any public readonly fakegatoAPI: any // Event Handlers public readonly webhookEventHandler: { [x: string]: (context: any) => void } = {} public readonly bleEventHandler: { [x: string]: (context: any) => void } = {} constructor( log: Logging, config: SwitchBotPlatformConfig, api: API, ) { this.api = api this.log = log // only load if configured if (!config) { this.log.error('No configuration found for the plugin, please check your config.') return } // Plugin options into our config variables. this.config = { platform: 'SwitchBotPlatform', name: config.name, credentials: config.credentials as object, options: config.options as object, devices: config.devices as { deviceId: string }[], } // Plugin Configuration this.getPlatformLogSettings() this.getPlatformRateSettings() this.getPlatformConfigSettings() this.getVersion() // Finish initializing the platform this.debugLog(`Finished initializing platform: ${config.name}`) // verify the config try { this.verifyConfig() this.debugLog('Config OK') } catch (e: any) { this.errorLog(`Verify Config, Error Message: ${e.message ?? e}, Submit Bugs Here: ` + 'https://tinyurl.com/SwitchBotBug') this.debugErrorLog(`Verify Config, Error: ${e.message ?? e}`) return } // SwitchBot OpenAPI if (this.config.credentials?.token && this.config.credentials?.secret) { this.switchBotAPI = new SwitchBotOpenAPI(this.config.credentials.token, this.config.credentials.secret, this.config.options?.hostname) } else { this.debugErrorLog('Missing SwitchBot API credentials (token or secret).') } // Listen for log events if (!this.config.options?.disableLogsforOpenAPI && this.switchBotAPI) { this.switchBotAPI.on('log', (log) => { switch (log.level) { case LogLevel.SUCCESS: this.successLog(log.message) break case LogLevel.DEBUGSUCCESS: this.debugSuccessLog(log.message) break case LogLevel.WARN: this.warnLog(log.message) break case LogLevel.DEBUGWARN: this.debugWarnLog(log.message) break case LogLevel.ERROR: this.errorLog(log.message) break case LogLevel.DEBUGERROR: this.debugErrorLog(log.message) break case LogLevel.DEBUG: this.debugLog(log.message) break case LogLevel.INFO: default: this.infoLog(log.message) } }) } else { this.debugErrorLog(`SwitchBot OpenAPI logs are disabled, enable it by setting disableLogsforOpenAPI to false.`) this.debugLog(`SwitchBot OpenAPI: ${JSON.stringify(this.switchBotAPI)}, disableLogsforOpenAPI: ${this.config.options?.disableLogsforOpenAPI}`) } // import fakegato-history module and EVE characteristics this.fakegatoAPI = fakegato(api) this.eve = new EveHomeKitTypes(api) // When this event is fired it means Homebridge has restored all cached accessories from disk. // Dynamic Platform plugins should only register new accessories after this event was fired, // in order to ensure they weren't added to homebridge already. This event can also be used // to start discovery of new accessories. this.api.on('didFinishLaunching', async () => { this.debugLog('Executed didFinishLaunching callback') // run the method to discover / register your devices as accessories try { await this.discoverDevices() } catch (e: any) { this.errorLog(`Failed to Discover, Error Message: ${e.message ?? e}, Submit Bugs Here: ` + 'https://tinyurl.com/SwitchBotBug') this.debugErrorLog(`Failed to Discover, Error: ${e.message ?? e}`) } }) try { this.setupMqtt() } catch (e: any) { this.errorLog(`Setup MQTT, Error Message: ${e.message ?? e}, Submit Bugs Here: ` + 'https://tinyurl.com/SwitchBotBug') } try { this.setupwebhook() } catch (e: any) { this.errorLog(`Setup Webhook, Error Message: ${e.message ?? e}, Submit Bugs Here: ` + 'https://tinyurl.com/SwitchBotBug') } try { this.setupBlE() } catch (e: any) { this.errorLog(`Setup Platform BLE, Error Message: ${e.message ?? e}, Submit Bugs Here: ` + 'https://tinyurl.com/SwitchBotBug') } } async setupMqtt(): Promise<void> { if (this.config.options?.mqttURL) { try { const { connectAsync } = asyncmqtt this.mqttClient = await connectAsync(this.config.options?.mqttURL, this.config.options.mqttOptions || {}) this.debugLog('MQTT connection has been established successfully.') this.mqttClient.on('error', async (e: Error) => { this.errorLog(`Failed to publish MQTT messages. ${e.message ?? e}`) }) if (!this.config.options?.webhookURL) { // receive webhook events via MQTT this.infoLog(`Webhook is configured to be received through ${this.config.options.mqttURL}/homebridge-switchbot/webhook.`) this.mqttClient.subscribe('homebridge-switchbot/webhook/+') this.mqttClient.on('message', async (topic: string, message) => { try { this.debugLog(`Received Webhook via MQTT: ${topic}=${message}`) const context = JSON.parse(message.toString()) this.webhookEventHandler[context.deviceMac]?.(context) } catch (e: any) { this.errorLog(`Failed to handle webhook event. Error:${e.message ?? e}`) } }) } } catch (e: any) { this.mqttClient = null this.errorLog(`Failed to establish MQTT connection. ${e.message ?? e}`) } } } async setupwebhook() { // webhook configuration if (this.config.options?.webhookURL) { const url = this.config.options?.webhookURL try { this.switchBotAPI.setupWebhook(url) // Listen for webhook events this.switchBotAPI.on('webhookEvent', (body) => { if (this.config.options?.mqttURL) { const mac = body.context.deviceMac?.toLowerCase().match(/[\s\S]{1,2}/g)?.join(':') const options = this.config.options?.mqttPubOptions || {} this.mqttClient?.publish(`homebridge-switchbot/webhook/${mac}`, `${JSON.stringify(body.context)}`, options) } this.webhookEventHandler[body.context.deviceMac]?.(body.context) }) } catch (e: any) { this.errorLog(`Failed to setup webhook. Error:${e.message ?? e}`) } this.api.on('shutdown', async () => { try { this.switchBotAPI.deleteWebhook(url) } catch (e: any) { this.errorLog(`Failed to delete webhook. Error:${e.message ?? e}`) } }) } } async setupBlE() { this.switchBotBLE = new SwitchBotBLE() // Listen for log events if (!this.config.options?.disableLogsforBLE) { this.switchBotBLE.on('log', (log) => { switch (log.level) { case LogLevel.SUCCESS: this.successLog(log.message) break case LogLevel.DEBUGSUCCESS: this.debugSuccessLog(log.message) break case LogLevel.WARN: this.warnLog(log.message) break case LogLevel.DEBUGWARN: this.debugWarnLog(log.message) break case LogLevel.ERROR: this.errorLog(log.message) break case LogLevel.DEBUGERROR: this.debugErrorLog(log.message) break case LogLevel.DEBUG: this.debugLog(log.message) break case LogLevel.INFO: default: this.infoLog(log.message) } }) } if (this.config.options?.BLE) { this.debugLog('setupBLE') if (this.switchBotBLE === undefined) { this.errorLog(`wasn't able to establish BLE Connection, node-switchbot: ${JSON.stringify(this.switchBotBLE)}`) } else { // Start to monitor advertisement packets (async () => { // Start to monitor advertisement packets this.debugLog('Scanning for BLE SwitchBot devices...') try { await this.switchBotBLE.startScan() } catch (e: any) { this.errorLog(`Failed to start BLE scanning. Error:${e.message ?? e}`) } // Set an event handler to monitor advertisement packets this.switchBotBLE.onadvertisement = async (ad: any) => { try { this.bleEventHandler[ad.address]?.(ad.serviceData) } catch (e: any) { this.errorLog(`Failed to handle BLE event. Error:${e.message ?? e}`) } } })() this.api.on('shutdown', async () => { try { // this.switchBotBLE.stopScan() this.infoLog('Stopped BLE scanning to close listening.') } catch (e: any) { this.errorLog(`Failed to stop Platform BLE scanning. Error:${e.message ?? e}`) } }) } } else { this.debugLog('Platform BLE is not enabled') } } /** * 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. */ async configureAccessory(accessory: PlatformAccessory) { const { displayName } = accessory this.debugLog(`Loading accessory from cache: ${displayName}`) // add the restored accessory to the accessories cache so we can track if it has already been registered this.accessories.push(accessory) } /** * Verify the config passed to the plugin is valid */ async verifyConfig() { this.debugLog('Verifying Config') this.config = this.config || {} this.config.options = this.config.options || {} if (this.config.options) { // Device Config if (this.config.options.devices) { for (const deviceConfig of this.config.options.devices) { if (!deviceConfig.hide_device) { if (!deviceConfig.deviceId) { throw new Error('The devices config section is missing the *Device ID* in the config. Please check your config.') } if (!deviceConfig.configDeviceType && (deviceConfig as devicesConfig).connectionType) { throw new Error('The devices config section is missing the *Device Type* in the config. Please check your config.') } } } } // IR Device Config if (this.config.options.irdevices) { for (const irDeviceConfig of this.config.options.irdevices) { if (!irDeviceConfig.hide_device) { if (!irDeviceConfig.deviceId) { this.errorLog('The devices config section is missing the *Device ID* in the config. Please check your config.') } if (!irDeviceConfig.deviceId && !irDeviceConfig.configRemoteType) { this.errorLog('The devices config section is missing the *Device Type* in the config. Please check your config.') } } } } } if (!this.config.credentials && !this.config.options) { this.debugWarnLog('Missing Credentials') } else if (this.config.credentials && !this.config.credentials.notice) { if (!this.config.credentials?.token) { this.debugErrorLog('Missing token') this.debugWarnLog('Cloud Enabled SwitchBot Devices & IR Devices will not work') } if (this.config.credentials?.token) { if (!this.config.credentials?.secret) { this.debugErrorLog('Missing secret') this.debugWarnLog('Cloud Enabled SwitchBot Devices & IR Devices will not work') } } } } async discoverDevices() { if (!this.config.credentials?.token) { return this.handleManualConfig() } let retryCount = 0 const maxRetries = this.platformMaxRetries ?? 5 const delayBetweenRetries = this.platformDelayBetweenRetries || 5000 this.debugWarnLog(`Retry Count: ${retryCount}`) this.debugWarnLog(`Max Retries: ${this.platformMaxRetries}`) this.debugWarnLog(`Delay Between Retries: ${this.platformDelayBetweenRetries}`) while (retryCount < maxRetries) { try { const { response, statusCode } = await this.switchBotAPI.getDevices() this.debugLog(`response: ${JSON.stringify(response)}`) if (this.isSuccessfulResponse(statusCode)) { await this.handleDevices(Array.isArray(response.body.deviceList) ? response.body.deviceList : []) await this.handleIRDevices(Array.isArray(response.body.infraredRemoteList) ? response.body.infraredRemoteList : []) break } else { await this.handleErrorResponse(statusCode, retryCount, maxRetries, delayBetweenRetries) retryCount++ } } catch (e: any) { retryCount++ this.debugErrorLog(`Failed to Discover Devices, Error Message: ${JSON.stringify(e.message)}, Submit Bugs Here: https://tinyurl.com/SwitchBotBug`) this.debugErrorLog(`Failed to Discover Devices, Error: ${e.message ?? e}`) } } } private async handleManualConfig() { if (this.config.options?.devices) { this.debugLog(`SwitchBot Device Manual Config Set: ${JSON.stringify(this.config.options?.devices)}`) const devices = this.config.options.devices.map((v: any) => v) for (const device of devices) { device.deviceType = device.configDeviceType !== undefined ? device.configDeviceType : 'Unknown' device.deviceName = device.configDeviceName !== undefined ? device.configDeviceName : 'Unknown' try { device.deviceId = formatDeviceIdAsMac(device.deviceId, true) this.debugLog(`deviceId: ${device.deviceId}`) if (device.deviceType) { await this.createDevice(device) } } catch (error) { this.errorLog(`failed to format device ID as MAC, Error: ${error}`) } } } else { this.errorLog('Neither SwitchBot Token or Device Config are set.') } } private isSuccessfulResponse(apiStatusCode: number): boolean { return (apiStatusCode === 200 || apiStatusCode === 100) } private async handleDevices(deviceLists: any[]) { if (!this.config.options?.devices && !this.config.options?.deviceConfig) { this.debugLog(`SwitchBot Device Config Not Set: ${JSON.stringify(this.config.options?.devices)}`) if (deviceLists.length === 0) { this.debugLog('SwitchBot API Has No Devices With Cloud Services Enabled') } else { for (const device of deviceLists) { if (device.deviceType) { if (device.configDeviceName) { device.deviceName = device.configDeviceName } await this.createDevice(device) } } } } else if (this.config.options?.devices || this.config.options?.deviceConfig) { this.debugLog(`SwitchBot Device Config Set: ${JSON.stringify(this.config.options?.devices)}`) // Step 1: Check and assign configDeviceType to deviceType if deviceType is not present const devicesWithTypeConfigPromises = deviceLists.map(async (device) => { if (!device.deviceType) { device.deviceType = device.configDeviceType !== undefined ? device.configDeviceType : 'Unknown' this.warnLog(`API is displaying no deviceType: ${device.deviceType}, So using configDeviceType: ${device.configDeviceType}`) } // Retrieve deviceTypeConfig for each device and merge it const deviceTypeConfig = this.config.options?.deviceConfig?.[device.deviceType] || {} return Object.assign({}, device, deviceTypeConfig) }) // Wait for all promises to resolve const devicesWithTypeConfig = (await Promise.all(devicesWithTypeConfigPromises)).filter(device => device !== null) // Filter out skipped devices const devices = this.mergeByDeviceId(this.config.options.devices ?? [], devicesWithTypeConfig ?? []) this.debugLog(`SwitchBot Devices: ${JSON.stringify(devices)}`) for (const device of devices) { const deviceIdConfig = this.config.options?.devices?.[device.deviceId] || {} const deviceWithConfig = Object.assign({}, device, deviceIdConfig) if (device.configDeviceName) { device.deviceName = device.configDeviceName } // Pass the merged device object to createDevice await this.createDevice(deviceWithConfig) } } } private async handleIRDevices(irDeviceLists: any[]) { if (!this.config.options?.irdevices && !this.config.options?.irdeviceConfig) { this.debugLog(`IR Device Config Not Set: ${JSON.stringify(this.config.options?.irdevices)}`) for (const device of irDeviceLists) { if (device.remoteType) { await this.createIRDevice(device) } } } else if (this.config.options?.irdevices || this.config.options?.irdeviceConfig) { this.debugLog(`IR Device Config Set: ${JSON.stringify(this.config.options?.irdevices)}`) // Step 1: Check and assign configRemoteType to remoteType if remoteType is not present const devicesWithTypeConfigPromises = irDeviceLists.map(async (device) => { if (!device.remoteType && device.configRemoteType) { device.remoteType = device.configRemoteType this.warnLog(`API is displaying no remoteType: ${device.remoteType}, So using configRemoteType: ${device.configRemoteType}`) } else if (!device.remoteType && !device.configDeviceName) { this.errorLog('No remoteType or configRemoteType for device. No device will be created.') return null // Skip this device } // Retrieve remoteTypeConfig for each device and merge it const remoteTypeConfig = this.config.options?.irdeviceConfig?.[device.remoteType] || {} return Object.assign({}, device, remoteTypeConfig) }) // Wait for all promises to resolve const devicesWithRemoteTypeConfig = (await Promise.all(devicesWithTypeConfigPromises)).filter(device => device !== null) // Filter out skipped devices const devices = this.mergeByDeviceId(this.config.options.irdevices ?? [], devicesWithRemoteTypeConfig ?? []) this.debugLog(`IR Devices: ${JSON.stringify(devices)}`) for (const device of devices) { const irdeviceIdConfig = this.config.options?.irdevices?.[device.deviceId] || {} const irdeviceWithConfig = Object.assign({}, device, irdeviceIdConfig) if (device.configDeviceName) { device.deviceName = device.configDeviceName } await this.createIRDevice(irdeviceWithConfig) } } } private mergeByDeviceId(a1: { deviceId: string }[], a2: any[]) { const normalizeDeviceId = (deviceId: string) => deviceId.toUpperCase().replace(/[^A-Z0-9]+/g, '') return a1.map((itm) => { const matchingItem = a2.find(item => normalizeDeviceId(item.deviceId) === normalizeDeviceId(itm.deviceId)) return { ...matchingItem, ...itm } }) } private async handleErrorResponse(apiStatusCode: number, retryCount: number, maxRetries: number, delayBetweenRetries: number) { await this.statusCode(apiStatusCode) if (apiStatusCode === 500) { this.infoLog(`statusCode: ${apiStatusCode} Attempt ${retryCount + 1} of ${maxRetries}`) await sleep(delayBetweenRetries) } } private async createDevice(device: device & devicesConfig) { const deviceTypeHandlers: { [key: string]: (device: device & devicesConfig) => Promise<void> } = { 'Humidifier': this.createHumidifier.bind(this), 'Humidifier2': this.createHumidifier.bind(this), 'Hub 2': this.createHub2.bind(this), 'Bot': this.createBot.bind(this), 'Relay Switch 1': this.createRelaySwitch.bind(this), 'Relay Switch 1PM': this.createRelaySwitch.bind(this), 'Meter': this.createMeter.bind(this), 'MeterPlus': this.createMeterPlus.bind(this), 'Meter Plus (JP)': this.createMeterPlus.bind(this), 'Meter Pro': this.createMeterPro.bind(this), 'MeterPro(CO2)': this.createMeterPro.bind(this), 'WoIOSensor': this.createIOSensor.bind(this), 'Water Detector': this.createWaterDetector.bind(this), 'Motion Sensor': this.createMotion.bind(this), 'Contact Sensor': this.createContact.bind(this), 'Curtain': this.createCurtain.bind(this), 'Curtain3': this.createCurtain.bind(this), 'WoRollerShade': this.createCurtain.bind(this), 'Roller Shade': this.createCurtain.bind(this), 'Blind Tilt': this.createBlindTilt.bind(this), 'Plug': this.createPlug.bind(this), 'Plug Mini (US)': this.createPlug.bind(this), 'Plug Mini (JP)': this.createPlug.bind(this), 'Smart Lock': this.createLock.bind(this), 'Smart Lock Pro': this.createLock.bind(this), 'Color Bulb': this.createColorBulb.bind(this), 'K10+': this.createRobotVacuumCleaner.bind(this), 'K10+ Pro': this.createRobotVacuumCleaner.bind(this), 'WoSweeper': this.createRobotVacuumCleaner.bind(this), 'WoSweeperMini': this.createRobotVacuumCleaner.bind(this), 'Robot Vacuum Cleaner S1': this.createRobotVacuumCleaner.bind(this), 'Robot Vacuum Cleaner S1 Plus': this.createRobotVacuumCleaner.bind(this), 'Robot Vacuum Cleaner S10': this.createRobotVacuumCleaner.bind(this), 'Ceiling Light': this.createCeilingLight.bind(this), 'Ceiling Light Pro': this.createCeilingLight.bind(this), 'Strip Light': this.createStripLight.bind(this), 'Battery Circulator Fan': this.createFan.bind(this), } if (deviceTypeHandlers[device.deviceType!]) { this.debugLog(`Discovered ${device.deviceType}: ${device.deviceId}`) await deviceTypeHandlers[device.deviceType!](device) } else if (['Hub Mini', 'Hub Plus', 'Remote', 'Indoor Cam', 'remote with screen'].includes(device.deviceType!)) { this.debugLog(`Discovered ${device.deviceType}: ${device.deviceId}, is currently not supported, device: ${JSON.stringify(device)}`) } else { this.warnLog(`Device: ${device.deviceName} with Device Type: ${device.deviceType}, is currently not supported. Submit Feature Requests Here: https://tinyurl.com/SwitchBotFeatureRequest, device: ${JSON.stringify(device)}`) } } private async createIRDevice(device: irdevice & irDevicesConfig) { device.connectionType = device.connectionType ?? 'OpenAPI' const deviceTypeHandlers: { [key: string]: (device: irdevice & irDevicesConfig) => Promise<void> } = { 'TV': this.createTV.bind(this), 'DIY TV': this.createTV.bind(this), 'Projector': this.createTV.bind(this), 'DIY Projector': this.createTV.bind(this), 'Set Top Box': this.createTV.bind(this), 'DIY Set Top Box': this.createTV.bind(this), 'IPTV': this.createTV.bind(this), 'DIY IPTV': this.createTV.bind(this), 'DVD': this.createTV.bind(this), 'DIY DVD': this.createTV.bind(this), 'Speaker': this.createTV.bind(this), 'DIY Speaker': this.createTV.bind(this), 'Fan': this.createIRFan.bind(this), 'DIY Fan': this.createIRFan.bind(this), 'Air Conditioner': this.createAirConditioner.bind(this), 'DIY Air Conditioner': this.createAirConditioner.bind(this), 'Light': this.createLight.bind(this), 'DIY Light': this.createLight.bind(this), 'Air Purifier': this.createAirPurifier.bind(this), 'DIY Air Purifier': this.createAirPurifier.bind(this), 'Water Heater': this.createWaterHeater.bind(this), 'DIY Water Heater': this.createWaterHeater.bind(this), 'Vacuum Cleaner': this.createVacuumCleaner.bind(this), 'DIY Vacuum Cleaner': this.createVacuumCleaner.bind(this), 'Camera': this.createCamera.bind(this), 'DIY Camera': this.createCamera.bind(this), 'Others': this.createOthers.bind(this), } if (deviceTypeHandlers[device.remoteType!]) { this.debugLog(`Discovered ${device.remoteType}: ${device.deviceId}`) if (device.remoteType.startsWith('DIY') && device.external === undefined) { device.external = true } await deviceTypeHandlers[device.remoteType!](device) } else { this.warnLog(`Device: ${device.deviceName} with Device Type: ${device.remoteType}, is currently not supported. Submit Feature Requests Here: https://tinyurl.com/SwitchBotFeatureRequest, device: ${JSON.stringify(device)}`) } } private async createHumidifier(device: device & devicesConfig) { const uuid = this.api.hap.uuid.generate(`${device.deviceId}-${device.deviceType}`) // see if an accessory with the same uuid has already been registered and restored from // the cached devices we stored in the `configureAccessory` method above const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid) if (existingAccessory) { // the accessory already exists if (await this.registerDevice(device)) { // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: existingAccessory.context.device = device existingAccessory.context.deviceId = device.deviceId existingAccessory.context.deviceType = device.deviceType existingAccessory.context.model = device.deviceType === 'Humidifier2' ? SwitchBotModel.Humidifier2 : SwitchBotModel.Humidifier existingAccessory.displayName = device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName) existingAccessory.context.connectionType = await this.connectionType(device) existingAccessory.context.version = device.firmware ?? device.version ?? this.version ?? '0.0.0' this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} deviceId: ${device.deviceId}`) this.api.updatePlatformAccessories([existingAccessory]) // create the accessory handler for the restored accessory // this is imported from `platformAccessory.ts` new Humidifier(this, existingAccessory, device) this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${existingAccessory.UUID})`) } else { this.unregisterPlatformAccessories(existingAccessory) } } else if (await this.registerDevice(device)) { // create a new accessory const accessory = new this.api.platformAccessory(device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName), uuid) // store a copy of the device object in the `accessory.context` // the `context` property can be used to store any data about the accessory you may need accessory.context.device = device accessory.context.deviceId = device.deviceId accessory.context.deviceType = device.deviceType accessory.context.model = device.deviceType === 'Humidifier2' ? SwitchBotModel.Humidifier2 : SwitchBotModel.Humidifier accessory.displayName = device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName) accessory.context.connectionType = await this.connectionType(device) accessory.context.version = device.firmware ?? device.version ?? this.version ?? '0.0.0' const newOrExternal = !device.external ? 'Adding new' : 'Loading external' this.infoLog(`${newOrExternal} accessory: ${accessory.displayName} deviceId: ${device.deviceId}`) // create the accessory handler for the newly create accessory // this is imported from `platformAccessory.ts` new Humidifier(this, accessory, device) this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${accessory.UUID})`) // publish device externally or link the accessory to your platform this.externalOrPlatform(device, accessory) this.accessories.push(accessory) } else { this.debugLog(`Device not registered: ${device.deviceName} ${device.deviceType} deviceId: ${device.deviceId}`) } } private async createBot(device: device & devicesConfig) { const uuid = this.api.hap.uuid.generate(`${device.deviceId}-${device.deviceType}`) // see if an accessory with the same uuid has already been registered and restored from // the cached devices we stored in the `configureAccessory` method above const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid) if (existingAccessory) { // the accessory already exists if (await this.registerDevice(device)) { // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: existingAccessory.context.device = device existingAccessory.context.deviceId = device.deviceId existingAccessory.context.deviceType = device.deviceType existingAccessory.context.model = SwitchBotModel.Bot existingAccessory.displayName = device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName) existingAccessory.context.connectionType = await this.connectionType(device) existingAccessory.context.version = device.firmware ?? device.version ?? this.version ?? '0.0.0' this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} deviceId: ${device.deviceId}`) this.api.updatePlatformAccessories([existingAccessory]) // create the accessory handler for the restored accessory // this is imported from `platformAccessory.ts` new Bot(this, existingAccessory, device) this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${existingAccessory.UUID})`) } else { this.unregisterPlatformAccessories(existingAccessory) } } else if (await this.registerDevice(device)) { // create a new accessory const accessory = new this.api.platformAccessory(device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName), uuid) // store a copy of the device object in the `accessory.context` // the `context` property can be used to store any data about the accessory you may need accessory.context.device = device accessory.context.deviceId = device.deviceId accessory.context.deviceType = device.deviceType accessory.context.model = SwitchBotModel.Bot accessory.displayName = device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName) accessory.context.connectionType = await this.connectionType(device) accessory.context.connectionType = await this.connectionType(device) accessory.context.version = device.firmware ?? device.version ?? this.version ?? '0.0.0' const newOrExternal = !device.external ? 'Adding new' : 'Loading external' this.infoLog(`${newOrExternal} accessory: ${accessory.displayName} deviceId: ${device.deviceId}`) // accessory.context.version = findaccessories.accessoryAttribute.softwareRevision; // create the accessory handler for the newly create accessory // this is imported from `platformAccessory.ts` new Bot(this, accessory, device) this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${accessory.UUID})`) // publish device externally or link the accessory to your platform this.externalOrPlatform(device, accessory) this.accessories.push(accessory) } else { this.debugLog(`Device not registered: ${device.deviceName} ${device.deviceType} deviceId: ${device.deviceId}`) } } private async createRelaySwitch(device: device & devicesConfig) { const uuid = this.api.hap.uuid.generate(`${device.deviceId}-${device.deviceType}`) // see if an accessory with the same uuid has already been registered and restored from // the cached devices we stored in the `configureAccessory` method above const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid) if (existingAccessory) { // the accessory already exists if (await this.registerDevice(device)) { // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: existingAccessory.context.device = device existingAccessory.context.deviceId = device.deviceId existingAccessory.context.deviceType = device.deviceType existingAccessory.context.model = device.deviceType === 'Relay Switch 1' ? SwitchBotModel.RelaySwitch1 : SwitchBotModel.RelaySwitch1PM existingAccessory.displayName = device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName) existingAccessory.context.connectionType = await this.connectionType(device) existingAccessory.context.version = device.firmware ?? device.version ?? this.version ?? '0.0.0' this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} deviceId: ${device.deviceId}`) this.api.updatePlatformAccessories([existingAccessory]) // create the accessory handler for the restored accessory // this is imported from `platformAccessory.ts` new RelaySwitch(this, existingAccessory, device) this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${existingAccessory.UUID})`) } else { this.unregisterPlatformAccessories(existingAccessory) } } else if (await this.registerDevice(device)) { // create a new accessory const accessory = new this.api.platformAccessory(device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName), uuid) // store a copy of the device object in the `accessory.context` // the `context` property can be used to store any data about the accessory you may need accessory.context.device = device accessory.context.deviceId = device.deviceId accessory.context.deviceType = device.deviceType accessory.context.model = device.deviceType === 'Relay Switch 1' ? SwitchBotModel.RelaySwitch1 : SwitchBotModel.RelaySwitch1PM accessory.displayName = device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName) accessory.context.connectionType = await this.connectionType(device) accessory.context.connectionType = await this.connectionType(device) accessory.context.version = device.firmware ?? device.version ?? this.version ?? '0.0.0' const newOrExternal = !device.external ? 'Adding new' : 'Loading external' this.infoLog(`${newOrExternal} accessory: ${accessory.displayName} deviceId: ${device.deviceId}`) // accessory.context.version = findaccessories.accessoryAttribute.softwareRevision; // create the accessory handler for the newly create accessory // this is imported from `platformAccessory.ts` new RelaySwitch(this, accessory, device) this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${accessory.UUID})`) // publish device externally or link the accessory to your platform this.externalOrPlatform(device, accessory) this.accessories.push(accessory) } else { this.debugLog(`Device not registered: ${device.deviceName} ${device.deviceType} deviceId: ${device.deviceId}`) } } private async createMeter(device: device & devicesConfig) { const uuid = this.api.hap.uuid.generate(`${device.deviceId}-${device.deviceType}`) // see if an accessory with the same uuid has already been registered and restored from // the cached devices we stored in the `configureAccessory` method above const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid) if (existingAccessory) { // the accessory already exists if (await this.registerDevice(device)) { // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: existingAccessory.context.device = device existingAccessory.context.model = SwitchBotModel.Meter existingAccessory.context.deviceId = device.deviceId existingAccessory.context.deviceType = device.deviceType existingAccessory.displayName = device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName) existingAccessory.context.connectionType = await this.connectionType(device) existingAccessory.context.version = device.firmware ?? device.version ?? this.version ?? '0.0.0' this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} deviceId: ${device.deviceId}`) this.api.updatePlatformAccessories([existingAccessory]) // create the accessory handler for the restored accessory // this is imported from `platformAccessory.ts` new Meter(this, existingAccessory, device) this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${existingAccessory.UUID})`) } else { this.unregisterPlatformAccessories(existingAccessory) } } else if (await this.registerDevice(device)) { // create a new accessory const accessory = new this.api.platformAccessory(device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName), uuid) // store a copy of the device object in the `accessory.context` // the `context` property can be used to store any data about the accessory you may need accessory.context.device = device accessory.context.model = SwitchBotModel.Meter accessory.context.deviceId = device.deviceId accessory.context.deviceType = device.deviceType accessory.displayName = device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName) accessory.context.connectionType = await this.connectionType(device) accessory.context.connectionType = await this.connectionType(device) accessory.context.version = device.firmware ?? device.version ?? this.version ?? '0.0.0' const newOrExternal = !device.external ? 'Adding new' : 'Loading external' this.infoLog(`${newOrExternal} accessory: ${accessory.displayName} deviceId: ${device.deviceId}`) // create the accessory handler for the newly create accessory // this is imported from `platformAccessory.ts` new Meter(this, accessory, device) this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${accessory.UUID})`) // publish device externally or link the accessory to your platform this.externalOrPlatform(device, accessory) this.accessories.push(accessory) } else { this.debugLog(`Device not registered: ${device.deviceName} ${device.deviceType} deviceId: ${device.deviceId}`) } } private async createMeterPlus(device: device & devicesConfig) { const uuid = this.api.hap.uuid.generate(`${device.deviceId}-${device.deviceType}`) // see if an accessory with the same uuid has already been registered and restored from // the cached devices we stored in the `configureAccessory` method above const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid) if (existingAccessory) { // the accessory already exists if (await this.registerDevice(device)) { // console.log("existingAccessory", existingAccessory); // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: existingAccessory.context.device = device existingAccessory.context.model = SwitchBotModel.MeterPlusUS ?? SwitchBotModel.MeterPlusJP existingAccessory.context.deviceId = device.deviceId existingAccessory.context.deviceType = device.deviceType existingAccessory.displayName = device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName) existingAccessory.context.connectionType = await this.connectionType(device) existingAccessory.context.version = device.firmware ?? device.version ?? this.version ?? '0.0.0' this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} deviceId: ${device.deviceId}`) this.api.updatePlatformAccessories([existingAccessory]) // create the accessory handler for the restored accessory // this is imported from `platformAccessory.ts` new MeterPlus(this, existingAccessory, device) this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${existingAccessory.UUID})`) } else { this.unregisterPlatformAccessories(existingAccessory) } } else if (await this.registerDevice(device)) { // create a new accessory const accessory = new this.api.platformAccessory(device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName), uuid) // store a copy of the device object in the `accessory.context` // the `context` property can be used to store any data about the accessory you may need accessory.context.device = device accessory.context.model = SwitchBotModel.MeterPlusUS ?? SwitchBotModel.MeterPlusJP accessory.context.deviceId = device.deviceId accessory.context.deviceType = device.deviceType accessory.displayName = device.configDeviceName ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName) : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName) accessory.context.connectionType = await this.connectionType(device) accessory.context.connectionType = await this.connectionType(device) accessory.context.version = device.firmware ?? device.version ?? this.version ?? '0.0.0' const newOrExternal = !device.external ? 'Adding new' : 'Loading external' this.infoLog(`${newOrExternal} accessory: ${accessory.displayName} deviceId: ${device.deviceId}`) // create the accessory handler for the newly create accessory // this is imported from `platformAccessory.ts` new MeterPlus(this, accessory, device) this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${accessory.UUID})`) // publish device externally or link the accessory to your platform this.externalOrPlatform(device, accessory) this.accessories.push(accessory) } else { this.debugLog(`Device not registered: ${device.deviceName} ${device.deviceType} deviceId: ${device.deviceId}`) } } private async createMeterPro(device: device & devicesConfig) { const uuid = this.api.hap.uuid.generate(`${device.deviceId}-${device.deviceType}`) // see if an accessory with the same uuid has already been registered and restored from // the cached devices we stored in the `configureAccessory` method above const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid) if (existingAccessory) { // the accessory already exists if (await this.registerDevice(device)) { // console.log("existingAccessory", existingAccessory); // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: existingAccessory.context.device = device existingAccessory.context.model = SwitchBotModel.MeterPro ?? SwitchBotModel.MeterProCO2 existingAccessory.context.deviceId = device.deviceId existingAccessory.context.deviceType