@switchbot/homebridge-switchbot
Version:
The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.
345 lines (312 loc) • 15.7 kB
text/typescript
/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
*
* irdevice.ts: @switchbot/homebridge-switchbot.
*/
import type { API, CharacteristicValue, HAP, Logging, PlatformAccessory, Service } from 'homebridge'
import type { bodyChange, irdevice } from 'node-switchbot'
import type { SwitchBotPlatform } from '../platform.js'
import type { irAirConfig, irDevicesConfig, irFanConfig, irLightConfig, irOtherConfig, SwitchBotPlatformConfig } from '../settings.js'
export abstract class irdeviceBase {
public readonly api: API
public readonly log: Logging
public readonly config!: SwitchBotPlatformConfig
protected readonly hap: HAP
// Config
protected deviceLogging!: string
protected deviceRefreshRate!: number
protected deviceUpdateRate!: number
protected devicePushRate!: number
protected deviceMaxRetries!: number
protected deviceDelayBetweenRetries!: number
protected deviceDisablePushOn!: boolean
protected deviceDisablePushOff!: boolean
protected deviceDisablePushDetail?: boolean
constructor(
protected readonly platform: SwitchBotPlatform,
protected accessory: PlatformAccessory,
protected device: irdevice & irDevicesConfig,
) {
this.api = this.platform.api
this.log = this.platform.log
this.config = this.platform.config
this.hap = this.api.hap
this.getDeviceLogSettings(accessory, device)
this.getDeviceRateSettings(device)
this.getDeviceConfigSettings(device)
this.getDeviceContext(accessory, device)
// Set accessory information
accessory
.getService(this.hap.Service.AccessoryInformation)!
.setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot')
.setCharacteristic(this.hap.Characteristic.AppMatchingIdentifier, 'id1087374760')
.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName)
.setCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName)
.setCharacteristic(this.hap.Characteristic.Model, accessory.context.model ?? 'Unknown')
.setCharacteristic(this.hap.Characteristic.ProductData, device.deviceId)
.setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId)
}
async getDeviceLogSettings(accessory: PlatformAccessory, device: irdevice & irDevicesConfig): Promise<void> {
this.deviceLogging = this.platform.debugMode ? 'debugMode' : device.logging ?? this.platform.platformLogging ?? 'standard'
const logging = this.platform.debugMode ? 'Debug Mode' : device.logging ? 'Device Config' : this.platform.platformLogging ? 'Platform Config' : 'Default'
accessory.context.deviceLogging = this.deviceLogging
this.debugLog(`Using ${logging} Logging: ${this.deviceLogging}`)
}
async getDeviceRateSettings(device: irdevice & irDevicesConfig): Promise<void> {
// refreshRate
this.deviceRefreshRate = device.refreshRate ?? this.platform.platformRefreshRate ?? 300
const refreshRate = device.refreshRate ? 'Device Config' : this.platform.platformRefreshRate ? 'Platform Config' : 'Default'
this.accessory.context.refreshRate = this.deviceRefreshRate
// updateRate
this.deviceUpdateRate = device.updateRate ?? this.platform.platformUpdateRate ?? 5
const updateRate = device.updateRate ? 'Device Config' : this.platform.platformUpdateRate ? 'Platform Config' : 'Default'
this.accessory.context.updateRate = this.deviceUpdateRate
// pushRate
this.devicePushRate = device.pushRate ?? this.platform.platformPushRate ?? 0.1
const pushRate = device.pushRate ? 'Device Config' : this.platform.platformPushRate ? 'Platform Config' : 'Default'
this.accessory.context.pushRate = this.devicePushRate
this.debugLog(`Using ${refreshRate} refreshRate: ${this.deviceRefreshRate}, ${updateRate} updateRate: ${this.deviceUpdateRate}, ${pushRate} pushRate: ${this.devicePushRate}`)
// maxRetries
this.deviceMaxRetries = device.maxRetries ?? this.platform.platformMaxRetries ?? 2
const maxRetries = device.maxRetries ? 'Device' : this.platform.platformMaxRetries ? 'Platform' : 'Default'
this.debugLog(`Using ${maxRetries} Max Retries: ${this.deviceMaxRetries}`)
// delayBetweenRetries
this.deviceDelayBetweenRetries = device.delayBetweenRetries ? (device.delayBetweenRetries * 1000) : this.platform.platformDelayBetweenRetries ?? 3000
const delayBetweenRetries = device.delayBetweenRetries ? 'Device' : this.platform.platformDelayBetweenRetries ? 'Platform' : 'Default'
this.debugLog(`Using ${delayBetweenRetries} Delay Between Retries: ${this.deviceDelayBetweenRetries}`)
// disablePushOn
this.deviceDisablePushOn = device.disablePushOn ?? false
const disablePushOn = device.disablePushOn ? 'Device Config' : 'Default'
// disablePushOff
this.deviceDisablePushOff = device.disablePushOff ?? false
const disablePushOff = device.disablePushOff ? 'Device Config' : 'Default'
// disablePushDetail
this.deviceDisablePushDetail = device.disablePushDetail ?? false
const disablePushDetail = device.disablePushDetail ? 'Device Config' : 'Default'
this.debugLog(`Using ${disablePushOn} Disable Push On: ${this.deviceDisablePushOn}, ${disablePushOff} Disable Push Off: ${this.deviceDisablePushOff}, ${disablePushDetail} Disable Push Detail: ${this.deviceDisablePushDetail}`)
}
async getDeviceConfigSettings(device: irdevice & irDevicesConfig): Promise<void> {
const deviceConfig = Object.assign(
{},
device.logging !== 'standard' && { logging: device.logging },
device.connectionType !== '' && { connectionType: device.connectionType },
device.external === true && { external: device.external },
device.customize === true && { customize: device.customize },
device.commandType !== '' && { commandType: device.commandType },
device.customOn !== '' && { customOn: device.customOn },
device.customOff !== '' && { customOff: device.customOff },
device.disablePushOn === true && { disablePushOn: device.disablePushOn },
device.disablePushOff === true && { disablePushOff: device.disablePushOff },
device.disablePushDetail === true && { disablePushDetail: device.disablePushDetail },
)
let deviceSpecificConfig = {}
switch (device.configRemoteType) {
case 'Fan':
case 'DIY Fan':
deviceSpecificConfig = device as irFanConfig
break
case 'Light':
case 'DIY Light':
deviceSpecificConfig = device as irLightConfig
break
case 'Air Conditioner':
case 'DIY Air Conditioner':
deviceSpecificConfig = device as irAirConfig
break
case 'Others':
deviceSpecificConfig = device as irOtherConfig
break
default:
break
}
const config = Object.assign(
{},
deviceConfig,
deviceSpecificConfig,
)
if (Object.keys(config).length !== 0) {
this.debugSuccessLog(`Config: ${JSON.stringify(config)}`)
}
}
async getDeviceContext(accessory: PlatformAccessory, device: irdevice & irDevicesConfig): Promise<void> {
accessory.context.name = device.deviceName
accessory.context.model = device.remoteType
accessory.context.deviceId = device.deviceId
accessory.context.remoteType = device.remoteType
const deviceFirmwareVersion = device.firmware ?? accessory.context.version ?? this.platform.version ?? '0.0.0'
const version = deviceFirmwareVersion.toString()
this.debugLog(`version: ${version?.replace(/^V|-.*$/g, '')}`)
let deviceVersion: string
if (version?.includes('.') === false) {
const replace = version?.replace(/^V|-.*$/g, '')
const match = replace?.match(/./g)
const validVersion = match?.join('.')
deviceVersion = validVersion ?? '0.0.0'
} else {
deviceVersion = version.replace(/^V|-.*$/g, '') ?? '0.0.0'
}
accessory
.getService(this.hap.Service.AccessoryInformation)!
.setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion)
.setCharacteristic(this.hap.Characteristic.SoftwareRevision, deviceVersion)
.setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion)
.getCharacteristic(this.hap.Characteristic.FirmwareRevision)
.updateValue(deviceVersion)
accessory.context.version = deviceVersion
this.debugSuccessLog(`version: ${accessory.context.version}`)
}
async pushChangeRequest(bodyChange: bodyChange): Promise<{ body: any, statusCode: number }> {
const { response, statusCode } = await this.platform.retryCommand(this.device, bodyChange, this.deviceMaxRetries, this.deviceDelayBetweenRetries)
return { body: response, statusCode }
}
async successfulStatusCodes(deviceStatus: any) {
return (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)
}
/**
* Update the characteristic value and log the change.
*
* @param Service Service
* @param Characteristic Characteristic
* @param CharacteristicValue CharacteristicValue | undefined
* @param CharacteristicName string
* @return: void
*
*/
async updateCharacteristic(Service: Service, Characteristic: any, CharacteristicValue: CharacteristicValue | undefined, CharacteristicName: string): Promise<void> {
if (CharacteristicValue === undefined) {
this.debugLog(`${CharacteristicName}: ${CharacteristicValue}`)
} else {
Service.updateCharacteristic(Characteristic, CharacteristicValue)
this.debugLog(`updateCharacteristic ${CharacteristicName}: ${CharacteristicValue}`)
this.debugWarnLog(`${CharacteristicName} context before: ${this.accessory.context[CharacteristicName]}`)
this.accessory.context[CharacteristicName] = CharacteristicValue
this.debugWarnLog(`${CharacteristicName} context after: ${this.accessory.context[CharacteristicName]}`)
}
}
async pushStatusCodes(deviceStatus: any) {
this.debugWarnLog(`deviceStatus: ${JSON.stringify(deviceStatus)}`)
this.debugWarnLog(`deviceStatus statusCode: ${deviceStatus.statusCode}`)
}
async successfulPushChange(deviceStatus: any, bodyChange: any) {
this.debugSuccessLog(`deviceStatus StatusCode: ${deviceStatus.statusCode}`)
this.successLog(`request to SwitchBot API, body: ${JSON.stringify(bodyChange)} sent successfully`)
}
async pushChangeError(e: Error) {
this.errorLog(`failed pushChanges with ${this.device.connectionType} Connection, Error Message: ${JSON.stringify(e.message)}`)
}
async commandType(): Promise<string> {
let commandType: string
if (this.device.commandType && this.device.customize) {
commandType = this.device.commandType
} else if (this.device.customize) {
commandType = 'customize'
} else {
commandType = 'command'
}
return commandType
}
async commandOn(): Promise<string> {
let command: string
if (this.device.customize && this.device.customOn) {
command = this.device.customOn
} else {
command = 'turnOn'
}
return command
}
async commandOff(): Promise<string> {
let command: string
if (this.device.customize && this.device.customOff) {
command = this.device.customOff
} else {
command = 'turnOff'
}
return command
}
async statusCode(statusCode: number): Promise<void> {
const statusMessages = {
151: 'Command not supported by this deviceType',
152: 'Device not found',
160: 'Command is not supported',
161: 'Device is offline',
171: `Hub Device is offline. Hub: ${this.device.hubDeviceId}`,
190: 'Device internal error due to device states not synchronized with server, or command format is invalid',
100: 'Command successfully sent',
200: 'Request successful',
400: 'Bad Request, an invalid payload request',
401: 'Unauthorized, Authorization for the API is required, but the request has not been authenticated',
403: 'Forbidden, The request has been authenticated but does not have appropriate permissions, or a requested resource is not found',
404: 'Not Found, Specifies the requested path does not exist',
406: 'Not Acceptable, a MIME type has been requested via the Accept header for a value not supported by the server',
415: 'Unsupported Media Type, a contentType header has been defined that is not supported by the server',
422: 'Unprocessable Entity: The server cannot process the request, often due to exceeded API limits.',
429: 'Too Many Requests, exceeded the number of requests allowed for a given time window',
500: 'Internal Server Error, An unexpected error occurred. These errors should be rare',
}
if (statusCode === 171 && (this.device.hubDeviceId === this.device.deviceId || this.device.hubDeviceId === '000000000000')) {
this.debugErrorLog(`statusCode 171 changed to 161: hubDeviceId ${this.device.hubDeviceId} matches deviceId ${this.device.deviceId}, device is its own hub.`)
statusCode = 161
}
const logMessage = statusMessages[statusCode] || `Unknown statusCode: ${statusCode}, Submit Bugs Here: https://tinyurl.com/SwitchBotBug`
const logMethod = [100, 200].includes(statusCode) ? 'debugLog' : statusMessages[statusCode] ? 'errorLog' : 'infoLog'
this[logMethod](`${logMessage}, statusCode: ${statusCode}`)
}
/**
* Logging for Device
*/
async infoLog(...log: any[]): Promise<void> {
if (await this.enablingDeviceLogging()) {
this.log.info(`${this.device.remoteType}: ${this.accessory.displayName}`, String(...log))
}
}
async successLog(...log: any[]): Promise<void> {
if (await this.enablingDeviceLogging()) {
this.log.success(`${this.device.remoteType}: ${this.accessory.displayName}`, String(...log))
}
}
async debugSuccessLog(...log: any[]): Promise<void> {
if (await this.enablingDeviceLogging()) {
if (await this.loggingIsDebug()) {
this.log.success(`[DEBUG] ${this.device.remoteType}: ${this.accessory.displayName}`, String(...log))
}
}
}
async warnLog(...log: any[]): Promise<void> {
if (await this.enablingDeviceLogging()) {
this.log.warn(`${this.device.remoteType}: ${this.accessory.displayName}`, String(...log))
}
}
async debugWarnLog(...log: any[]): Promise<void> {
if (await this.enablingDeviceLogging()) {
if (await this.loggingIsDebug()) {
this.log.warn(`[DEBUG] ${this.device.remoteType}: ${this.accessory.displayName}`, String(...log))
}
}
}
async errorLog(...log: any[]): Promise<void> {
if (await this.enablingDeviceLogging()) {
this.log.error(`${this.device.remoteType}: ${this.accessory.displayName}`, String(...log))
}
}
async debugErrorLog(...log: any[]): Promise<void> {
if (await this.enablingDeviceLogging()) {
if (await this.loggingIsDebug()) {
this.log.error(`[DEBUG] ${this.device.remoteType}: ${this.accessory.displayName}`, String(...log))
}
}
}
async debugLog(...log: any[]): Promise<void> {
if (await this.enablingDeviceLogging()) {
if (this.deviceLogging === 'debug') {
this.log.info(`[DEBUG] ${this.device.remoteType}: ${this.accessory.displayName}`, String(...log))
} else if (this.deviceLogging === 'debugMode') {
this.log.debug(`${this.device.remoteType}: ${this.accessory.displayName}`, String(...log))
}
}
}
async loggingIsDebug(): Promise<boolean> {
return this.deviceLogging === 'debugMode' || this.deviceLogging === 'debug'
}
async enablingDeviceLogging(): Promise<boolean> {
return this.deviceLogging === 'debugMode' || this.deviceLogging === 'debug' || this.deviceLogging === 'standard'
}
}