2 Commits
Author SHA1 Message Date
dennis fe1dcb1f7c 0.1.3 2023-02-05 00:00:16 +01:00
dennis 938598575e Changed/fixxed logging 2023-02-05 00:00:12 +01:00
24 changed files with 3853 additions and 4147 deletions

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended" // uses the recommended rules from the @typescript-eslint/eslint-plugin
],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"ignorePatterns": [
"dist"
],
"rules": {
"quotes": ["warn", "single"],
"indent": ["warn", 2, { "SwitchCase": 1 }],
"semi": ["off"],
"comma-dangle": ["warn", "always-multiline"],
"dot-notation": "off",
"eqeqeq": "warn",
"curly": ["warn", "all"],
"brace-style": ["warn"],
"prefer-arrow-callback": ["warn"],
"max-len": ["warn", 140],
"no-console": ["warn"], // use the provided Homebridge log method instead
"no-non-null-assertion": ["off"],
"comma-spacing": ["error"],
"no-multi-spaces": ["warn", { "ignoreEOLComments": true }],
"no-trailing-spaces": ["warn"],
"lines-between-class-members": ["warn", "always", {"exceptAfterSingleLine": true}],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/semi": ["warn"],
"@typescript-eslint/member-delimiter-style": ["warn"]
}
}
@@ -19,4 +19,5 @@ assignees: ''
**Additional context:**
<!-- Add any other context or screenshots about the feature request here. -->
<!-- Click the "Preview" tab before you submit to ensure the formatting is correct. -->
+6 -12
View File
@@ -7,15 +7,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [20.x, 22.x, 24.x]
# the Node.js versions to build on
node-version: [12.x, 13.x, 14.x, 15.x, 16.x]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
@@ -27,11 +27,5 @@ jobs:
- name: Build the project
run: npm run build
- name: List, audit, fix outdated dependencies and build again
run: |
npm list --outdated
npm audit || true # ignore failures
npm audit fix || true
npm list --outdated
npm run build
env:
CI: true
-24
View File
@@ -1,24 +0,0 @@
name: Publish Package
on:
push:
tags:
- 'v*'
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build --if-present
- run: npm publish
-5
View File
@@ -1,11 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<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" />
</profile>
</component>
+1 -6
View File
@@ -1,16 +1,13 @@
# Ignore source code
src
# IDEA files
.idea
# ------------- Defaults ------------- #
# gitHub actions
.github
# eslint
eslint.config.js
.eslintrc
# typescript
tsconfig.json
@@ -135,5 +132,3 @@ web_modules/
.yarn/unplugged
.yarn/build-state.yml
.pnp.*
test/
+2 -4
View File
@@ -1,10 +1,8 @@
{
"files.eol": "\n",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
"source.fixAll.eslint": true
},
"editor.rulers": [
140
],
"editor.rulers": [ 140 ],
"eslint.enable": true
}
+10 -15
View File
@@ -1,5 +1,5 @@
<p>
<img src="https://github.com/homebridge/branding/blob/latest/logos/homebridge-color-round-stylized.png" width="150" alt="Homebridge">
<p align="center">
<img src="https://github.com/homebridge/branding/raw/master/logos/homebridge-wordmark-logo-vertical.png" width="150" alt="Homebridge">
</p>
# Homebridge Multi-Tap-Switch
@@ -32,21 +32,22 @@ or manually by following these instructions:
The configuration allows the user to add multiple **Multi-Tap-Switches**. The following parameters are available:
| Option | Default Value | Characteristic (runtime configuration) | Description |
|-------------------------------------|----------------|----------------------------------------|-------------------------------------------------------------------------------------------|
|------------------------------------|----------------|----------------------------------------|-------------------------------------------------------------------------------------------|
| `name` | MultiTapSwitch | | Name of the Platform Accessory (primarily shown in logs) |
| `devices[]` | | | Array of configured devices |
| `devices[].name` | | | Name of the switch |
| `devices[] .numberConfiguredScenes` | 5 | `Configured Scenes` | Number of **stateless** switches (cannot be changed during runtime) |
| `devices[] .triggerTimeout` | 10 | `Trigger Timeout` | Seconds, after which the switch is reset (starting from the first **programmable** switch |
| `devices[] .resetAfterSwitchOff` | false | | Reset the switch (like after timeout), when it is turned ```OFF```. |
| `devices[] .logging` | false | | Logging of switch/trigger actions. |
| `devices[].numberConfiguredScenes` | 5 | `Configured Scenes` | Number of **stateless** switches (cannot be changed during runtime) |
| `devices[].triggerTimeout` | 10 | `Trigger Timeout` | Seconds, after which the switch is reset (starting from the first **programmable** switch |
| `devices[].resetAfterSwitchOff` | false | | Reset the switch (like after timeout), when it is turned ```OFF```. |
| `devices[].logging` | false | | Logging of switch/trigger actions. |
The parameter ```triggerTimeout``` can be changed in the configuration during runtime. So, this is just the initial value.
The parameter ```numberConfiguredScenes``` just defines how many **programmable** switches are available (currently, there is no limit). The number of
The parameter ```numberConfiguredScenes``` just defines how many **programmable** switches are available. The number of
really **used** switches can configured during runtime, between 0 and the configured number.
> **NOTE:** Additional parameters (also called **Characteristics**) can only be configured via 3rd-party HomeKit apps, but not in the basic Home App!
> **NOTE:** Additional parameters (also called **Characteristics**) can only be configured via 3rd-party HomeKit apps (i.e.
> Home+, Controller for HomeKit, etc.), but in the basic Home App!
So, an example **platform** plugin config will look as follows:
@@ -65,9 +66,3 @@ So, an example **platform** plugin config will look as follows:
]
}
````
## Contribution
Contributions are always welcome. You can also use the code for own projects and adapt it to your specific needs.
If you find any bugs or have ideas for optimizations or new features, feel free.
+9 -20
View File
@@ -10,9 +10,14 @@
"title": "Platform Name",
"type": "string",
"required": true,
"minLength": 3,
"default": "MultiTapSwitch"
},
"enabled": {
"title": "Enable",
"type": "boolean",
"required": true,
"default": true
},
"devices": {
"type": "array",
"items": {
@@ -43,19 +48,12 @@
"required": true,
"default": 10,
"minimum": 0,
"maximum": 60
"maximum": 30
},
"resetAfterSwitchOff": {
"title": "Reset when switch is turned `Off`",
"title": "Reset when switch turned `Off`",
"type": "boolean",
"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>.",
"description": "Reset the switch and trigger index, when the trigger switch is turned <code>Off</code> (independent from timeout)",
"required": true,
"default": false
},
@@ -64,15 +62,6 @@
"type": "boolean",
"required": true,
"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
}
}
}
-35
View File
@@ -1,35 +0,0 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['dist/**'],
},
{
rules: {
'quotes': ['error', 'single'],
'indent': ['error', 2, { 'SwitchCase': 0 }],
'linebreak-style': ['error', 'unix'],
'semi': ['error', 'always'],
'comma-dangle': ['error', 'always-multiline'],
'dot-notation': 'error',
'eqeqeq': ['error', 'smart'],
'curly': ['error', 'all'],
'brace-style': ['error'],
'prefer-arrow-callback': 'warn',
'max-len': ['warn', 160],
'object-curly-spacing': ['error', 'always'],
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': ['error', { 'classes': false, 'enums': false }],
'@typescript-eslint/no-unused-vars': ['error', { 'caughtErrors': 'none' }],
},
},
{
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
},
eslint.configs.recommended,
...tseslint.configs.recommended,
);
+3636 -3579
View File
File diff suppressed because it is too large. Load diff
+12 -15
View File
@@ -2,7 +2,7 @@
"private": false,
"displayName": "Homebridge Multi-Tap-Switch",
"name": "homebridge-multi-tap-switch",
"version": "0.5.0",
"version": "0.1.3",
"description": "A configurable multi-tap switch for i.e. switching scenes",
"license": "MIT",
"repository": {
@@ -13,8 +13,8 @@
"url": "https://github.com/dtwardow/homebridge-multi-tap-switch/issues"
},
"engines": {
"node": "^20.18.0 || ^22.10.0 || ^24.0.0",
"homebridge": "^1.8.0 || ^2.0.0-beta.0"
"node": ">=14.18.1",
"homebridge": ">=1.3.5"
},
"main": "dist/index.js",
"scripts": {
@@ -26,19 +26,16 @@
"keywords": [
"homebridge-plugin"
],
"dependencies": {
"homebridge-lib": "^7.1.12"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"eslint": "^9.39.1",
"homebridge": "^2.0.0-beta.55",
"nodemon": "^3.1.11",
"rimraf": "^6.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.4"
"@types/node": "^16.10.9",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"homebridge": "^1.3.5",
"nodemon": "^2.0.13",
"rimraf": "^3.0.2",
"ts-node": "^10.3.0",
"typescript": "^4.4.4"
},
"homepage": "https://github.com/dtwardow/homebridge-multi-tap-switch#readme",
"author": "Dennis Twardowsky <twardowsky@gmail.com>"
+3 -27
View File
@@ -1,21 +1,16 @@
import { PlatformConfig } from 'homebridge';
import {PlatformConfig} from 'homebridge';
const DEFAULT_NUM_OF_CONFIGURED_SCENES = 5;
const DEFAULT_TRIGGER_TIMEOUT = 10;
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_DEBOUNCE_TIMEOUT_MS = 150;
export interface PluginDeviceConfig {
readonly name: string;
readonly numberConfiguredScenes?: number;
readonly triggerTimeout?: number;
readonly resetAfterSwitchOff?: boolean;
readonly triggerSceneAfterSwitchOff?: boolean;
readonly logging?: boolean;
readonly debounceTimerMs?: number;
}
export class PluginConfig {
@@ -55,41 +50,22 @@ export class DeviceConfig {
|| 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 {
return this.device.logging
|| DEFAULT_LOGGING;
}
DebounceTimeoutMs(): number {
return this.device.debounceTimerMs
|| DEFAULT_DEBOUNCE_TIMEOUT_MS;
}
}
export interface AccessoryState {
numberConfiguredScenes: number;
triggerTimeout: number;
triggerTimeoutManuallyChanged: boolean;
//nextSceneToTrigger: number;
}
export function initializeAccessoryState(initConfig: PluginDeviceConfig | AccessoryState): AccessoryState {
return {
numberConfiguredScenes: initConfig.numberConfiguredScenes || DEFAULT_NUM_OF_CONFIGURED_SCENES,
triggerTimeout: initConfig.triggerTimeout || DEFAULT_TRIGGER_TIMEOUT,
triggerTimeoutManuallyChanged: false,
//nextSceneToTrigger: 0,
};
}
-45
View File
@@ -1,45 +0,0 @@
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;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { API } from 'homebridge';
import { PLATFORM_NAME } from './settings';
import MultiTapSwitchPlatform from './platform';
import { MultiTapSwitchPlatform } from './platform';
/**
* This method registers the platform with Homebridge
+31 -43
View File
@@ -4,69 +4,69 @@ import { PLATFORM_NAME, PLUGIN_NAME } from './settings';
import { DeviceAccessory } from './platformAccessory';
// Necessary definitions for custom characteristics!
import { initializeAccessoryState, PluginConfig } from './config';
import ConfiguredScenes from './types/characteristicConfiguredScenes';
let IConfiguredScenes;
import {initializeAccessoryState, PluginConfig} from './config';
let ITriggerTimeout;
import TriggerTimeout from './types/characteristicTimeout';
/**
* HomebridgePlatform
*/
class MultiTapSwitchPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service;
public readonly Characteristic: typeof Characteristic;
export class MultiTapSwitchPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic:
typeof Characteristic &
typeof IConfiguredScenes &
typeof ITriggerTimeout;
private readonly CharacteristicConfiguredScenes;
private readonly CharacteristicTriggerTimeout;
// Track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
public readonly Config: PluginConfig;
/**
* Construct the Homebridge platform
* @param log Homebridge Logger
* @param config Homebridge Configuration (from config.json)
* @param api Homebridge API
*/
constructor(
public readonly log: Logger,
public readonly config: PlatformConfig,
public readonly api: API,
) {
this.Config = new PluginConfig(this.config);
this.Service = api.hap.Service;
this.Characteristic = api.hap.Characteristic;
this.log.debug('Finished initializing platform:', this.Config.Name);
this.CharacteristicConfiguredScenes = ConfiguredScenes(this.api);
IConfiguredScenes = this.CharacteristicConfiguredScenes;
this.CharacteristicTriggerTimeout = TriggerTimeout(this.api);
ITriggerTimeout = this.CharacteristicTriggerTimeout;
this.api.on('didFinishLaunching', () => {
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});
}
/**
* Homebridge Accessory Configuration Callback
* @param accessory Accessory to previously added by this platform
*/
configureAccessory(accessory: PlatformAccessory) {
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
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* Calculate the UUIDs
* @param name Name to use for the calculation
* @param serial (optional) Provide an additional number (might be obsolete in future releases)
*/
defineUuid(name: string | undefined, serial = 'UNDEF'): string {
return this.api.hap.uuid.generate(PLATFORM_NAME + '_' + name + '_' + serial);
return this.api.hap.uuid.generate(name + '_' + serial);
}
/**
* Discover new and existing accessories
*/
discoverDevices() {
const knownUuids: string[] = [];
const parsedConfig = new PluginConfig(this.config);
for (const device of parsedConfig.Devices) {
const uuid = this.defineUuid(device.name, '0');
@@ -82,7 +82,7 @@ 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 (JSON.stringify(existingAccessory.context.config) !== JSON.stringify(device)) {
if (existingAccessory.context.config !== device) {
this.log.debug('Update Accessory ->', device.name);
existingAccessory.context.config = device;
this.api.updatePlatformAccessories([existingAccessory]);
@@ -90,9 +90,7 @@ class MultiTapSwitchPlatform implements DynamicPlatformPlugin {
// Create the accessory handler for the restored accessory
// This is imported from `platformAccessory.ts`
new DeviceAccessory(this, this.api, existingAccessory);
knownUuids.push(uuid);
new DeviceAccessory(this, existingAccessory);
} else {
// Accessory does not yet exist, so we need to create it
this.log.info('Add Device ->', device.name, '/',
@@ -108,21 +106,11 @@ class MultiTapSwitchPlatform implements DynamicPlatformPlugin {
// Create the accessory handler for the new accessory
// This is imported from `platformAccessory.ts`
new DeviceAccessory(this, this.api, accessory);
new DeviceAccessory(this, accessory);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
}
// Unregister unused accessories
for (const device of this.accessories) {
if (!knownUuids.find(id => id === device.UUID)) {
this.log.info('Remove unused device ->', device.displayName, '/', device.UUID);
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [device]);
}
}
}
}
export default MultiTapSwitchPlatform;
+66 -201
View File
@@ -1,12 +1,8 @@
import { CharacteristicValue, PlatformAccessory, Service, API } from 'homebridge';
import MultiTapSwitchPlatform from './platform';
import { AccessoryState, DeviceConfig } from './config';
import { Logging } from './helper/logger';
import { VERSION } from './settings';
import { attachCharacteristic_ConfiguredScenes } from './types/characteristicConfiguredScenes';
import { attachCharacteristic_TriggerTimeout } from './types/characteristicTimeout';
import { attachCharacteristic_CurrentScene } from './types/characteristicCurrentScene';
import TimeoutHandler from './helper/timeoutHandler';
import {CharacteristicValue, PlatformAccessory, Service} from 'homebridge';
import {MultiTapSwitchPlatform} from './platform';
import {AccessoryState, DeviceConfig} from './config';
import {Logging} from './helper/logger';
import {VERSION} from './settings';
/**
* Platform Accessory
@@ -15,31 +11,7 @@ export class DeviceAccessory {
private serviceIn: Service;
private serviceOut: Service;
private readonly singleButtonEvents;
private readonly State: AccessoryState;
private readonly Config: DeviceConfig;
private readonly TriggerButtons: Service[] = [];
private SwitchResetTimer: NodeJS.Timeout | undefined;
private SwitchOn: boolean = false;
private NextItemToTrigger: number = 0;
private LastTriggerReceived: TimeoutHandler;
private Log: Logging;
/**
* Construct the accessory services
* @param platform Parent Homebridge platform
* @param api Parent platform API
* @param accessory Accessory to which these services will be attached
*/
constructor(
private readonly platform: MultiTapSwitchPlatform,
private readonly api: API,
private readonly accessory: PlatformAccessory,
) {
this.singleButtonEvents = {
private readonly singleButtonEvents = {
minValue: this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS,
maxValue: this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS,
validValues: [
@@ -47,26 +19,34 @@ 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,
) {
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());
this.Log.log('Initialize Accessory', this.Config.Name());
this.LastTriggerReceived = new TimeoutHandler(this.Config.DebounceTimeoutMs(), this.Log);
/**
* Accessory information
*/
// Set accessory information
this.accessory.getService(this.platform.Service.AccessoryInformation)!
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'HB/dtw')
.setCharacteristic(this.platform.Characteristic.Model, 'Multi-Tap-Switch')
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'TippyTapper')
.setCharacteristic(this.platform.Characteristic.Model, 'SceneSwitch')
.setCharacteristic(this.platform.Characteristic.Version, VERSION)
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.Config.Name());
.setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.UUID);
/**
* Input Service / Switch
*/
// Get or Create the input switch service
this.serviceIn = this.accessory.getService(this.platform.Service.Switch)
|| this.accessory.addService(this.platform.Service.Switch);
@@ -80,9 +60,10 @@ export class DeviceAccessory {
if (this.State.numberConfiguredScenes > this.Config.NumOfScenes()) {
this.State.numberConfiguredScenes = this.Config.NumOfScenes();
}
attachCharacteristic_ConfiguredScenes(this.serviceIn, this.api)
(this.serviceIn.getCharacteristic(this.platform.Characteristic.ConfiguredScenes)
|| this.serviceIn.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredScenes))
.setProps({
'minValue': 1,
'minValue': 0,
'maxValue': this.Config.NumOfScenes(),
'minStep': 1,
})
@@ -91,13 +72,8 @@ export class DeviceAccessory {
.updateValue(this.State.numberConfiguredScenes);
// Register handlers for the TriggerTimeout characteristic & restore previous value
if (!this.State.triggerTimeoutManuallyChanged) {
this.State.triggerTimeout = this.Config.TriggerTimeout();
} else {
this.Log.debug('Current Trigger Timeout [' + this.State.triggerTimeout + '] differs from configured value [' +
this.Config.TriggerTimeout() + ']. It won\'t be updated until it\'s equal again.');
}
attachCharacteristic_TriggerTimeout(this.serviceIn, this.api)
(this.serviceIn.getCharacteristic(this.platform.Characteristic.TriggerTimeout)
|| this.serviceIn.addOptionalCharacteristic(this.platform.Characteristic.TriggerTimeout))
.setProps({
'minValue': 0,
'maxValue': 30,
@@ -107,29 +83,15 @@ export class DeviceAccessory {
.onGet(this.getTriggerTimeout.bind(this))
.updateValue(this.State.triggerTimeout);
// Register handlers for the CurrentScene characteristic & restore previous value
attachCharacteristic_CurrentScene(this.serviceIn, this.api)
.setProps({
'minValue': 0,
'maxValue': this.Config.NumOfScenes(),
'minStep': 1,
})
.onSet(this.setCurrentScene.bind(this))
.onGet(this.getCurrentScene.bind(this))
.updateValue(this.NextItemToTrigger = this.Config.FirstTriggerScene());
// 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());
/**
* Output Service / Service Label & Stateless Programmable Switches
*/
// 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 programmable switches
// Create max configured number of scene buttons
for (let i = 1; i <= this.Config.NumOfScenes(); i++) {
this.Log.debug('Handle Scene Button -> ', i);
@@ -141,16 +103,13 @@ export class DeviceAccessory {
this.serviceOut.addLinkedService(newButton);
newButton.getCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent)
.setProps(this.singleButtonEvents);
newButton
.setCharacteristic(this.platform.Characteristic.ServiceLabelIndex, i)
.setCharacteristic(this.platform.Characteristic.Name, 'Scene ' + i);
newButton.setCharacteristic(this.platform.Characteristic.ServiceLabelIndex, i);
}
// Push switches to an internal list for later use
this.TriggerButtons.push(newButton);
}
// Remove any programmable switches, that exceed the maximum of configured switches
// Remove buttons, if max buttons is smaller
for (let i = this.TriggerButtons.length + 1;; i++) {
const exButton = this.accessory.getService('Scene ' + i);
if (exButton) {
@@ -167,15 +126,16 @@ export class DeviceAccessory {
}
/**
* Trigger/Fire the next programmable switch event
* Trigger the next event
*/
trigger(index: number) {
if ((index >= 0) && (index < this.State.numberConfiguredScenes)) {
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, ')');
triggerNext() {
if (this.NextItemToTrigger >= this.State.numberConfiguredScenes) {
this.NextItemToTrigger = 0;
}
// Trigger Scene Button
this.Log.log('Trigger Scene -> ', this.NextItemToTrigger);
this.TriggerButtons[this.NextItemToTrigger++].updateCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent, 0);
}
/**
@@ -183,170 +143,75 @@ export class DeviceAccessory {
*/
resetTrigger() {
this.Log.log('Reset Scene Trigger');
this.serviceIn.updateCharacteristic(this.platform.Characteristic.On, this.SwitchOn = false);
this.NextItemToTrigger = this.Config.FirstTriggerScene();
this.serviceIn.updateCharacteristic(this.platform.Characteristic.On,
this.SwitchOn = false);
this.NextItemToTrigger = 0;
}
// =====================
// = Handler Functions =
// =====================
/**
* Handle state changes of the input switch
* This function does all the necessary work.
* @param value Target switch state from Automation or manual state change
*/
// Switch Handlers
async setOn(value: CharacteristicValue) {
// Check if debounce timer has elapsed. If not, ignore the command.
if (this.LastTriggerReceived.elapsedAndTrigger()) {
// implement your own code to turn your device on/off
this.SwitchOn = value as boolean;
if (this.SwitchOn) { // Switch is set to 'ON'
// Disable an active reset timer, before starting a new one.
if (this.SwitchOn) {
if (this.SwitchResetTimer) {
clearTimeout(this.SwitchResetTimer);
}
// Start the reset timer, if parameter is greater than 0.
// Start the reset timer, if paramter 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()) {
// Trigger Scene
this.triggerNext();
} else if (this.Config.isResetWhenOff() && !this.SwitchOn) {
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());
}
this.Log.debug('Set Characteristic On ->', value);
}
this.Log.debug('Set Switch State ->', value);
}
}
/**
* Report state of the input switch back to HomeKit (for visualization)
*/
async getOn(): Promise<CharacteristicValue> {
const isOn = this.SwitchOn;
this.Log.debug('Get Switch State ->', 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;
}
/**
* Set the number of active switches.
* The final value is limited to the maximum configured number of programmable switches.
* @param value Number of active switches
*/
// NumScenes Handlers
async setItemsToHandle(value: CharacteristicValue) {
let numItemsToHandle = value as number;
if ((numItemsToHandle > 0) && (numItemsToHandle <= this.Config.NumOfScenes())) {
this.Log.log('Set number of Active Switches ->', numItemsToHandle);
} else {
this.Log.warn(
'Set number of Active Switches ->', numItemsToHandle, '-> Out-of-Range / Value must be between 1 and', this.Config.NumOfScenes());
if (numItemsToHandle > this.Config.NumOfScenes()) {
numItemsToHandle = this.Config.NumOfScenes();
} else {
numItemsToHandle = 1;
}
}
const numItemsToHandle = value as number;
this.Log.debug('Set Characteristic NumScenes ->', numItemsToHandle);
this.State.numberConfiguredScenes = numItemsToHandle;
//this.updatePersistentData();
}
/**
* Report number of active switches back to HomeKit (for visualization)
*/
async getItemsToHandle(): Promise<CharacteristicValue> {
const numItemsToHandle = this.State.numberConfiguredScenes;
this.Log.debug('Get Number of Active Switches ->', numItemsToHandle);
this.Log.debug('Get Characteristic NumScenes ->', numItemsToHandle);
return numItemsToHandle;
}
/**
* Set the timeout before resetting the trigger counter.
* @param value Time in seconds
*/
// TriggerTimeout Handlers
async setTriggerTimeout(value: CharacteristicValue) {
let triggerTimeout = value as number;
if (triggerTimeout >= 0) {
this.Log.log('Set Trigger Timeout ->', triggerTimeout);
} else {
this.Log.warn(
'Set Trigger Timeout ->', triggerTimeout, '-> Out-of-Range / Value must be greater or equal to 0');
triggerTimeout = 0;
}
this.State.triggerTimeoutManuallyChanged = (this.Config.TriggerTimeout() !== triggerTimeout);
if (!this.State.triggerTimeoutManuallyChanged) {
this.Log.debug('Trigger Timeout in sync with config.');
} else {
this.Log.warn('Trigger Timeout not in sync with config value [' + this.Config.TriggerTimeout() + '].');
this.Log.warn('It will not be updated by config changes, until it is in sync again!');
}
const triggerTimeout = value as number;
this.Log.debug('Set Characteristic TriggerTimeout ->', triggerTimeout);
this.State.triggerTimeout = triggerTimeout;
//this.updatePersistentData();
}
/**
* Report the timeout before resetting the trigger back to HomeKit (for visualization)
*/
async getTriggerTimeout(): Promise<CharacteristicValue> {
const triggerTimeout = this.State.triggerTimeout;
this.Log.debug('Get Trigger Timeout ->', triggerTimeout);
this.Log.debug('Get Characteristic TriggerTimeout ->', triggerTimeout);
return triggerTimeout;
}
/**
* Set the number of active switches.
* The final value is limited to the maximum configured number of programmable switches.
* @param value Number of active switches
*/
async setCurrentScene(value: CharacteristicValue) {
let currentScene = value as number;
if ((currentScene > 0) && (currentScene <= this.Config.NumOfScenes())) {
this.Log.log('Set current active scene ->', currentScene);
} else {
this.Log.warn(
'Set current scene ->', currentScene, '-> Out-of-Range / Value must be between 1 and', this.Config.NumOfScenes());
if (currentScene > this.Config.NumOfScenes()) {
currentScene = this.Config.NumOfScenes();
} else {
currentScene = 1;
}
}
// value must be corrected by one, because it's used as array index
this.NextItemToTrigger = currentScene - 1;
}
/**
* Report number of active switches back to HomeKit (for visualization)
*/
async getCurrentScene(): Promise<CharacteristicValue> {
const currentScene = this.NextItemToTrigger + 1;
this.Log.debug('Get Number of current scene ->', currentScene);
return currentScene;
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
import package_config from '../package.json';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const package_config = require('../package.json');
/**
* This is the name of the platform that users will use to register the plugin in the Homebridge config.json
+12 -13
View File
@@ -1,23 +1,22 @@
import { API, Characteristic, Formats, Perms, Service } from 'homebridge';
import { Formats, Perms } from 'homebridge';
const DISPLAY_NAME = 'Configured Scenes';
const UUID = 'A8E3F4DF-5E5D-6A7F-8FA7-A22FA5AE9F53';
export = (homebridge) => {
const CustomCharacteristic = homebridge.hap.Characteristic;
const characteristicUUID = 'A8E3F4DF-5E5D-6A7F-8FA7-A22FA5AE9F53';
export function attachCharacteristic_ConfiguredScenes(target: Service, api: API): Characteristic {
let result: Characteristic;
return class ConfiguredScenes extends CustomCharacteristic {
public static readonly UUID: string = characteristicUUID;
if (target.testCharacteristic(DISPLAY_NAME)) {
result = target.getCharacteristic(DISPLAY_NAME)!; // Already tested it exists
} else {
result = target.addCharacteristic(new api.hap.Characteristic(DISPLAY_NAME, UUID, {
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();
}
return result;
}
};
};
-23
View File
@@ -1,23 +0,0 @@
import { API, Characteristic, Formats, Perms, Service } from 'homebridge';
const DISPLAY_NAME = 'Current Scene';
const UUID = '290E9D33-0EFC-4640-891E-CC25AAD740EA';
export function attachCharacteristic_CurrentScene(target: Service, api: API): Characteristic {
let result: Characteristic;
if (target.testCharacteristic(DISPLAY_NAME)) {
result = target.getCharacteristic(DISPLAY_NAME)!; // Already tested it exists
} else {
result = target.addCharacteristic(new api.hap.Characteristic(DISPLAY_NAME, UUID, {
format: Formats.UINT8,
//unit: '',
minValue: 0,
maxValue: 0,
minStep: 1,
perms: [Perms.PAIRED_WRITE, Perms.PAIRED_READ, Perms.NOTIFY],
}));
}
return result;
}
+12 -13
View File
@@ -1,23 +1,22 @@
import { API, Characteristic, Formats, Perms, Service } from 'homebridge';
import { Formats, Perms } from 'homebridge';
const DISPLAY_NAME = 'Trigger Timeout';
const UUID = 'E2C19A02-1D2F-F592-A2BF-BF920014FB18';
export = (homebridge) => {
const CustomCharacteristic = homebridge.hap.Characteristic;
const characteristicUUID = 'E2C19A02-1D2F-F592-A2BF-BF920014FB18';
export function attachCharacteristic_TriggerTimeout(target: Service, api: API): Characteristic {
let result: Characteristic;
return class TriggerTimeout extends CustomCharacteristic {
public static readonly UUID: string = characteristicUUID;
if (target.testCharacteristic(DISPLAY_NAME)) {
result = target.getCharacteristic(DISPLAY_NAME)!; // Already tested it exists
} else {
result = target.addCharacteristic(new api.hap.Characteristic(DISPLAY_NAME, UUID, {
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();
}
return result;
}
};
};
-10
View File
@@ -1,10 +0,0 @@
[
{
"id": 1,
"username": "test",
"name": "test",
"hashedPassword": "df121e72b850a058bd68f3d05b1f94dc985821a3885b5da38e263654f29843f98bb6d8e594f6042ff871a424602a9bff50dffa97f258bdc1dca4c79e87bac20c",
"salt": "64085e70da64670349f042d4c3d3cac75b20233ffeb71db68399973f60da077d",
"admin": true
}
]
-43
View File
@@ -1,43 +0,0 @@
{
"bridge": {
"name": "HomebridgeTest",
"username": "CC:22:3D:E3:CE:30",
"manufacturer": "homebridge.io",
"model": "homebridge",
"port": 51826,
"pin": "031-45-154"
},
"description": "This is an example configuration file with one fake accessory and one fake platform. You can use this as a template for creating your own configuration file containing devices you actually own.",
"ports": {
"start": 52100,
"end": 52150,
"comment": "This section is used to control the range of ports that separate accessory (like camera or television) should be bind to."
},
"accessories": [
],
"platforms": [
{
"platform" : "MultiTapSwitch",
"name" : "MTS",
"devices": [
{
"name": "MTS1",
"numberConfiguredScenes": 5,
"triggerTimeout": 10,
"resetAfterSwitchOff": false,
"logging": true
},
{
"name": "MTS2",
"numberConfiguredScenes": 5,
"triggerTimeout": 10,
"resetAfterSwitchOff": false,
"triggerSceneAfterSwitchOff": true,
"logging": true
}
]
}
]
}
+11 -12
View File
@@ -1,24 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"target": "ES2018", // ~node10
"module": "commonjs",
"lib": [
"DOM",
"ES2022"
"es2015",
"es2016",
"es2017",
"es2018"
],
"rootDir": "src",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"declaration": true,
"outDir": "dist",
"declarationMap": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
"noImplicitAny": false
},
"include": [
"eslint.config.js",
"homebridge-ui",
"src/"
],
"exclude": [