UNPKG

@switchbot/homebridge-switchbot

Version:

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

345 lines (312 loc) • 15.7 kB
/* 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' } }