diff --git a/README.md b/README.md index c2d3986..28304e5 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ This plugin enables the use of a GDO BlaQ device with Homebridge (and derivative * Firmware Version Status * Motion Sensor Status * Play Pre-close Warning -* Obstruction Sensor Status (coming soon on v0.2.X) +* Obstruction Sensor Status It *could*, but does not currently, support: diff --git a/package-lock.json b/package-lock.json index 4309a1d..ff05d70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-blaq", - "version": "0.2.6", + "version": "0.2.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homebridge-blaq", - "version": "0.2.6", + "version": "0.2.7", "license": "Apache-2.0", "dependencies": { "bonjour-service": "^1.2.1", diff --git a/package.json b/package.json index 39417b5..413bc9d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": false, "displayName": "Konnected BlaQ", "name": "homebridge-blaq", - "version": "0.2.6", + "version": "0.2.7", "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-motion-sensor.ts b/src/accessory/garage-motion-sensor.ts index e95cbe1..3c91d7e 100644 --- a/src/accessory/garage-motion-sensor.ts +++ b/src/accessory/garage-motion-sensor.ts @@ -53,7 +53,7 @@ export class BlaQGarageMotionSensorAccessory implements BaseBlaQAccessory { }: BlaQGarageMotionSensorAccessoryConstructorParams) { this.platform = platform; this.logger = this.platform.logger; - this.logger.debug('Initializing BlaQGarageLightAccessory...'); + this.logger.debug('Initializing BlaQGarageMotionSensorAccessory...'); this.accessory = accessory; this.model = model; this.serialNumber = serialNumber; diff --git a/src/accessory/garage-obstruction-sensor.ts b/src/accessory/garage-obstruction-sensor.ts new file mode 100644 index 0000000..0f7fc76 --- /dev/null +++ b/src/accessory/garage-obstruction-sensor.ts @@ -0,0 +1,163 @@ +import { CharacteristicValue, Logger, PlatformAccessory, Service } from 'homebridge'; + +import { BlaQHomebridgePluginPlatform } from '../platform.js'; +import { + BlaQBinarySensorEvent, + 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 BlaQGarageObstructionSensorAccessoryConstructorParams = { + 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 BlaQGarageObstructionSensorAccessory implements BaseBlaQAccessory { + private logger: Logger; + private accessoryInformationService: Service; + private occupancySensorService: Service; + private apiBaseURL: string; + private firmwareVersion?: string; + private obstructionDetected?: boolean; + private readonly platform: BlaQHomebridgePluginPlatform; + private readonly accessory: PlatformAccessory; + private readonly model: string; + private readonly serialNumber: string; + + constructor({ + platform, + accessory, + model, + serialNumber, + apiBaseURL, + }: BlaQGarageObstructionSensorAccessoryConstructorParams) { + this.platform = platform; + this.logger = this.platform.logger; + this.logger.debug('Initializing BlaQGarageObstructionSensorAccessory...'); + this.accessory = accessory; + this.model = model; + this.serialNumber = serialNumber; + this.apiBaseURL = correctAPIBaseURL(apiBaseURL); + this.occupancySensorService = this.accessory.getService(this.platform.service.OccupancySensor) + || this.accessory.addService(this.platform.service.OccupancySensor); + + 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.occupancySensorService.setCharacteristic(this.platform.characteristic.Name, accessory.context.device.displayName); + + this.occupancySensorService.getCharacteristic(this.platform.characteristic.OccupancyDetected) + .onGet(this.getObstructionDetected.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 BlaQGarageObstructionSensorAccessory!'); + } + + getFirmwareVersion(): CharacteristicValue { + return this.firmwareVersion || ''; + } + + private setFirmwareVersion(version: string) { + this.firmwareVersion = version; + this.accessoryInformationService.setCharacteristic( + this.platform.characteristic.FirmwareRevision, + version, + ); + } + + getObstructionDetected(): CharacteristicValue { + return this.obstructionDetected || false; + } + + setObstructionDetected(obstructionDetected: boolean) { + this.obstructionDetected = obstructionDetected; + this.occupancySensorService.setCharacteristic( + this.platform.characteristic.OccupancyDetected, + this.obstructionDetected, + ); + } + + 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 (['binary_sensor-obstruction'].includes(stateInfo.id)) { + const sensorEvent = stateInfo as BlaQBinarySensorEvent; + if(['OFF', 'ON'].includes(sensorEvent.state.toUpperCase())){ + this.setObstructionDetected(sensorEvent.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 obstructionStateOn = + lowercaseLogStr.includes('obstruction') && lowercaseLogStr.includes('state') && lowercaseLogStr.includes('on'); + const obstructionStateOff = + lowercaseLogStr.includes('obstruction') && lowercaseLogStr.includes('state') && lowercaseLogStr.includes('off'); + const obstructionObstructed = + lowercaseLogStr.includes('obstruction') && lowercaseLogStr.includes('obstructed'); + const obstructionClear = + lowercaseLogStr.includes('obstruction') && lowercaseLogStr.includes('clear'); + if (obstructionStateOn || obstructionObstructed) { + this.setObstructionDetected(true); + } else if (obstructionStateOff || obstructionClear) { + this.setObstructionDetected(false); + } + } catch(e) { + this.logger.error('Log parsing error:', e); + } + } +} diff --git a/src/hub.ts b/src/hub.ts index 71c0b56..a142260 100644 --- a/src/hub.ts +++ b/src/hub.ts @@ -9,6 +9,7 @@ import { BlaQGarageLockAccessory } from './accessory/garage-lock.js'; import { BlaQGarageMotionSensorAccessory } from './accessory/garage-motion-sensor.js'; import { BlaQGaragePreCloseWarningAccessory } from './accessory/garage-pre-close-warning.js'; import { BlaQGarageLearnModeAccessory } from './accessory/garage-learn-mode.js'; +import { BlaQGarageObstructionSensorAccessory } from './accessory/garage-obstruction-sensor.js'; interface BlaQPingEvent { title: string; @@ -172,6 +173,20 @@ export class BlaQHub { })); } + private initGarageObstructionSensorAccessory({ model, serialNumber }: ModelAndSerialNumber){ + const accessorySuffix = 'obstruction-sensor'; + 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 BlaQGarageObstructionSensorAccessory({ + platform, accessory, model, serialNumber, apiBaseURL: this.getAPIBaseURL(), + })); + } + private initAccessories({ model, serialNumber }: ModelAndSerialNumber){ this.initGarageDoorAccessory({ model, serialNumber }); this.initGarageLightAccessory({ model, serialNumber }); @@ -179,6 +194,7 @@ export class BlaQHub { this.initGarageMotionSensorAccessory({ model, serialNumber }); this.initGaragePreCloseWarningAccessory({ model, serialNumber }); this.initGarageLearnModeAccessory({ model, serialNumber }); + this.initGarageObstructionSensorAccessory({ model, serialNumber }); } private handlePingUpdate(msg: PingMessageEvent){