@switchbot/homebridge-switchbot
Version:
The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.
1,001 lines (916 loc) • 166 kB
text/typescript
/* 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