From 79373e040aaf361d820a0b5b31030b03381ed521 Mon Sep 17 00:00:00 2001 From: kyleboyer Date: Sat, 13 Jul 2024 12:27:31 -0500 Subject: [PATCH] Added pre-close-warning --- README.md | 4 +- package-lock.json | 4 +- package.json | 2 +- src/accessory/garage-pre-close-warning.ts | 178 ++++++++++++++++++++++ src/hub.ts | 16 ++ 5 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 src/accessory/garage-pre-close-warning.ts diff --git a/README.md b/README.md index b9ad3a1..27aa7d8 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ This plugin enables the use of a GDO BlaQ device with Homebridge (and derivative * Garage Light Status/Control * Garage Remote Lock Status/Control * Firmware Version Status +* Motion Sensor Status +* Play Pre-close Warning * Obstruction Sensor Status (coming soon on v0.2.X) -* Motion Sensor Status (coming soon on v0.2.X) -* Play Pre-close Warning (coming soon on v0.2.X) It *could*, but does not currently, support: diff --git a/package-lock.json b/package-lock.json index 5f7b6c2..ffd5f3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-blaq", - "version": "0.2.3", + "version": "0.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homebridge-blaq", - "version": "0.2.3", + "version": "0.2.4", "license": "Apache-2.0", "dependencies": { "bonjour-service": "^1.2.1", diff --git a/package.json b/package.json index 6d038a2..b77c617 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": false, "displayName": "Konnected BlaQ", "name": "homebridge-blaq", - "version": "0.2.3", + "version": "0.2.4", "description": "Control and view your garage door(s) remotely with real-time updates using Konnected's BlaQ hardware", "license": "Apache-2.0", "type": "module", diff --git a/src/accessory/garage-pre-close-warning.ts b/src/accessory/garage-pre-close-warning.ts new file mode 100644 index 0000000..4ae0b50 --- /dev/null +++ b/src/accessory/garage-pre-close-warning.ts @@ -0,0 +1,178 @@ +import { CharacteristicValue, Logger, PlatformAccessory, Service } from 'homebridge'; +import fetch from 'node-fetch'; // I am, in fact, trying to make fetch happen. + +import { BlaQHomebridgePluginPlatform } from '../platform.js'; +import { + BlaQButtonEvent, + BlaQTextSensorEvent, +} from '../types.js'; +import { LogMessageEvent, StateUpdateMessageEvent, StateUpdateRecord } from '../utils/eventsource.js'; +import { BaseBlaQAccessory } from './base.js'; + +const correctAPIBaseURL = (inputURL: string) => { + let correctedAPIBaseURL = inputURL; + if(!correctedAPIBaseURL.includes('://')){ + correctedAPIBaseURL = `http://${correctedAPIBaseURL}`; + } + if(correctedAPIBaseURL.endsWith('/')){ + correctedAPIBaseURL = correctedAPIBaseURL.slice(0, -1); + } + return correctedAPIBaseURL; +}; + +type BlaQGaragePreCloseWarningAccessoryConstructorParams = { + platform: BlaQHomebridgePluginPlatform; + accessory: PlatformAccessory; + model: string; + serialNumber: string; + apiBaseURL: string; +}; + +/** + * Platform Accessory + * An instance of this class is created for each accessory your platform registers + * Each accessory may expose multiple services of different service types. + */ +export class BlaQGaragePreCloseWarningAccessory implements BaseBlaQAccessory { + private logger: Logger; + private accessoryInformationService: Service; + private outletService: Service; + private apiBaseURL: string; + private firmwareVersion?: string; + private isOn?: boolean; + private readonly platform: BlaQHomebridgePluginPlatform; + private readonly accessory: PlatformAccessory; + private readonly model: string; + private readonly serialNumber: string; + + constructor({ + platform, + accessory, + model, + serialNumber, + apiBaseURL, + }: BlaQGaragePreCloseWarningAccessoryConstructorParams) { + this.platform = platform; + this.logger = this.platform.logger; + this.logger.debug('Initializing BlaQGaragePreCloseWarningAccessory...'); + this.accessory = accessory; + this.model = model; + this.serialNumber = serialNumber; + this.apiBaseURL = correctAPIBaseURL(apiBaseURL); + this.outletService = this.accessory.getService(this.platform.service.Outlet) + || this.accessory.addService(this.platform.service.Outlet); + + this.accessoryInformationService = this.accessory.getService(this.platform.service.AccessoryInformation) + || this.accessory.addService(this.platform.service.AccessoryInformation); + + // set accessory information + this.accessoryInformationService + .setCharacteristic(this.platform.characteristic.Manufacturer, 'Konnected') + .setCharacteristic(this.platform.characteristic.Model, this.model) + .setCharacteristic(this.platform.characteristic.SerialNumber, this.serialNumber); + + // Set the service name. This is what is displayed as the name on the Home + // app. We use what we stored in `accessory.context` in `discoverDevices`. + this.outletService.setCharacteristic(this.platform.characteristic.Name, accessory.context.device.displayName); + + this.outletService.getCharacteristic(this.platform.characteristic.On) + .onGet(this.getIsOn.bind(this)) + .onSet(this.changeIsOn.bind(this)); + + // Publish firmware version; this may not be initialized yet, so we set a getter. + // Note that this is against the AccessoryInformation service, not the GDO service. + this.accessoryInformationService + .getCharacteristic(this.platform.characteristic.FirmwareRevision) + .onGet(this.getFirmwareVersion.bind(this)); + this.logger.debug('Initialized BlaQGaragePreCloseWarningAccessory!'); + } + + getFirmwareVersion(): CharacteristicValue { + return this.firmwareVersion || ''; + } + + private setFirmwareVersion(version: string) { + this.firmwareVersion = version; + this.accessoryInformationService.setCharacteristic( + this.platform.characteristic.FirmwareRevision, + version, + ); + } + + getIsOn(): CharacteristicValue { + return this.isOn || false; + } + + setIsOn(isOn: boolean) { + this.isOn = isOn; + this.outletService.setCharacteristic( + this.platform.characteristic.On, + this.isOn, + ); + } + + private async changeIsOn(target: CharacteristicValue){ + if(target && target !== this.isOn){ // only call the API when target = true (button on) + await fetch(`${this.apiBaseURL}/button/pre_close_warning/press`, {method: 'POST'}); + } + } + + setAPIBaseURL(url: string){ + this.apiBaseURL = correctAPIBaseURL(url); + } + + handleStateEvent(stateEvent: StateUpdateMessageEvent){ + this.logger.debug('Processing state event:', stateEvent.data); + try { + const stateInfo = JSON.parse(stateEvent.data) as StateUpdateRecord; + if (['button-pre-close_warning'].includes(stateInfo.id)) { + const buttonEvent = stateInfo as BlaQButtonEvent & { state: 'ON' | 'OFF' }; + if(['OFF', 'ON'].includes(buttonEvent.state.toUpperCase())){ + this.setIsOn(buttonEvent.state.toUpperCase() === 'ON'); + } + } else if (['text_sensor-esphome_version', 'text_sensor-firmware_version'].includes(stateInfo.id)) { + const b = stateInfo as BlaQTextSensorEvent; + if (b.value === b.state && b.value !== '' && b.value !== null && b.value !== undefined) { + this.logger.info('Firmware version:', b.value); + this.setFirmwareVersion(b.value); + } else { + this.logger.error('Mismatched firmware versions in value/state:', b.value, b.state); + this.firmwareVersion = undefined; + } + } + } catch(e) { + this.logger.error('Cannot deserialize message:', stateEvent); + this.logger.error('Deserialization yielded:', e); + } + } + + handleLogEvent(logEvent: LogMessageEvent){ + this.logger.debug('BlaQ log:', logEvent.data); + try { + const logStr = logEvent.data; + const lowercaseLogStr = logStr.toLowerCase(); + const preCloseWarningPressed = + lowercaseLogStr.includes('pre') && + lowercaseLogStr.includes('close') && + lowercaseLogStr.includes('warning') && + lowercaseLogStr.includes('pressed'); + const playSoundPressed = + lowercaseLogStr.includes('play') && + lowercaseLogStr.includes('sound') && + lowercaseLogStr.includes('pressed'); + const playingSong = + lowercaseLogStr.includes('playing') && + lowercaseLogStr.includes('song'); + const playbackFinished = + lowercaseLogStr.includes('playback') && + lowercaseLogStr.includes('finished'); + if (preCloseWarningPressed || playSoundPressed || playingSong) { + this.setIsOn(true); + } else if (playbackFinished) { + this.setIsOn(false); + } + } catch(e) { + this.logger.error('Log parsing error:', e); + } + } +} diff --git a/src/hub.ts b/src/hub.ts index f8d28eb..eb78e79 100644 --- a/src/hub.ts +++ b/src/hub.ts @@ -7,6 +7,7 @@ import { BlaQGarageDoorAccessory } from './accessory/garage-door.js'; import { BlaQGarageLightAccessory } from './accessory/garage-light.js'; import { BlaQGarageLockAccessory } from './accessory/garage-lock.js'; import { BlaQGarageMotionSensorAccessory } from './accessory/garage-motion-sensor.js'; +import { BlaQGaragePreCloseWarningAccessory } from './accessory/garage-pre-close-warning.js'; interface BlaQPingEvent { title: string; @@ -142,11 +143,26 @@ export class BlaQHub { })); } + private initGaragePreCloseWarningAccessory({ model, serialNumber }: ModelAndSerialNumber){ + const accessorySuffix = 'pre-close-warning'; + const nonMainAccessoryMACAddress = this.configDevice.mac && `${this.configDevice.mac}-${accessorySuffix}`; + const nonMainAccessorySerialNumber = `${serialNumber}-${accessorySuffix}`; + const {platform, accessory} = this.initAccessoryCallback( + { ...this.configDevice, mac: nonMainAccessoryMACAddress }, + model, + nonMainAccessorySerialNumber, + ); + this.accessories.push(new BlaQGaragePreCloseWarningAccessory({ + platform, accessory, model, serialNumber, apiBaseURL: this.getAPIBaseURL(), + })); + } + private initAccessories({ model, serialNumber }: ModelAndSerialNumber){ this.initGarageDoorAccessory({ model, serialNumber }); this.initGarageLightAccessory({ model, serialNumber }); this.initGarageLockAccessory({ model, serialNumber }); this.initGarageMotionSensorAccessory({ model, serialNumber }); + this.initGaragePreCloseWarningAccessory({ model, serialNumber }); } private handlePingUpdate(msg: PingMessageEvent){