diff --git a/config.schema.json b/config.schema.json index 9d91164..ec1b5e7 100644 --- a/config.schema.json +++ b/config.schema.json @@ -3,6 +3,18 @@ "pluginType": "platform", "singular": true, "schema": { + "name": { + "title": "Platform Name", + "type": "string", + "required": "true", + "default": "MultiTapSwitch" + }, + "enabled": { + "title": "Enable", + "type": "boolean", + "required": true, + "default": true + }, "devices": { "type": "array", "items": { @@ -14,17 +26,29 @@ "type": "string", "required": true }, - "numScenes": { + "numberConfiguredScenes": { "title": "Max. Scenes", "type": "number", "required": true, "default": 5 }, - "secondsToKeep": { + "triggerTimeout": { "title": "Reset Timeout", "type": "number", "required": true, "default": 10 + }, + "resetAfterSwitchOff": { + "title": "Reset after Switch-Off", + "type": "boolean", + "required": true, + "default": false + }, + "logging": { + "title": "Log state changes", + "type": "boolean", + "required": true, + "default": false } } } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..f78be26 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,69 @@ +import {PlatformConfig} from 'homebridge'; + +export interface PluginBaseConfig { + Name: string; + Enable: boolean; +} + +export interface PluginDeviceConfig { + readonly name: string; + readonly numberConfiguredScenes: number; + readonly triggerTimeout: number; + readonly resetAfterSwitchOff: boolean; + readonly logging: boolean; +} + +export class PluginConfig { + readonly Name: string; + readonly Enable: boolean; + readonly Devices: PluginDeviceConfig[]; + + constructor(config: PlatformConfig) { + this.Name = config.name!; + this.Enable = config.enable; + + this.Devices = config.devices; + } +} + +export class DeviceConfig { + private device: PluginDeviceConfig; + + constructor(item: PluginDeviceConfig) { + this.device = item; + } + + Name() { + return this.device.name; + } + + NumOfScenes() { + return this.device.numberConfiguredScenes; + } + + TriggerTimeout() { + return this.device.triggerTimeout; + } + + isResetWhenOff() { + return this.device.resetAfterSwitchOff; + } + + isLogging() { + return this.device.logging; + } +} + +export interface AccessoryState { + numberConfiguredScenes: number; + triggerTimeout: number; + //nextSceneToTrigger: number; +} + +export function initializeAccessoryState(initConfig: PluginDeviceConfig | AccessoryState): AccessoryState { + return { + numberConfiguredScenes: initConfig.numberConfiguredScenes, + triggerTimeout: initConfig.triggerTimeout, + //nextSceneToTrigger: 0, + }; +} diff --git a/src/helper/logger.ts b/src/helper/logger.ts new file mode 100644 index 0000000..53d1d44 --- /dev/null +++ b/src/helper/logger.ts @@ -0,0 +1,31 @@ +import {Logger} from 'homebridge'; + +export class Logging { + private readonly logHandler: Logger; + private readonly prefix: string; + private readonly isDebug: boolean; + + constructor( + log: Logger, + prefix: string, + isDebug: boolean, + ) { + this.logHandler = log; + this.prefix = prefix; + this.isDebug = isDebug; + } + + private msgPrefix() { + return '[' + this.prefix + ']'; + } + + log(...parts: any) { + this.isDebug ? + this.logHandler.debug(this.msgPrefix(), ...parts) : + this.logHandler.info(this.msgPrefix(), ...parts); + } + + warn = (...parts: any) => this.logHandler.warn(this.msgPrefix(), ...parts); + error = (...parts: any) => this.logHandler.error(this.msgPrefix(), ...parts); + debug = (...parts: any) => this.logHandler.debug(this.msgPrefix(), ...parts); +} \ No newline at end of file diff --git a/src/platform.ts b/src/platform.ts index fcfa2e6..d0fade0 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -6,21 +6,19 @@ import { DeviceAccessory } from './platformAccessory'; // Necessary definitions for custom characteristics! import ConfiguredScenes from './types/characteristicConfiguredScenes'; let IConfiguredScenes; -import TriggerTimeout from './types/characteristicTimeout'; +import {initializeAccessoryState, PluginConfig} from './config'; let ITriggerTimeout; - -interface PlatformConfigData { - Name: string; - MaxNumScenes: number; - SecondsBeforeReset: number; -} +import TriggerTimeout from './types/characteristicTimeout'; /** * HomebridgePlatform */ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin { public readonly Service: typeof Service = this.api.hap.Service; - public readonly Characteristic: typeof Characteristic & typeof IConfiguredScenes & typeof ITriggerTimeout; + public readonly Characteristic: + typeof Characteristic & + typeof IConfiguredScenes & + typeof ITriggerTimeout; private readonly CharacteristicConfiguredScenes; private readonly CharacteristicTriggerTimeout; @@ -28,12 +26,16 @@ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin { // Track restored cached accessories public readonly accessories: PlatformAccessory[] = []; + public readonly Config: PluginConfig; + constructor( public readonly log: Logger, public readonly config: PlatformConfig, public readonly api: API, ) { - this.log.debug('Finished initializing platform:', this.config.name); + this.Config = new PluginConfig(this.config); + + this.log.debug('Finished initializing platform:', this.Config.Name); this.CharacteristicConfiguredScenes = ConfiguredScenes(this.api); IConfiguredScenes = this.CharacteristicConfiguredScenes; @@ -54,7 +56,7 @@ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin { } configureAccessory(accessory: PlatformAccessory) { - this.log.info('Loading accessory from cache:', accessory.displayName); + this.log.info('Load cached Accessory:', accessory.displayName); // add the restored accessory to the accessories cache so we can track if it has already been registered this.accessories.push(accessory); @@ -65,14 +67,9 @@ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin { } discoverDevices() { - for (let i = 0; i < this.config.devices.length; i++) { - const accessoryConfig: PlatformConfigData = { - Name: this.config.devices[i].name, - MaxNumScenes: this.config.devices[i].numScenes, - SecondsBeforeReset: this.config.devices[i].secondsToKeep, - }; - - const uuid = this.defineUuid(accessoryConfig.Name, '0'); + const parsedConfig = new PluginConfig(this.config); + for (const device of parsedConfig.Devices) { + const uuid = this.defineUuid(device.name, '0'); // see if an accessory with the same uuid has already been registered and restored from // the cached devices we stored in the `configureAccessory` method above @@ -85,8 +82,9 @@ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin { // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: // existingAccessory.context.device = device; // this.api.updatePlatformAccessories([existingAccessory]); - if (existingAccessory.context.config !== accessoryConfig) { - existingAccessory.context.config = accessoryConfig; + if (existingAccessory.context.config !== device) { + this.log.info('Update Accessory ->', device.name); + existingAccessory.context.config = device; this.api.updatePlatformAccessories([existingAccessory]); } @@ -95,14 +93,16 @@ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin { new DeviceAccessory(this, existingAccessory); } else { // Accessory does not yet exist, so we need to create it - this.log.info('Adding INPUT:', accessoryConfig.Name, ' / Max ', - accessoryConfig.MaxNumScenes, ' scenes / ', accessoryConfig.SecondsBeforeReset, ' seconds before reset'); + this.log.info('Add Device ->', device.name, '/', + 'Max', device.numberConfiguredScenes, 'scenes /', + device.triggerTimeout, 'seconds before reset'); // Create new accessory - const accessory = new this.api.platformAccessory(accessoryConfig.Name, uuid); + const accessory = new this.api.platformAccessory(device.name, uuid); // Store config in accessory context - accessory.context.config = accessoryConfig; + accessory.context.config = device; + accessory.context.persistent = initializeAccessoryState(device); // Create the accessory handler for the new accessory // This is imported from `platformAccessory.ts` diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index 509b2ce..95fdcd3 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -1,10 +1,7 @@ import {CharacteristicValue, PlatformAccessory, Service} from 'homebridge'; import {MultiTapSwitchPlatform} from './platform'; - -interface AccessoryState { - NumConfiguredScenes: number; - SecondsToKeepState: number; -} +import {AccessoryState, DeviceConfig} from './config'; +import {Logging} from './helper/logger'; /** * Platform Accessory @@ -22,57 +19,55 @@ export class DeviceAccessory { }; private readonly State: AccessoryState; + private readonly Config: DeviceConfig; private readonly TriggerButtons: Service[] = []; private SwitchResetTimer: NodeJS.Timeout | undefined; private SwitchOn = false; private NextItemToTrigger = 0; + private Log: Logging; + constructor( private readonly platform: MultiTapSwitchPlatform, private readonly accessory: PlatformAccessory, ) { - if (this.accessory.context.persistent) { - this.platform.log.debug('Restore previous state'); - this.State = this.accessory.context.persistent; - } else { - this.platform.log.debug('First run, use configuration data'); - this.State = { - NumConfiguredScenes: accessory.context.config.MaxNumScenes, - SecondsToKeepState: accessory.context.config.SecondsBeforeReset, - }; - } + this.Config = new DeviceConfig(accessory.context.config); + this.State = this.accessory.context.persistent; + + this.Log = new Logging(platform.log, this.Config.Name(), this.Config.isLogging()); + this.Log.log('Initialize Accessory', this.Config.Name()); // Set accessory information this.accessory.getService(this.platform.Service.AccessoryInformation)! - .setCharacteristic(this.platform.Characteristic.Manufacturer, 'TippyTapper Solutions') + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'TippyTapper') .setCharacteristic(this.platform.Characteristic.Model, 'SceneSwitch') - .setCharacteristic(this.platform.Characteristic.SerialNumber, 'DE:AD:BE:EF:00:00'); + .setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.UUID); // Get or Create the input switch service this.serviceIn = this.accessory.getService(this.platform.Service.Switch) || this.accessory.addService(this.platform.Service.Switch); - // register handlers for the On/Off Characteristic + // Register handlers for the On/Off Characteristic this.serviceIn.getCharacteristic(this.platform.Characteristic.On) .onSet(this.setOn.bind(this)) .onGet(this.getOn.bind(this)); // Register handlers for the NumScenes characteristic & restore previous value - if (this.State.NumConfiguredScenes > this.accessory.context.config.MaxNumScenes) { - this.State.NumConfiguredScenes = this.accessory.context.config.MaxNumScenes; + if (this.State.numberConfiguredScenes > this.Config.NumOfScenes()) { + this.State.numberConfiguredScenes = this.Config.NumOfScenes(); } (this.serviceIn.getCharacteristic(this.platform.Characteristic.ConfiguredScenes) || this.serviceIn.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredScenes)) .setProps({ 'minValue': 0, - 'maxValue': this.accessory.context.config.MaxNumScenes, + 'maxValue': this.Config.NumOfScenes(), 'minStep': 1, }) .onSet(this.setItemsToHandle.bind(this)) .onGet(this.getItemsToHandle.bind(this)) - .updateValue(this.State.NumConfiguredScenes); + .updateValue(this.State.numberConfiguredScenes); // Register handlers for the TriggerTimeout characteristic & restore previous value (this.serviceIn.getCharacteristic(this.platform.Characteristic.TriggerTimeout) @@ -84,7 +79,10 @@ export class DeviceAccessory { }) .onSet(this.setTriggerTimeout.bind(this)) .onGet(this.getTriggerTimeout.bind(this)) - .updateValue(this.State.SecondsToKeepState); + .updateValue(this.State.triggerTimeout); + + // Set the service name, this is what is displayed as the default name on the Home app + this.serviceIn.setCharacteristic(this.platform.Characteristic.Name, this.Config.Name()); // Get or Create the output service (excluding the buttons) this.serviceOut = this.accessory.getService(this.platform.Service.ServiceLabel) @@ -92,12 +90,12 @@ export class DeviceAccessory { this.serviceOut.setCharacteristic(this.platform.Characteristic.ServiceLabelNamespace, 1); // Create max configured number of scene buttons - for (let i = 1; i <= this.accessory.context.config.MaxNumScenes; i++) { - this.platform.log.debug('Handle Scene Button -> ', i); + for (let i = 1; i <= this.Config.NumOfScenes(); i++) { + this.Log.debug('Handle Scene Button -> ', i); let newButton = this.accessory.getService('Scene ' + i); if (!newButton) { - this.platform.log.debug('Add new Button'); + this.Log.debug('Add new Button'); newButton = this.accessory.addService(this.platform.Service.StatelessProgrammableSwitch, 'Scene ' + i, 'scene' + i); this.serviceOut.addLinkedService(newButton); @@ -113,7 +111,7 @@ export class DeviceAccessory { for (let i = this.TriggerButtons.length + 1;; i++) { const exButton = this.accessory.getService('Scene ' + i); if (exButton) { - this.platform.log.debug('Remove Scene Button -> ', i); + this.Log.debug('Remove Scene Button -> ', i); this.serviceOut.removeLinkedService(exButton); accessory.removeService(exButton); } else { @@ -122,48 +120,60 @@ export class DeviceAccessory { } // Set the service name, this is what is displayed as the default name on the Home app - this.serviceOut.setCharacteristic(this.platform.Characteristic.Name, accessory.context.config.Name); - - // Store persistent data - this.accessory.context.persistent = this.State; + this.serviceOut.setCharacteristic(this.platform.Characteristic.Name, this.Config.Name()); } - triggerNextItem() { - if (this.NextItemToTrigger >= this.State.NumConfiguredScenes) { + /** + * Trigger the next event + */ + triggerNext() { + if (this.NextItemToTrigger >= this.State.numberConfiguredScenes) { this.NextItemToTrigger = 0; } // Trigger Scene Button - this.platform.log.debug('Trigger Scene -> ', this.NextItemToTrigger); + this.Log.log('Trigger Scene -> ', this.NextItemToTrigger); this.TriggerButtons[this.NextItemToTrigger++].updateCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent, 0); } + /** + * Reset the trigger mechanism + */ + resetTrigger() { + this.Log.log('Reset Scene Trigger'); + this.serviceIn.updateCharacteristic(this.platform.Characteristic.On, + this.SwitchOn = false); + this.NextItemToTrigger = 0; + } + + // Switch Handlers async setOn(value: CharacteristicValue) { // implement your own code to turn your device on/off this.SwitchOn = value as boolean; - if (this.SwitchResetTimer) { - clearTimeout(this.SwitchResetTimer); - } if (this.SwitchOn) { - this.SwitchResetTimer = setTimeout(() => { - this.platform.log.debug('Reset Scene Trigger'); - this.serviceIn.updateCharacteristic(this.platform.Characteristic.On, - this.SwitchOn = false); - this.NextItemToTrigger = 0; - }, this.State.SecondsToKeepState * 1000); + if (this.SwitchResetTimer) { + clearTimeout(this.SwitchResetTimer); + } + // Start the reset timer + this.SwitchResetTimer = setTimeout(this.resetTrigger.bind(this), this.State.triggerTimeout * 1000); // Trigger Scene - this.triggerNextItem(); + this.triggerNext(); + } else if (this.Config.isResetWhenOff() && !this.SwitchOn) { + if (this.SwitchResetTimer) { + clearTimeout(this.SwitchResetTimer); + } + this.resetTrigger(); } - this.platform.log.debug('Set Characteristic On ->', value); + this.Log.debug('Set Characteristic On ->', value); } async getOn(): Promise { const isOn = this.SwitchOn; - this.platform.log.debug('Get Characteristic On ->', isOn); + this.Log.debug('Get Characteristic On ->', isOn); // if you need to return an error to show the device as "Not Responding" in the Home app: // throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE); return isOn; @@ -172,30 +182,30 @@ export class DeviceAccessory { // NumScenes Handlers async setItemsToHandle(value: CharacteristicValue) { const numItemsToHandle = value as number; - this.platform.log.debug('Set Characteristic NumScenes ->', numItemsToHandle); - this.State.NumConfiguredScenes = numItemsToHandle; + this.Log.debug('Set Characteristic NumScenes ->', numItemsToHandle); + this.State.numberConfiguredScenes = numItemsToHandle; //this.updatePersistentData(); } async getItemsToHandle(): Promise { - const numItemsToHandle = this.State.NumConfiguredScenes; - this.platform.log.debug('Get Characteristic NumScenes ->', numItemsToHandle); + const numItemsToHandle = this.State.numberConfiguredScenes; + this.Log.debug('Get Characteristic NumScenes ->', numItemsToHandle); return numItemsToHandle; } // TriggerTimeout Handlers async setTriggerTimeout(value: CharacteristicValue) { const triggerTimeout = value as number; - this.platform.log.debug('Set Characteristic TriggerTimeout ->', triggerTimeout); - this.State.SecondsToKeepState = triggerTimeout; + this.Log.debug('Set Characteristic TriggerTimeout ->', triggerTimeout); + this.State.triggerTimeout = triggerTimeout; //this.updatePersistentData(); } async getTriggerTimeout(): Promise { - const triggerTimeout = this.State.SecondsToKeepState; - this.platform.log.debug('Get Characteristic TriggerTimeout ->', triggerTimeout); + const triggerTimeout = this.State.triggerTimeout; + this.Log.debug('Get Characteristic TriggerTimeout ->', triggerTimeout); return triggerTimeout; } }