4 Commits
7 changed files with 133 additions and 19 deletions

No files matched your search

+5
View File
@@ -1,6 +1,11 @@
<component name="InspectionProjectProfileManager"> <component name="InspectionProjectProfileManager">
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="50" name="TypeScript" />
</Languages>
</inspection_tool>
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile> </profile>
</component> </component>
+19 -3
View File
@@ -43,12 +43,19 @@
"required": true, "required": true,
"default": 10, "default": 10,
"minimum": 0, "minimum": 0,
"maximum": 30 "maximum": 60
}, },
"resetAfterSwitchOff": { "resetAfterSwitchOff": {
"title": "Reset when switch turned `Off`", "title": "Reset when switch is turned `Off`",
"type": "boolean", "type": "boolean",
"description": "Reset the switch and trigger index, when the trigger switch is turned <code>Off</code> (independent from timeout)", "description": "Reset the switch and trigger index, when the stateless switch is turned <code>Off</code>",
"required": true,
"default": false
},
"triggerSceneAfterSwitchOff": {
"title": "Trigger scene, when switch is turned `Off`",
"type": "boolean",
"description": "Trigger scene <code>0</code> (so called <code>Off</code>-scene), when the stateless switch is turned <code>Off</code> (independent from timeout).<br><b>Note:</b> If this is enabled, rotation starts from switch <code>1</code>, otherwise <code>0</code>.",
"required": true, "required": true,
"default": false "default": false
}, },
@@ -57,6 +64,15 @@
"type": "boolean", "type": "boolean",
"required": true, "required": true,
"default": false "default": false
},
"debounceTimerMs": {
"title": "Debounce Timer (in milliseconds)",
"type": "number",
"description": "Time to wait before the next switch trigger is accepted.<br>Only change this value, if you encounter problems using the HomeKit slider in the Home App",
"required": true,
"minimum": 10,
"maximum": 1000,
"default": 150
} }
} }
} }
+1 -1
View File
@@ -2,7 +2,7 @@
"private": false, "private": false,
"displayName": "Homebridge Multi-Tap-Switch", "displayName": "Homebridge Multi-Tap-Switch",
"name": "homebridge-multi-tap-switch", "name": "homebridge-multi-tap-switch",
"version": "0.4.0", "version": "0.5.0",
"description": "A configurable multi-tap switch for i.e. switching scenes", "description": "A configurable multi-tap switch for i.e. switching scenes",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
+24
View File
@@ -3,14 +3,19 @@ import { PlatformConfig } from 'homebridge';
const DEFAULT_NUM_OF_CONFIGURED_SCENES = 5; const DEFAULT_NUM_OF_CONFIGURED_SCENES = 5;
const DEFAULT_TRIGGER_TIMEOUT = 10; const DEFAULT_TRIGGER_TIMEOUT = 10;
const DEFAULT_RESET_AFTER_SWITCH_OFF = false; const DEFAULT_RESET_AFTER_SWITCH_OFF = false;
const DEFAULT_TRIGGER_SCENE_AFTER_SWITCH_OFF = false;
const DEFAULT_TRIGGER_SCENE_OFF = 0;
const DEFAULT_LOGGING = false; const DEFAULT_LOGGING = false;
const DEFAULT_DEBOUNCE_TIMEOUT_MS = 150;
export interface PluginDeviceConfig { export interface PluginDeviceConfig {
readonly name: string; readonly name: string;
readonly numberConfiguredScenes?: number; readonly numberConfiguredScenes?: number;
readonly triggerTimeout?: number; readonly triggerTimeout?: number;
readonly resetAfterSwitchOff?: boolean; readonly resetAfterSwitchOff?: boolean;
readonly triggerSceneAfterSwitchOff?: boolean;
readonly logging?: boolean; readonly logging?: boolean;
readonly debounceTimerMs?: number;
} }
export class PluginConfig { export class PluginConfig {
@@ -50,10 +55,29 @@ export class DeviceConfig {
|| DEFAULT_RESET_AFTER_SWITCH_OFF; || DEFAULT_RESET_AFTER_SWITCH_OFF;
} }
isTriggerSceneWhenOff(): boolean {
return this.device.triggerSceneAfterSwitchOff
|| DEFAULT_TRIGGER_SCENE_AFTER_SWITCH_OFF;
}
// When triggerSceneAfterSwitchOff is 'true', start at 1, otherwise 0 (normal behavior)
FirstTriggerScene(): number {
return this.isTriggerSceneWhenOff() ? 1 : 0;
}
TriggerOffSceneIndex(): number {
return DEFAULT_TRIGGER_SCENE_OFF;
}
isLogging(): boolean { isLogging(): boolean {
return this.device.logging return this.device.logging
|| DEFAULT_LOGGING; || DEFAULT_LOGGING;
} }
DebounceTimeoutMs(): number {
return this.device.debounceTimerMs
|| DEFAULT_DEBOUNCE_TIMEOUT_MS;
}
} }
export interface AccessoryState { export interface AccessoryState {
+45
View File
@@ -0,0 +1,45 @@
import { Logging } from './logger';
import { clearTimeout } from 'node:timers';
export default class TimeoutHandler {
private timeoutObject: NodeJS.Timeout | undefined;
public timeoutMs: number = 0;
private log: Logging | undefined;
private isRunning: boolean = false;
constructor(timeoutMs: number, logger: Logging) {
this.log = logger;
this.timeoutMs = timeoutMs;
}
public trigger(): void {
if (!this.isRunning) {
this.timeoutStarted();
this.timeoutObject = setTimeout(this.timeoutTriggered.bind(this), this.timeoutMs);
}
}
public cancel(): void {
if (!this.isRunning) {
clearTimeout(this.timeoutObject);
this.isRunning = false;
}
}
public elapsedAndTrigger(): boolean {
if (!this.isRunning) {
this.trigger();
return true;
}
return false;
}
private timeoutStarted(): void {
this.isRunning = true;
}
async timeoutTriggered(): Promise<void> {
this.isRunning = false;
}
}
+38 -15
View File
@@ -6,6 +6,7 @@ import { VERSION } from './settings';
import { attachCharacteristic_ConfiguredScenes } from './types/characteristicConfiguredScenes'; import { attachCharacteristic_ConfiguredScenes } from './types/characteristicConfiguredScenes';
import { attachCharacteristic_TriggerTimeout } from './types/characteristicTimeout'; import { attachCharacteristic_TriggerTimeout } from './types/characteristicTimeout';
import { attachCharacteristic_CurrentScene } from './types/characteristicCurrentScene'; import { attachCharacteristic_CurrentScene } from './types/characteristicCurrentScene';
import TimeoutHandler from './helper/timeoutHandler';
/** /**
* Platform Accessory * Platform Accessory
@@ -23,6 +24,7 @@ export class DeviceAccessory {
private SwitchResetTimer: NodeJS.Timeout | undefined; private SwitchResetTimer: NodeJS.Timeout | undefined;
private SwitchOn: boolean = false; private SwitchOn: boolean = false;
private NextItemToTrigger: number = 0; private NextItemToTrigger: number = 0;
private LastTriggerReceived: TimeoutHandler;
private Log: Logging; private Log: Logging;
@@ -51,6 +53,8 @@ export class DeviceAccessory {
this.Log = new Logging(platform.log, this.Config.Name(), this.Config.isLogging()); this.Log = new Logging(platform.log, this.Config.Name(), this.Config.isLogging());
this.Log.log('Initialize Accessory ->', this.Config.Name()); this.Log.log('Initialize Accessory ->', this.Config.Name());
this.LastTriggerReceived = new TimeoutHandler(this.Config.DebounceTimeoutMs(), this.Log);
/** /**
* Accessory information * Accessory information
*/ */
@@ -106,13 +110,13 @@ export class DeviceAccessory {
// Register handlers for the CurrentScene characteristic & restore previous value // Register handlers for the CurrentScene characteristic & restore previous value
attachCharacteristic_CurrentScene(this.serviceIn, this.api) attachCharacteristic_CurrentScene(this.serviceIn, this.api)
.setProps({ .setProps({
'minValue': 1, 'minValue': 0,
'maxValue': this.Config.NumOfScenes(), 'maxValue': this.Config.NumOfScenes(),
'minStep': 1, 'minStep': 1,
}) })
.onSet(this.setCurrentScene.bind(this)) .onSet(this.setCurrentScene.bind(this))
.onGet(this.getCurrentScene.bind(this)) .onGet(this.getCurrentScene.bind(this))
.updateValue(this.NextItemToTrigger); .updateValue(this.NextItemToTrigger = this.Config.FirstTriggerScene());
// Set the service name, this is what is displayed as the default name on the Home app // 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()); this.serviceIn.setCharacteristic(this.platform.Characteristic.Name, this.Config.Name());
@@ -165,14 +169,13 @@ export class DeviceAccessory {
/** /**
* Trigger/Fire the next programmable switch event * Trigger/Fire the next programmable switch event
*/ */
triggerNext() { trigger(index: number) {
if (this.NextItemToTrigger >= this.State.numberConfiguredScenes) { if ((index >= 0) && (index < this.State.numberConfiguredScenes)) {
this.NextItemToTrigger = 0; this.Log.log('Trigger Event -> ', index);
this.TriggerButtons[index].updateCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent, 0);
} else {
this.Log.error('Trigger Event -> Index ', index, ' out of bounds (min = 0, max = ', this.State.numberConfiguredScenes - 1, ')');
} }
// Trigger Scene Button
this.Log.log('Trigger Event -> ', this.NextItemToTrigger);
this.TriggerButtons[this.NextItemToTrigger++].updateCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent, 0);
} }
/** /**
@@ -181,19 +184,25 @@ export class DeviceAccessory {
resetTrigger() { resetTrigger() {
this.Log.log('Reset Scene Trigger'); this.Log.log('Reset Scene Trigger');
this.serviceIn.updateCharacteristic(this.platform.Characteristic.On, this.SwitchOn = false); this.serviceIn.updateCharacteristic(this.platform.Characteristic.On, this.SwitchOn = false);
this.NextItemToTrigger = 0; this.NextItemToTrigger = this.Config.FirstTriggerScene();
} }
// =====================
// = Handler Functions =
// =====================
/** /**
* Handle state changes of the input switch * Handle state changes of the input switch
* This function does all the necessary work. * This function does all the necessary work.
* @param value Target switch state from Automation or manual state change * @param value Target switch state from Automation or manual state change
*/ */
async setOn(value: CharacteristicValue) { async setOn(value: CharacteristicValue) {
// implement your own code to turn your device on/off // Check if debounce timer has elapsed. If not, ignore the command.
if (this.LastTriggerReceived.elapsedAndTrigger()) {
this.SwitchOn = value as boolean; this.SwitchOn = value as boolean;
if (this.SwitchOn) { if (this.SwitchOn) { // Switch is set to 'ON'
// Disable an active reset timer, before starting a new one.
if (this.SwitchResetTimer) { if (this.SwitchResetTimer) {
clearTimeout(this.SwitchResetTimer); clearTimeout(this.SwitchResetTimer);
} }
@@ -204,17 +213,31 @@ export class DeviceAccessory {
this.SwitchResetTimer = setTimeout(this.resetTrigger.bind(this), this.State.triggerTimeout * 1000); this.SwitchResetTimer = setTimeout(this.resetTrigger.bind(this), this.State.triggerTimeout * 1000);
} }
// Trigger Scene // Determine next item to trigger
this.triggerNext(); if (this.NextItemToTrigger >= this.State.numberConfiguredScenes) {
} else if (this.Config.isResetWhenOff() && !this.SwitchOn) { this.NextItemToTrigger = this.Config.FirstTriggerScene();
}
// Trigger Switch
this.trigger(this.NextItemToTrigger++);
} else { // Switch is set to 'OFF'
// If configured: Disable any active reset timer, when the switch is manually turned off
if (this.Config.isResetWhenOff()) {
if (this.SwitchResetTimer) { if (this.SwitchResetTimer) {
clearTimeout(this.SwitchResetTimer); clearTimeout(this.SwitchResetTimer);
} }
this.resetTrigger(); this.resetTrigger();
} }
// If configured: Trigger stateless switch / scene when manually switched off
if (this.Config.isTriggerSceneWhenOff()) {
// Trigger scene 0
this.trigger(this.Config.TriggerOffSceneIndex());
}
}
this.Log.debug('Set Switch State ->', value); this.Log.debug('Set Switch State ->', value);
} }
}
/** /**
* Report state of the input switch back to HomeKit (for visualization) * Report state of the input switch back to HomeKit (for visualization)
+1
View File
@@ -34,6 +34,7 @@
"numberConfiguredScenes": 5, "numberConfiguredScenes": 5,
"triggerTimeout": 10, "triggerTimeout": 10,
"resetAfterSwitchOff": false, "resetAfterSwitchOff": false,
"triggerSceneAfterSwitchOff": true,
"logging": true "logging": true
} }
] ]