diff --git a/src/Accessories/Sequence.ts b/src/Accessories/Sequence.ts new file mode 100644 index 0000000..5fc1265 --- /dev/null +++ b/src/Accessories/Sequence.ts @@ -0,0 +1,101 @@ +import { + CharacteristicGetCallback, + CharacteristicSetCallback, + PlatformAccessory, + Service, +} from "homebridge"; +import HarmonyDataProvider from "../DataProviders/HarmonyDataProvider"; +import { ISequence } from "../Models/Config/ISequence"; +import { HarmonyDevice } from "../Models/HarmonyDevice"; +import { Platform } from "../platform"; +import { sleep } from "../Util"; + +export class Sequence { + private _devices: { [deviceName: string]: HarmonyDevice }; + private _switchService: Service; + + constructor( + private readonly _platform: Platform, + private readonly _accessory: PlatformAccessory, + private _dataProvider: HarmonyDataProvider, + private _sequence: ISequence + ) { + this._accessory + .getService(this._platform.Service.AccessoryInformation)! + .setCharacteristic( + this._platform.Characteristic.Manufacturer, + "Brandon Watson" + ) + .setCharacteristic(this._platform.Characteristic.Model, "Sequence Button") + .setCharacteristic( + this._platform.Characteristic.SerialNumber, + "123-456-789" + ); + + const switchUUID = this._platform.api.hap.uuid.generate( + `${this._accessory.displayName} Switch` + ); + + this._switchService = + this._accessory.getService(this._platform.Service.Switch) || + this._accessory.addService( + this._platform.Service.Switch, + this._accessory.displayName, + switchUUID + ); + + this._switchService + .getCharacteristic(this._platform.Characteristic.On) + .on("set", this.onSwitchSet) + .updateValue(false) + .on("get", (callback: CharacteristicGetCallback): void => { + return callback(null); + }); + + this._devices = {}; + // Get devices in sequence + for (const deviceName of _sequence.Steps.map((e) => e.DeviceName)) { + if (!deviceName) { + continue; + } + const device = this._dataProvider.getDeviceFromName(deviceName); + if (device) { + this._devices[deviceName] = device; + } else { + this._platform.log.warn( + `Device ${deviceName} was not found in harmony configuration` + ); + } + } + } + + /** + * Handler for switchSet command + * @param callback + */ + public async onSwitchSet(callback: CharacteristicSetCallback): Promise { + // Execute sequence + for (const step of this._sequence.Steps) { + await sleep(step.Delay); + const device: HarmonyDevice = this._devices[step.DeviceName ?? ""]; + if ( + device && + step.DeviceCommand && + device.supportsCommand(step.DeviceCommand) + ) { + await device.sendCommand(step.DeviceCommand); + } else { + this._platform.log.warn( + `Attempted to execute command ${step.DeviceCommand} on device ${step.DeviceName} but the device or command was not found` + ); + } + } + + // Deactivate button + this._switchService + .getCharacteristic(this._platform.Characteristic.On) + .updateValue(false); + + callback(null); + } +} diff --git a/src/Models/Config/ISequence.ts b/src/Models/Config/ISequence.ts index 9ea26c3..fae3b16 100644 --- a/src/Models/Config/ISequence.ts +++ b/src/Models/Config/ISequence.ts @@ -1,10 +1,10 @@ export interface ISequence { - name: string; - steps: Array; + DisplayName: string; + Steps: Array; } export interface IStep { - deviceName: string; - deviceCommand: string; - delay: number; + DeviceName?: string; + DeviceCommand?: string; + Delay: number; } diff --git a/src/platform.ts b/src/platform.ts index 3614aaf..0dcf19d 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -8,8 +8,10 @@ import { Service, } from "homebridge"; import { ControlUnit, DeviceButton } from "./Accessories"; +import { Sequence } from "./Accessories/Sequence"; import HarmonyDataProvider from "./DataProviders/HarmonyDataProvider"; import { IConfig, IControlUnit, IDeviceButton } from "./Models/Config"; +import { ISequence } from "./Models/Config/ISequence"; import { HarmonyDevice } from "./Models/HarmonyDevice"; import { HarmonyHub } from "./Models/HarmonyHub"; import { PLATFORM_NAME, PLUGIN_NAME } from "./settings"; @@ -33,7 +35,6 @@ export class Platform implements DynamicPlatformPlugin { this.api.on("didFinishLaunching", async () => { log.debug("Executed didFinishLaunching callback"); - this.discoverDevices(dataProvider); }); this.dataProvider = null; @@ -51,6 +52,9 @@ export class Platform implements DynamicPlatformPlugin { this.dataProvider.on("Ready", () => { this.log.info("All hubs connected"); + this.discoverControlUnitAccessories(dataProvider); + this.discoverDeviceButtonAccessories(dataProvider); + this.discoverSequenceAccessories(dataProvider); if (this.config.EmitDevicesOnStartup) { const hubs = this.dataProvider!.hubs; Object.values(hubs).forEach((hub: HarmonyHub) => { @@ -78,10 +82,17 @@ export class Platform implements DynamicPlatformPlugin { public config: IConfig; public dataProvider: HarmonyDataProvider | null; - public discoverDevices(dataProvider: HarmonyDataProvider) { + /** + * Discover new control unit accessories + * @param dataProvider + */ + private discoverControlUnitAccessories( + dataProvider: HarmonyDataProvider + ): void { this.config.ControlUnits.forEach((unit: IControlUnit) => { const uuid = this.api.hap.uuid.generate(unit.DisplayName); const existingAccessory = this.accessories.find((e) => e.UUID === uuid); + if (existingAccessory) { this.log.info( "Restoring existing accessory from cache: " + @@ -106,7 +117,15 @@ export class Platform implements DynamicPlatformPlugin { console.log("Publishing external accessory: " + uuid); } }); + } + /** + * Discover new device button accessories + * @param dataProvider + */ + private discoverDeviceButtonAccessories( + dataProvider: HarmonyDataProvider + ): void { this.config.DeviceButtons.forEach((button: IDeviceButton) => { const uuid = this.api.hap.uuid.generate(button.DisplayName); const existingAccessory = this.accessories.find((e) => e.UUID === uuid); @@ -133,6 +152,65 @@ export class Platform implements DynamicPlatformPlugin { ]); } }); + + // Remove old device buttons + for (const accessory of this.accessories) { + if ( + this.config.DeviceButtons.filter( + (button) => button.DisplayName === accessory.displayName + ).length === 0 + ) { + this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [ + accessory, + ]); + } + } + } + + /** + * Discover new sequence accessories + * @param dataProvider + */ + public discoverSequenceAccessories(dataProvider: HarmonyDataProvider): void { + this.config.Sequences.forEach((sequence: ISequence) => { + const uuid = this.api.hap.uuid.generate(sequence.DisplayName); + const existingAccessory = this.accessories.find((e) => e.UUID === uuid); + if (existingAccessory) { + this.log.info( + "Restoring existing accessory from cache: " + + existingAccessory.displayName + ); + + new Sequence(this, existingAccessory, dataProvider, sequence); + this.api.updatePlatformAccessories([existingAccessory]); + } else { + this.log.info("Adding new accessory: " + sequence.DisplayName); + const accessory = new this.api.platformAccessory( + sequence.DisplayName, + uuid + ); + accessory.context["DeviceName"] = sequence.DisplayName; + + new Sequence(this, accessory, dataProvider, sequence); + + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [ + accessory, + ]); + } + }); + + // Remove old device buttons + for (const accessory of this.accessories) { + if ( + this.config.Sequences.filter( + (sequence) => sequence.DisplayName === accessory.displayName + ).length === 0 + ) { + this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [ + accessory, + ]); + } + } } configureAccessory(accessory: PlatformAccessory>): void {