mirror of
https://github.com/dtwardow/homebridge-multi-tap-switch.git
synced 2026-11-02 05:18:30 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
251c4ed5ea | ||
|
|
41375599a2 | ||
|
|
b5ddbf488f | ||
|
|
3ef3435417 |
No files matched your search
+5
@@ -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
@@ -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
@@ -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": {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
-30
@@ -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,39 +184,59 @@ 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.
|
||||||
this.SwitchOn = value as boolean;
|
if (this.LastTriggerReceived.elapsedAndTrigger()) {
|
||||||
|
this.SwitchOn = value as boolean;
|
||||||
|
|
||||||
if (this.SwitchOn) {
|
if (this.SwitchOn) { // Switch is set to 'ON'
|
||||||
if (this.SwitchResetTimer) {
|
// Disable an active reset timer, before starting a new one.
|
||||||
clearTimeout(this.SwitchResetTimer);
|
if (this.SwitchResetTimer) {
|
||||||
|
clearTimeout(this.SwitchResetTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the reset timer, if parameter is greater than 0.
|
||||||
|
// Otherwise, do nothing.
|
||||||
|
if (this.State.triggerTimeout > 0) {
|
||||||
|
this.SwitchResetTimer = setTimeout(this.resetTrigger.bind(this), this.State.triggerTimeout * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine next item to trigger
|
||||||
|
if (this.NextItemToTrigger >= this.State.numberConfiguredScenes) {
|
||||||
|
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) {
|
||||||
|
clearTimeout(this.SwitchResetTimer);
|
||||||
|
}
|
||||||
|
this.resetTrigger();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If configured: Trigger stateless switch / scene when manually switched off
|
||||||
|
if (this.Config.isTriggerSceneWhenOff()) {
|
||||||
|
// Trigger scene 0
|
||||||
|
this.trigger(this.Config.TriggerOffSceneIndex());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the reset timer, if parameter is greater than 0.
|
this.Log.debug('Set Switch State ->', value);
|
||||||
// Otherwise, do nothing.
|
|
||||||
if (this.State.triggerTimeout > 0) {
|
|
||||||
this.SwitchResetTimer = setTimeout(this.resetTrigger.bind(this), this.State.triggerTimeout * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger Scene
|
|
||||||
this.triggerNext();
|
|
||||||
} else if (this.Config.isResetWhenOff() && !this.SwitchOn) {
|
|
||||||
if (this.SwitchResetTimer) {
|
|
||||||
clearTimeout(this.SwitchResetTimer);
|
|
||||||
}
|
|
||||||
this.resetTrigger();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.Log.debug('Set Switch State ->', value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"numberConfiguredScenes": 5,
|
"numberConfiguredScenes": 5,
|
||||||
"triggerTimeout": 10,
|
"triggerTimeout": 10,
|
||||||
"resetAfterSwitchOff": false,
|
"resetAfterSwitchOff": false,
|
||||||
|
"triggerSceneAfterSwitchOff": true,
|
||||||
"logging": true
|
"logging": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in new issue
Block a user