diff --git a/package-lock.json b/package-lock.json index 4f9203d..bb5f37f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-blaq", - "version": "0.2.1", + "version": "0.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homebridge-blaq", - "version": "0.2.1", + "version": "0.2.2", "license": "Apache-2.0", "dependencies": { "bonjour-service": "^1.2.1", diff --git a/package.json b/package.json index ef09aec..9d3289f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": false, "displayName": "Konnected BlaQ", "name": "homebridge-blaq", - "version": "0.2.1", + "version": "0.2.2", "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-light.ts b/src/accessory/garage-light.ts index 4bfe6b3..cac3575 100644 --- a/src/accessory/garage-light.ts +++ b/src/accessory/garage-light.ts @@ -58,7 +58,7 @@ export class BlaQGarageLightAccessory implements BaseBlaQAccessory { }: BlaQGarageLightAccessoryConstructorParams) { this.platform = platform; this.logger = this.platform.logger; - this.logger.debug('Initializing BlaQGarageDoorAccessory...'); + this.logger.debug('Initializing BlaQGarageLightAccessory...'); this.accessory = accessory; this.model = model; this.serialNumber = serialNumber; @@ -88,7 +88,7 @@ export class BlaQGarageLightAccessory implements BaseBlaQAccessory { this.accessoryInformationService .getCharacteristic(this.platform.characteristic.FirmwareRevision) .onGet(this.getFirmwareVersion.bind(this)); - this.logger.debug('Initialized BlaQGarageDoorAccessory!'); + this.logger.debug('Initialized BlaQGarageLightAccessory!'); } getFirmwareVersion(): CharacteristicValue { diff --git a/src/accessory/garage-lock.ts b/src/accessory/garage-lock.ts index 51fa2d2..f69a8e2 100644 --- a/src/accessory/garage-lock.ts +++ b/src/accessory/garage-lock.ts @@ -58,7 +58,7 @@ export class BlaQGarageLockAccessory implements BaseBlaQAccessory { }: BlaQGarageLockAccessoryConstructorParams) { this.platform = platform; this.logger = this.platform.logger; - this.logger.debug('Initializing BlaQGarageDoorAccessory...'); + this.logger.debug('Initializing BlaQGarageLockAccessory...'); this.accessory = accessory; this.model = model; this.serialNumber = serialNumber; @@ -90,7 +90,7 @@ export class BlaQGarageLockAccessory implements BaseBlaQAccessory { this.accessoryInformationService .getCharacteristic(this.platform.characteristic.FirmwareRevision) .onGet(this.getFirmwareVersion.bind(this)); - this.logger.debug('Initialized BlaQGarageDoorAccessory!'); + this.logger.debug('Initialized BlaQGarageLockAccessory!'); } getFirmwareVersion(): CharacteristicValue { diff --git a/src/accessory/garage-motion-sensor.ts b/src/accessory/garage-motion-sensor.ts new file mode 100644 index 0000000..e95cbe1 --- /dev/null +++ b/src/accessory/garage-motion-sensor.ts @@ -0,0 +1,155 @@ +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 BlaQGarageMotionSensorAccessoryConstructorParams = { + 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 BlaQGarageMotionSensorAccessory implements BaseBlaQAccessory { + private logger: Logger; + private accessoryInformationService: Service; + private motionSensorService: Service; + private apiBaseURL: string; + private firmwareVersion?: string; + private motionDetected?: boolean; + private readonly platform: BlaQHomebridgePluginPlatform; + private readonly accessory: PlatformAccessory; + private readonly model: string; + private readonly serialNumber: string; + + constructor({ + platform, + accessory, + model, + serialNumber, + apiBaseURL, + }: BlaQGarageMotionSensorAccessoryConstructorParams) { + this.platform = platform; + this.logger = this.platform.logger; + this.logger.debug('Initializing BlaQGarageLightAccessory...'); + this.accessory = accessory; + this.model = model; + this.serialNumber = serialNumber; + this.apiBaseURL = correctAPIBaseURL(apiBaseURL); + this.motionSensorService = this.accessory.getService(this.platform.service.MotionSensor) + || this.accessory.addService(this.platform.service.MotionSensor); + + 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.motionSensorService.setCharacteristic(this.platform.characteristic.Name, accessory.context.device.displayName); + + this.motionSensorService.getCharacteristic(this.platform.characteristic.MotionDetected) + .onGet(this.getMotionDetected.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 BlaQGarageMotionSensorAccessory!'); + } + + getFirmwareVersion(): CharacteristicValue { + return this.firmwareVersion || ''; + } + + private setFirmwareVersion(version: string) { + this.firmwareVersion = version; + this.accessoryInformationService.setCharacteristic( + this.platform.characteristic.FirmwareRevision, + version, + ); + } + + getMotionDetected(): CharacteristicValue { + return this.motionDetected || false; + } + + setMotionDetected(motionDetected: boolean) { + this.motionDetected = motionDetected; + this.motionSensorService.setCharacteristic( + this.platform.characteristic.MotionDetected, + this.motionDetected, + ); + } + + 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-motion'].includes(stateInfo.id)) { + const sensorEvent = stateInfo as BlaQBinarySensorEvent; + if(['OFF', 'ON'].includes(sensorEvent.state.toUpperCase())){ + this.setMotionDetected(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(); + if (lowercaseLogStr.includes('motion') && lowercaseLogStr.includes('state') && lowercaseLogStr.includes('on')) { + this.setMotionDetected(true); + } else if (lowercaseLogStr.includes('motion') && lowercaseLogStr.includes('state') && lowercaseLogStr.includes('off')) { + this.setMotionDetected(false); + } + } catch(e) { + this.logger.error('Log parsing error:', e); + } + } +}