diff --git a/src/accessory.ts b/src/accessory.ts deleted file mode 100644 index 1d024b2..0000000 --- a/src/accessory.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; - -import { MultiTapSwitchPlatform } from './platform'; -import {TIMEOUT} from 'dns'; - -/** - * 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 DeviceAccessory { - private serviceIn: Service; - private serviceOut: Service; - - private readonly singleButtonEvents = { - minValue: this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, - maxValue: this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, - validValues: [ - this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, - ], - }; - - private currentState = { - "On": Boolean, - "NumScenes": Number, - "KeepSeconds": Number, - "Timeout": TIMEOUT - }; - - private readonly triggerButtons: Service[] = []; - - constructor( - private readonly platform: MultiTapSwitchPlatform, - private readonly accessory: PlatformAccessory, - private readonly numOfButtons: number, - ) { - - // set accessory information - this.accessory.getService(this.platform.Service.AccessoryInformation)! - .setCharacteristic(this.platform.Characteristic.Manufacturer, 'TippyTapper Solutions') - .setCharacteristic(this.platform.Characteristic.Model, 'SceneButton-000') - .setCharacteristic(this.platform.Characteristic.SerialNumber, 'DE:AD:BE:EF:00:00'); - - - this.serviceIn = this.accessory.getService(this.platform.Service.Switch) - || this.accessory.addService(this.platform.Service.Switch); - - this.serviceOut = this.accessory.getService(this.platform.Service.ServiceLabel) - || this.accessory.addService(this.platform.Service.ServiceLabel); - - this.serviceOut.setCharacteristic(this.platform.Characteristic.ServiceLabelNamespace, '') - this.serviceOut.setCharacteristic(this.platform.Characteristic.ServiceLabelIndex, 1); - - // Create configured number of scene buttons - for (let i = 1; i <= numOfButtons; i++) { - const newButton = this.accessory.addService(this.platform.Service.StatelessProgrammableSwitch); - this.serviceOut.addLinkedService(newButton); - this.triggerButtons.push(newButton); - - newButton.getCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent) - .setProps(this.singleButtonEvents); - newButton.setCharacteristic(this.platform.Characteristic.ServiceLabelIndex, i); - } - - // set the service name, this is what is displayed as the default name on the Home app - // in this example we are using the name we stored in the `accessory.context` in the `discoverDevices` method. - this.serviceOut.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.exampleDisplayName); - - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/Lightbulb - - // register handlers for the On/Off Characteristic - this.serviceOut.getCharacteristic(this.platform.Characteristic.On) - .onSet(this.setOn.bind(this)) // SET - bind to the `setOn` method below - .onGet(this.getOn.bind(this)); // GET - bind to the `getOn` method below - - // register handlers for the Brightness Characteristic - this.serviceOut.getCharacteristic(this.platform.Characteristic.Brightness) - .onSet(this.setBrightness.bind(this)); // SET - bind to the 'setBrightness` method below - - /** - * Creating multiple services of the same type. - * - * To avoid "Cannot add a Service with the same UUID another Service without also defining a unique 'subtype' property." error, - * when creating multiple services of the same type, you need to use the following syntax to specify a name and subtype id: - * this.accessory.getService('NAME') || this.accessory.addService(this.platform.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE_ID'); - * - * The USER_DEFINED_SUBTYPE must be unique to the platform accessory (if you platform exposes multiple accessories, each accessory - * can use the same sub type id.) - */ - - // Example: add two "motion sensor" services to the accessory - const inputService = this.accessory.getService(this.accessory.displayName + ' Trigger') || - this.accessory.addService(this.platform.Service.MotionSensor, this.accessory.displayName + ' Trigger', 'YourUniqueIdentifier-1'); - - const outputService = this.accessory.getService(this.accessory.displayName + ' SceneSwitch') || - this.accessory.addService(this.platform.Service.MotionSensor, this.accessory.displayName + ' SceneSwitch', 'YourUniqueIdentifier-2'); - - /** - * Updating characteristics values asynchronously. - * - * Example showing how to update the state of a Characteristic asynchronously instead - * of using the `on('get')` handlers. - * Here we change update the motion sensor trigger states on and off every 10 seconds - * the `updateCharacteristic` method. - * - */ - let motionDetected = false; - setInterval(() => { - // EXAMPLE - inverse the trigger - motionDetected = !motionDetected; - - // push the new value to HomeKit - inputService.updateCharacteristic(this.platform.Characteristic.MotionDetected, motionDetected); - outputService.updateCharacteristic(this.platform.Characteristic.MotionDetected, !motionDetected); - - this.platform.log.debug('Triggering motionSensorOneService:', motionDetected); - this.platform.log.debug('Triggering motionSensorTwoService:', !motionDetected); - }, 10000); - - } - - /** - * Handle "SET" requests from HomeKit - * These are sent when the user changes the state of an accessory, for example, turning on a Light bulb. - */ - async setOn(value: CharacteristicValue) { - // implement your own code to turn your device on/off - this.currentState.On = value as boolean; - - this.currentState.Timeout = setTimeout(() => { - this.currentState.On = false; - this.setOn(false); - }, 1000 * 10); - this.platform.log.debug('Set Characteristic On ->', value); - } - - /** - * Handle the "GET" requests from HomeKit - * These are sent when HomeKit wants to know the current state of the accessory, for example, checking if a Light bulb is on. - * - * GET requests should return as fast as possbile. A long delay here will result in - * HomeKit being unresponsive and a bad user experience in general. - * - * If your device takes time to respond you should update the status of your device - * asynchronously instead using the `updateCharacteristic` method instead. - - * @example - * this.service.updateCharacteristic(this.platform.Characteristic.On, true) - */ - async getOn(): Promise { - // implement your own code to check if the device is on - const isOn = this.currentState.On; - - this.platform.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; - } - - /** - * Handle "SET" requests from HomeKit - * These are sent when the user changes the state of an accessory, for example, changing the Brightness - */ - async setBrightness(value: CharacteristicValue) { - // implement your own code to set the brightness - //this.currentState.KeepSeconds = value as number; - - this.platform.log.debug('Set Characteristic KeepSeconds -> ', value); - } - -} diff --git a/src/platform.ts b/src/platform.ts index 684b257..300e762 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,18 +1,31 @@ import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge'; import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; -import { DeviceAccessory } from './accessory'; +import { DeviceAccessory } from './platformAccessory'; + +// Necessary definitions for custom characteristics! +import ConfiguredScenes from './types/characteristicConfiguredScenes'; +let IConfiguredScenes; +import TriggerTimeout from './types/characteristicTimeout'; +let ITriggerTimeout; + +interface PlatformConfigData { + Name: string; + MaxNumScenes: number; + SecondsBeforeReset: number; +} /** * HomebridgePlatform - * This class is the main constructor for your plugin, this is where you should - * parse the user config and discover/register accessories with Homebridge. */ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin { public readonly Service: typeof Service = this.api.hap.Service; - public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic; + public readonly Characteristic: typeof Characteristic & typeof IConfiguredScenes & typeof ITriggerTimeout; - // this is used to track restored cached accessories + private readonly CharacteristicConfiguredScenes; + private readonly CharacteristicTriggerTimeout; + + // Track restored cached accessories public readonly accessories: PlatformAccessory[] = []; constructor( @@ -22,21 +35,24 @@ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin { ) { this.log.debug('Finished initializing platform:', this.config.name); - // When this event is fired it means Homebridge has restored all cached accessories from disk. - // Dynamic Platform plugins should only register new accessories after this event was fired, - // in order to ensure they weren't added to homebridge already. This event can also be used - // to start discovery of new accessories. + this.CharacteristicConfiguredScenes = ConfiguredScenes(this.api); + IConfiguredScenes = this.CharacteristicConfiguredScenes; + this.CharacteristicTriggerTimeout = TriggerTimeout(this.api); + ITriggerTimeout = this.CharacteristicTriggerTimeout; + this.api.on('didFinishLaunching', () => { - log.debug('Executed didFinishLaunching callback'); - // run the method to discover / register your devices as accessories + log.debug('Discover/Register Devices ...'); this.discoverDevices(); }); + + this.Characteristic = Object.defineProperty(this.api.hap.Characteristic, + 'ConfiguredScenes', + {value: this.CharacteristicConfiguredScenes}); + this.Characteristic = Object.defineProperty(this.api.hap.Characteristic, + 'TriggerTimeout', + {value: this.CharacteristicTriggerTimeout}); } - /** - * This function is invoked when homebridge restores cached accessories from disk at startup. - * It should be used to setup event handlers for characteristics and update respective values. - */ configureAccessory(accessory: PlatformAccessory) { this.log.info('Loading accessory from cache:', accessory.displayName); @@ -44,64 +60,52 @@ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin { this.accessories.push(accessory); } - defineUuid(name: string | undefined, serial: string): string { + defineUuid(name: string | undefined, serial = 'UNDEF'): string { return this.api.hap.uuid.generate(name + '_' + serial); } - /** - * This is an example method showing how to register discovered accessories. - * Accessories must only be registered once, previously created accessories - * must not be registered again to prevent "duplicate UUID" errors. - */ discoverDevices() { + const uuid = this.defineUuid(this.config.name, '0'); - // EXAMPLE ONLY - // A real plugin you would discover accessories from the local network, cloud services - // or a user-defined array in the platform config. - - // generate a unique id for the accessory this should be generated from - // something globally unique, but constant, for example, the device serial - // number or MAC address - //const uuid = this.api.hap.uuid.generate(device.exampleUniqueId); - const uuid = this.defineUuid(this.config.name, "0"); + const accessoryConfig: PlatformConfigData = { + Name: this.config.name ? this.config.name : 'Scene Switch', + MaxNumScenes: this.config.numScenes, + SecondsBeforeReset: this.config.secondsToKeep, + }; // 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 const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); if (existingAccessory) { - // the accessory already exists + // Accessory already exists this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); // 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; + this.api.updatePlatformAccessories([existingAccessory]); + } - // create the accessory handler for the restored accessory - // this is imported from `accessoryInput.ts` - new DeviceAccessory(this, existingAccessory, this.config.numScenes); - - // it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.: - // remove platform accessories when no longer present - // this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]); - // this.log.info('Removing existing accessory from cache:', existingAccessory.displayName); + // Create the accessory handler for the restored accessory + // This is imported from `platformAccessory.ts` + new DeviceAccessory(this, existingAccessory); } else { - // the accessory does not yet exist, so we need to create it - this.log.info('Adding INPUT:', this.config.name); + // 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'); - // create a new accessory - const accessory = new this.api.platformAccessory(this.config.name, uuid); + // Create new accessory + const accessory = new this.api.platformAccessory(accessoryConfig.Name, uuid); - // store a copy of the device object in the `accessory.context` - // the `context` property can be used to store any data about the accessory you may need - accessory.context.device = { - "numScenes": this.config.numScenes, - "keepSeconds": this.config.keepSeconds - }; + // Store config in accessory context + accessory.context.config = accessoryConfig; - // create the accessory handler for the newly create accessory - // this is imported from `accessoryInput.ts` - new DeviceAccessory(this, accessory, this.config.numScenes); + // Create the accessory handler for the new accessory + // This is imported from `platformAccessory.ts` + new DeviceAccessory(this, accessory); // link the accessory to your platform this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts new file mode 100644 index 0000000..509b2ce --- /dev/null +++ b/src/platformAccessory.ts @@ -0,0 +1,201 @@ +import {CharacteristicValue, PlatformAccessory, Service} from 'homebridge'; +import {MultiTapSwitchPlatform} from './platform'; + +interface AccessoryState { + NumConfiguredScenes: number; + SecondsToKeepState: number; +} + +/** + * Platform Accessory + */ +export class DeviceAccessory { + private serviceIn: Service; + private serviceOut: Service; + + private readonly singleButtonEvents = { + minValue: this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, + maxValue: this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, + validValues: [ + this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, + ], + }; + + private readonly State: AccessoryState; + + private readonly TriggerButtons: Service[] = []; + private SwitchResetTimer: NodeJS.Timeout | undefined; + private SwitchOn = false; + private NextItemToTrigger = 0; + + 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, + }; + } + + // Set accessory information + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'TippyTapper Solutions') + .setCharacteristic(this.platform.Characteristic.Model, 'SceneSwitch') + .setCharacteristic(this.platform.Characteristic.SerialNumber, 'DE:AD:BE:EF:00:00'); + + + // 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 + 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; + } + (this.serviceIn.getCharacteristic(this.platform.Characteristic.ConfiguredScenes) + || this.serviceIn.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredScenes)) + .setProps({ + 'minValue': 0, + 'maxValue': this.accessory.context.config.MaxNumScenes, + 'minStep': 1, + }) + .onSet(this.setItemsToHandle.bind(this)) + .onGet(this.getItemsToHandle.bind(this)) + .updateValue(this.State.NumConfiguredScenes); + + // Register handlers for the TriggerTimeout characteristic & restore previous value + (this.serviceIn.getCharacteristic(this.platform.Characteristic.TriggerTimeout) + || this.serviceIn.addOptionalCharacteristic(this.platform.Characteristic.TriggerTimeout)) + .setProps({ + 'minValue': 0, + 'maxValue': 30, + 'minStep': 1, + }) + .onSet(this.setTriggerTimeout.bind(this)) + .onGet(this.getTriggerTimeout.bind(this)) + .updateValue(this.State.SecondsToKeepState); + + // Get or Create the output service (excluding the buttons) + this.serviceOut = this.accessory.getService(this.platform.Service.ServiceLabel) + || this.accessory.addService(this.platform.Service.ServiceLabel); + 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); + + let newButton = this.accessory.getService('Scene ' + i); + if (!newButton) { + this.platform.log.debug('Add new Button'); + newButton = this.accessory.addService(this.platform.Service.StatelessProgrammableSwitch, 'Scene ' + i, 'scene' + i); + + this.serviceOut.addLinkedService(newButton); + newButton.getCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent) + .setProps(this.singleButtonEvents); + newButton.setCharacteristic(this.platform.Characteristic.ServiceLabelIndex, i); + } + + this.TriggerButtons.push(newButton); + } + + // Remove buttons, if max buttons is smaller + 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.serviceOut.removeLinkedService(exButton); + accessory.removeService(exButton); + } else { + break; + } + } + + // 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; + } + + triggerNextItem() { + if (this.NextItemToTrigger >= this.State.NumConfiguredScenes) { + this.NextItemToTrigger = 0; + } + + // Trigger Scene Button + this.platform.log.debug('Trigger Scene -> ', this.NextItemToTrigger); + this.TriggerButtons[this.NextItemToTrigger++].updateCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent, 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); + + // Trigger Scene + this.triggerNextItem(); + } + + this.platform.log.debug('Set Characteristic On ->', value); + } + + async getOn(): Promise { + const isOn = this.SwitchOn; + this.platform.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; + } + + // NumScenes Handlers + async setItemsToHandle(value: CharacteristicValue) { + const numItemsToHandle = value as number; + this.platform.log.debug('Set Characteristic NumScenes ->', numItemsToHandle); + this.State.NumConfiguredScenes = numItemsToHandle; + + //this.updatePersistentData(); + } + + async getItemsToHandle(): Promise { + const numItemsToHandle = this.State.NumConfiguredScenes; + this.platform.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.updatePersistentData(); + } + + async getTriggerTimeout(): Promise { + const triggerTimeout = this.State.SecondsToKeepState; + this.platform.log.debug('Get Characteristic TriggerTimeout ->', triggerTimeout); + return triggerTimeout; + } +} diff --git a/src/types/characteristicConfiguredScenes.ts b/src/types/characteristicConfiguredScenes.ts new file mode 100644 index 0000000..963835f --- /dev/null +++ b/src/types/characteristicConfiguredScenes.ts @@ -0,0 +1,22 @@ +import { Formats, Perms } from 'homebridge'; + +export = (homebridge) => { + const CustomCharacteristic = homebridge.hap.Characteristic; + const characteristicUUID = 'A8E3F4DF-5E5D-6A7F-8FA7-A22FA5AE9F53'; + + return class ConfiguredScenes extends CustomCharacteristic { + public static readonly UUID: string = characteristicUUID; + + constructor() { + super('Configured Scenes', ConfiguredScenes.UUID, { + format: Formats.UINT8, + //unit: '', + minValue: 0, + maxValue: 0, + minStep: 1, + perms: [Perms.PAIRED_WRITE, Perms.PAIRED_READ, Perms.NOTIFY], + }); + this.value = this.getDefaultValue(); + } + }; +}; diff --git a/src/types/characteristicTimeout.ts b/src/types/characteristicTimeout.ts new file mode 100644 index 0000000..1155e77 --- /dev/null +++ b/src/types/characteristicTimeout.ts @@ -0,0 +1,22 @@ +import { Formats, Perms } from 'homebridge'; + +export = (homebridge) => { + const CustomCharacteristic = homebridge.hap.Characteristic; + const characteristicUUID = 'E2C19A02-1D2F-F592-A2BF-BF920014FB18'; + + return class TriggerTimeout extends CustomCharacteristic { + public static readonly UUID: string = characteristicUUID; + + constructor() { + super('Trigger Timeout', TriggerTimeout.UUID, { + format: Formats.UINT16, + //unit: '', + minValue: 0, + maxValue: 60, + minStep: 1, + perms: [Perms.PAIRED_WRITE, Perms.PAIRED_READ, Perms.NOTIFY], + }); + this.value = this.getDefaultValue(); + } + }; +};