21 Commits
Author SHA1 Message Date
dennis 62465b1deb Update publish.yml 2026-02-08 17:12:56 +01:00
dennis e74bfeb4e2 Add Test directory and config 2026-02-08 17:04:39 +01:00
dennis b31fbe03fa Update NPMjs publish configuration 2026-02-08 16:54:57 +01:00
dennis 7f97b81f77 Fix code according to ESlint 2026-02-08 16:38:16 +01:00
dennis 79045cfaa0 Fix code according to ESlint 2026-02-08 16:37:02 +01:00
dennis 68988d5098 Fix code according to ESlint 2026-02-08 16:32:14 +01:00
dennis cdc370a69a Fix config.ts according to ESlint 2026-02-08 16:26:27 +01:00
dennis bccc0fa4cd Renamed ESlint config file 2026-02-08 16:23:52 +01:00
dennis 2b840ec462 Update template and workflow files from Homebridge template plugin 2026-02-08 16:21:15 +01:00
dennis a8bdeddabd Update README.md 2026-02-08 14:13:05 +01:00
dennis 904ad68f6d Compatibility update for Homebridge 2.0 2026-02-08 13:58:38 +01:00
dennis 08cbdc46b9 0.3.0 2023-02-24 22:08:30 +01:00
dennis 0134932ba3 Trigger Timeout handling updated 2023-02-24 22:08:29 +01:00
dennis ac935d9cf5 Provide identifiers for better identification in Home apps 2023-02-24 22:06:00 +01:00
dennis 6640dc3d07 Fix UUID handling and removal of unused accessories 2023-02-24 22:06:00 +01:00
dennis 92c9de5c31 Update README.md 2023-02-09 08:18:03 +01:00
dennis ce9c07c6e0 0.2.0 2023-02-05 21:41:31 +01:00
dennis 381a29ec06 Check that characteristic value are in their specified range 2023-02-05 21:41:03 +01:00
dennis 65ef31b84a Updated some log messages and log levels 2023-02-05 21:08:52 +01:00
dennis 419870a7b7 Change the service's manufacturer string 2023-02-05 21:06:39 +01:00
dennis 3fc96b63ed Updated code comments, by introducing function documentations 2023-02-05 21:05:40 +01:00
21 changed files with 4021 additions and 3857 deletions

No files matched your search

-38
View File
@@ -1,38 +0,0 @@
{
"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,5 +19,4 @@ assignees: ''
**Additional context:** **Additional context:**
<!-- Add any other context or screenshots about the feature request here. --> <!-- Add any other context or screenshots about the feature request here. -->
<!-- Click the "Preview" tab before you submit to ensure the formatting is correct. --> <!-- Click the "Preview" tab before you submit to ensure the formatting is correct. -->
+12 -6
View File
@@ -7,15 +7,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false
matrix: matrix:
# the Node.js versions to build on node-version: [20.x, 22.x, 24.x]
node-version: [12.x, 13.x, 14.x, 15.x, 16.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2 uses: actions/setup-node@v6
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@@ -27,5 +27,11 @@ jobs:
- name: Build the project - name: Build the project
run: npm run build run: npm run build
env:
CI: true - 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
+16 -10
View File
@@ -1,18 +1,24 @@
name: Publish Package to npmjs name: Publish Package
on: on:
release: push:
types: [created] tags:
- 'v*'
permissions:
id-token: write # Required for OIDC
contents: read
jobs: jobs:
build: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: '16.x' node-version: '24'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- run: npm ci - run: npm ci
- run: npm run build --if-present
- run: npm publish - run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+3 -1
View File
@@ -10,7 +10,7 @@ src
.github .github
# eslint # eslint
.eslintrc eslint.config.js
# typescript # typescript
tsconfig.json tsconfig.json
@@ -135,3 +135,5 @@ web_modules/
.yarn/unplugged .yarn/unplugged
.yarn/build-state.yml .yarn/build-state.yml
.pnp.* .pnp.*
test/
+4 -2
View File
@@ -1,8 +1,10 @@
{ {
"files.eol": "\n", "files.eol": "\n",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": "explicit"
}, },
"editor.rulers": [ 140 ], "editor.rulers": [
140
],
"eslint.enable": true "eslint.enable": true
} }
+11 -6
View File
@@ -1,5 +1,5 @@
<p align="center"> <p>
<img src="https://github.com/homebridge/branding/raw/master/logos/homebridge-wordmark-logo-vertical.png" width="150" alt="Homebridge"> <img src="https://github.com/homebridge/branding/blob/latest/logos/homebridge-color-round-stylized.png" width="150" alt="Homebridge">
</p> </p>
# Homebridge Multi-Tap-Switch # Homebridge Multi-Tap-Switch
@@ -32,7 +32,7 @@ or manually by following these instructions:
The configuration allows the user to add multiple **Multi-Tap-Switches**. The following parameters are available: The configuration allows the user to add multiple **Multi-Tap-Switches**. The following parameters are available:
| Option | Default Value | Characteristic (runtime configuration) | Description | | Option | Default Value | Characteristic (runtime configuration) | Description |
|------------------------------------|----------------|----------------------------------------|-------------------------------------------------------------------------------------------| |-------------------------------------|----------------|----------------------------------------|-------------------------------------------------------------------------------------------|
| `name` | MultiTapSwitch | | Name of the Platform Accessory (primarily shown in logs) | | `name` | MultiTapSwitch | | Name of the Platform Accessory (primarily shown in logs) |
| `devices[]` | | | Array of configured devices | | `devices[]` | | | Array of configured devices |
| `devices[].name` | | | Name of the switch | | `devices[].name` | | | Name of the switch |
@@ -43,11 +43,10 @@ The configuration allows the user to add multiple **Multi-Tap-Switches**. The fo
The parameter ```triggerTimeout``` can be changed in the configuration during runtime. So, this is just the initial value. 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. The number of The parameter ```numberConfiguredScenes``` just defines how many **programmable** switches are available (currently, there is no limit). The number of
really **used** switches can configured during runtime, between 0 and the configured number. 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 (i.e. > **NOTE:** Additional parameters (also called **Characteristics**) can only be configured via 3rd-party HomeKit apps, but not in the basic Home App!
> Home+, Controller for HomeKit, etc.), but in the basic Home App!
So, an example **platform** plugin config will look as follows: So, an example **platform** plugin config will look as follows:
@@ -66,3 +65,9 @@ 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.
+35
View File
@@ -0,0 +1,35 @@
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,
);
+3601 -3658
View File
File diff suppressed because it is too large. Load diff
+15 -12
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.1.6", "version": "0.4.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": {
@@ -13,8 +13,8 @@
"url": "https://github.com/dtwardow/homebridge-multi-tap-switch/issues" "url": "https://github.com/dtwardow/homebridge-multi-tap-switch/issues"
}, },
"engines": { "engines": {
"node": ">=14.18.1", "node": "^20.18.0 || ^22.10.0 || ^24.0.0",
"homebridge": ">=1.3.5" "homebridge": "^1.8.0 || ^2.0.0-beta.0"
}, },
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
@@ -26,16 +26,19 @@
"keywords": [ "keywords": [
"homebridge-plugin" "homebridge-plugin"
], ],
"dependencies": {
"homebridge-lib": "^7.1.12"
},
"devDependencies": { "devDependencies": {
"@types/node": "^16.10.9", "@eslint/js": "^9.39.1",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@types/node": "^24.10.1",
"@typescript-eslint/parser": "^5.0.0", "eslint": "^9.39.1",
"eslint": "^8.0.1", "homebridge": "^2.0.0-beta.55",
"homebridge": "^1.3.5", "nodemon": "^3.1.11",
"nodemon": "^2.0.13", "rimraf": "^6.1.0",
"rimraf": "^3.0.2", "ts-node": "^10.9.2",
"ts-node": "^10.3.0", "typescript": "^5.9.3",
"typescript": "^4.4.4" "typescript-eslint": "^8.46.4"
}, },
"homepage": "https://github.com/dtwardow/homebridge-multi-tap-switch#readme", "homepage": "https://github.com/dtwardow/homebridge-multi-tap-switch#readme",
"author": "Dennis Twardowsky <twardowsky@gmail.com>" "author": "Dennis Twardowsky <twardowsky@gmail.com>"
+2 -2
View File
@@ -59,13 +59,13 @@ export class DeviceConfig {
export interface AccessoryState { export interface AccessoryState {
numberConfiguredScenes: number; numberConfiguredScenes: number;
triggerTimeout: number; triggerTimeout: number;
//nextSceneToTrigger: number; triggerTimeoutManuallyChanged: boolean;
} }
export function initializeAccessoryState(initConfig: PluginDeviceConfig | AccessoryState): AccessoryState { export function initializeAccessoryState(initConfig: PluginDeviceConfig | AccessoryState): AccessoryState {
return { return {
numberConfiguredScenes: initConfig.numberConfiguredScenes || DEFAULT_NUM_OF_CONFIGURED_SCENES, numberConfiguredScenes: initConfig.numberConfiguredScenes || DEFAULT_NUM_OF_CONFIGURED_SCENES,
triggerTimeout: initConfig.triggerTimeout || DEFAULT_TRIGGER_TIMEOUT, triggerTimeout: initConfig.triggerTimeout || DEFAULT_TRIGGER_TIMEOUT,
//nextSceneToTrigger: 0, triggerTimeoutManuallyChanged: false,
}; };
} }
+1 -1
View File
@@ -1,7 +1,7 @@
import { API } from 'homebridge'; import { API } from 'homebridge';
import { PLATFORM_NAME } from './settings'; import { PLATFORM_NAME } from './settings';
import { MultiTapSwitchPlatform } from './platform'; import MultiTapSwitchPlatform from './platform';
/** /**
* This method registers the platform with Homebridge * This method registers the platform with Homebridge
+42 -30
View File
@@ -4,69 +4,69 @@ import { PLATFORM_NAME, PLUGIN_NAME } from './settings';
import { DeviceAccessory } from './platformAccessory'; import { DeviceAccessory } from './platformAccessory';
// Necessary definitions for custom characteristics! // Necessary definitions for custom characteristics!
import ConfiguredScenes from './types/characteristicConfiguredScenes';
let IConfiguredScenes;
import { initializeAccessoryState, PluginConfig } from './config'; import { initializeAccessoryState, PluginConfig } from './config';
let ITriggerTimeout;
import TriggerTimeout from './types/characteristicTimeout';
/** /**
* HomebridgePlatform * HomebridgePlatform
*/ */
export class MultiTapSwitchPlatform implements DynamicPlatformPlugin { class MultiTapSwitchPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service; public readonly Service: typeof Service;
public readonly Characteristic: public readonly Characteristic: typeof Characteristic;
typeof Characteristic &
typeof IConfiguredScenes &
typeof ITriggerTimeout;
private readonly CharacteristicConfiguredScenes;
private readonly CharacteristicTriggerTimeout;
// Track restored cached accessories // Track restored cached accessories
public readonly accessories: PlatformAccessory[] = []; public readonly accessories: PlatformAccessory[] = [];
public readonly Config: PluginConfig; public readonly Config: PluginConfig;
/**
* Construct the Homebridge platform
* @param log Homebridge Logger
* @param config Homebridge Configuration (from config.json)
* @param api Homebridge API
*/
constructor( constructor(
public readonly log: Logger, public readonly log: Logger,
public readonly config: PlatformConfig, public readonly config: PlatformConfig,
public readonly api: API, public readonly api: API,
) { ) {
this.Config = new PluginConfig(this.config); 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.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', () => { this.api.on('didFinishLaunching', () => {
log.debug('Discover/Register Devices ...'); log.debug('Discover/Register Devices ...');
this.discoverDevices(); 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) { configureAccessory(accessory: PlatformAccessory) {
this.log.info('Load cached Accessory:', 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 // add the restored accessory to the accessories cache, so we can track if it has already been registered
this.accessories.push(accessory); 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 { defineUuid(name: string | undefined, serial = 'UNDEF'): string {
return this.api.hap.uuid.generate(name + '_' + serial); return this.api.hap.uuid.generate(PLATFORM_NAME + '_' + name + '_' + serial);
} }
/**
* Discover new and existing accessories
*/
discoverDevices() { discoverDevices() {
const knownUuids: string[] = [];
const parsedConfig = new PluginConfig(this.config); const parsedConfig = new PluginConfig(this.config);
for (const device of parsedConfig.Devices) { for (const device of parsedConfig.Devices) {
const uuid = this.defineUuid(device.name, '0'); const uuid = this.defineUuid(device.name, '0');
@@ -82,7 +82,7 @@ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin {
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
// existingAccessory.context.device = device; // existingAccessory.context.device = device;
// this.api.updatePlatformAccessories([existingAccessory]); // this.api.updatePlatformAccessories([existingAccessory]);
if (existingAccessory.context.config !== device) { if (JSON.stringify(existingAccessory.context.config) !== JSON.stringify(device)) {
this.log.debug('Update Accessory ->', device.name); this.log.debug('Update Accessory ->', device.name);
existingAccessory.context.config = device; existingAccessory.context.config = device;
this.api.updatePlatformAccessories([existingAccessory]); this.api.updatePlatformAccessories([existingAccessory]);
@@ -90,7 +90,9 @@ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin {
// Create the accessory handler for the restored accessory // Create the accessory handler for the restored accessory
// This is imported from `platformAccessory.ts` // This is imported from `platformAccessory.ts`
new DeviceAccessory(this, existingAccessory); new DeviceAccessory(this, this.api, existingAccessory);
knownUuids.push(uuid);
} else { } else {
// Accessory does not yet exist, so we need to create it // Accessory does not yet exist, so we need to create it
this.log.info('Add Device ->', device.name, '/', this.log.info('Add Device ->', device.name, '/',
@@ -106,11 +108,21 @@ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin {
// Create the accessory handler for the new accessory // Create the accessory handler for the new accessory
// This is imported from `platformAccessory.ts` // This is imported from `platformAccessory.ts`
new DeviceAccessory(this, accessory); new DeviceAccessory(this, this.api, accessory);
// link the accessory to your platform // link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); 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;
+164 -52
View File
@@ -1,8 +1,11 @@
import {CharacteristicValue, PlatformAccessory, Service} from 'homebridge'; import { CharacteristicValue, PlatformAccessory, Service, API } from 'homebridge';
import {MultiTapSwitchPlatform} from './platform'; import MultiTapSwitchPlatform from './platform';
import { AccessoryState, DeviceConfig } from './config'; import { AccessoryState, DeviceConfig } from './config';
import { Logging } from './helper/logger'; import { Logging } from './helper/logger';
import { VERSION } from './settings'; import { VERSION } from './settings';
import { attachCharacteristic_ConfiguredScenes } from './types/characteristicConfiguredScenes';
import { attachCharacteristic_TriggerTimeout } from './types/characteristicTimeout';
import { attachCharacteristic_CurrentScene } from './types/characteristicCurrentScene';
/** /**
* Platform Accessory * Platform Accessory
@@ -11,7 +14,30 @@ export class DeviceAccessory {
private serviceIn: Service; private serviceIn: Service;
private serviceOut: Service; private serviceOut: Service;
private readonly singleButtonEvents = { 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 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 = {
minValue: this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, minValue: this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS,
maxValue: this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, maxValue: this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS,
validValues: [ validValues: [
@@ -19,34 +45,24 @@ 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.Config = new DeviceConfig(accessory.context.config);
this.State = this.accessory.context.persistent; this.State = this.accessory.context.persistent;
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());
// Set accessory information /**
* Accessory information
*/
this.accessory.getService(this.platform.Service.AccessoryInformation)! this.accessory.getService(this.platform.Service.AccessoryInformation)!
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'TippyTapper') .setCharacteristic(this.platform.Characteristic.Manufacturer, 'HB/dtw')
.setCharacteristic(this.platform.Characteristic.Model, 'SceneSwitch') .setCharacteristic(this.platform.Characteristic.Model, 'Multi-Tap-Switch')
.setCharacteristic(this.platform.Characteristic.Version, VERSION) .setCharacteristic(this.platform.Characteristic.Version, VERSION)
.setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.UUID); .setCharacteristic(this.platform.Characteristic.SerialNumber, this.Config.Name());
/**
* Input Service / Switch
*/
// Get or Create the input switch service // Get or Create the input switch service
this.serviceIn = this.accessory.getService(this.platform.Service.Switch) this.serviceIn = this.accessory.getService(this.platform.Service.Switch)
|| this.accessory.addService(this.platform.Service.Switch); || this.accessory.addService(this.platform.Service.Switch);
@@ -60,10 +76,9 @@ export class DeviceAccessory {
if (this.State.numberConfiguredScenes > this.Config.NumOfScenes()) { if (this.State.numberConfiguredScenes > this.Config.NumOfScenes()) {
this.State.numberConfiguredScenes = this.Config.NumOfScenes(); this.State.numberConfiguredScenes = this.Config.NumOfScenes();
} }
(this.serviceIn.getCharacteristic(this.platform.Characteristic.ConfiguredScenes) attachCharacteristic_ConfiguredScenes(this.serviceIn, this.api)
|| this.serviceIn.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredScenes))
.setProps({ .setProps({
'minValue': 0, 'minValue': 1,
'maxValue': this.Config.NumOfScenes(), 'maxValue': this.Config.NumOfScenes(),
'minStep': 1, 'minStep': 1,
}) })
@@ -72,8 +87,13 @@ export class DeviceAccessory {
.updateValue(this.State.numberConfiguredScenes); .updateValue(this.State.numberConfiguredScenes);
// Register handlers for the TriggerTimeout characteristic & restore previous value // Register handlers for the TriggerTimeout characteristic & restore previous value
(this.serviceIn.getCharacteristic(this.platform.Characteristic.TriggerTimeout) if (!this.State.triggerTimeoutManuallyChanged) {
|| this.serviceIn.addOptionalCharacteristic(this.platform.Characteristic.TriggerTimeout)) 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)
.setProps({ .setProps({
'minValue': 0, 'minValue': 0,
'maxValue': 30, 'maxValue': 30,
@@ -83,15 +103,29 @@ export class DeviceAccessory {
.onGet(this.getTriggerTimeout.bind(this)) .onGet(this.getTriggerTimeout.bind(this))
.updateValue(this.State.triggerTimeout); .updateValue(this.State.triggerTimeout);
// Register handlers for the CurrentScene characteristic & restore previous value
attachCharacteristic_CurrentScene(this.serviceIn, this.api)
.setProps({
'minValue': 1,
'maxValue': this.Config.NumOfScenes(),
'minStep': 1,
})
.onSet(this.setCurrentScene.bind(this))
.onGet(this.getCurrentScene.bind(this))
.updateValue(this.NextItemToTrigger);
// 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());
/**
* Output Service / Service Label & Stateless Programmable Switches
*/
// Get or Create the output service (excluding the buttons) // Get or Create the output service (excluding the buttons)
this.serviceOut = this.accessory.getService(this.platform.Service.ServiceLabel) this.serviceOut = this.accessory.getService(this.platform.Service.ServiceLabel)
|| this.accessory.addService(this.platform.Service.ServiceLabel); || this.accessory.addService(this.platform.Service.ServiceLabel);
this.serviceOut.setCharacteristic(this.platform.Characteristic.ServiceLabelNamespace, 1); this.serviceOut.setCharacteristic(this.platform.Characteristic.ServiceLabelNamespace, 1);
// Create max configured number of scene buttons // Create max configured number of programmable switches
for (let i = 1; i <= this.Config.NumOfScenes(); i++) { for (let i = 1; i <= this.Config.NumOfScenes(); i++) {
this.Log.debug('Handle Scene Button -> ', i); this.Log.debug('Handle Scene Button -> ', i);
@@ -103,13 +137,16 @@ export class DeviceAccessory {
this.serviceOut.addLinkedService(newButton); this.serviceOut.addLinkedService(newButton);
newButton.getCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent) newButton.getCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent)
.setProps(this.singleButtonEvents); .setProps(this.singleButtonEvents);
newButton.setCharacteristic(this.platform.Characteristic.ServiceLabelIndex, i); newButton
.setCharacteristic(this.platform.Characteristic.ServiceLabelIndex, i)
.setCharacteristic(this.platform.Characteristic.Name, 'Scene ' + i);
} }
// Push switches to an internal list for later use
this.TriggerButtons.push(newButton); this.TriggerButtons.push(newButton);
} }
// Remove buttons, if max buttons is smaller // Remove any programmable switches, that exceed the maximum of configured switches
for (let i = this.TriggerButtons.length + 1;; i++) { for (let i = this.TriggerButtons.length + 1;; i++) {
const exButton = this.accessory.getService('Scene ' + i); const exButton = this.accessory.getService('Scene ' + i);
if (exButton) { if (exButton) {
@@ -126,7 +163,7 @@ export class DeviceAccessory {
} }
/** /**
* Trigger the next event * Trigger/Fire the next programmable switch event
*/ */
triggerNext() { triggerNext() {
if (this.NextItemToTrigger >= this.State.numberConfiguredScenes) { if (this.NextItemToTrigger >= this.State.numberConfiguredScenes) {
@@ -134,7 +171,7 @@ export class DeviceAccessory {
} }
// Trigger Scene Button // Trigger Scene Button
this.Log.log('Trigger Scene -> ', this.NextItemToTrigger); this.Log.log('Trigger Event -> ', this.NextItemToTrigger);
this.TriggerButtons[this.NextItemToTrigger++].updateCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent, 0); this.TriggerButtons[this.NextItemToTrigger++].updateCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent, 0);
} }
@@ -143,13 +180,15 @@ 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.serviceIn.updateCharacteristic(this.platform.Characteristic.On, this.SwitchOn = false);
this.SwitchOn = false);
this.NextItemToTrigger = 0; this.NextItemToTrigger = 0;
} }
/**
// Switch Handlers * 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
*/
async setOn(value: CharacteristicValue) { async setOn(value: CharacteristicValue) {
// implement your own code to turn your device on/off // implement your own code to turn your device on/off
this.SwitchOn = value as boolean; this.SwitchOn = value as boolean;
@@ -159,7 +198,7 @@ export class DeviceAccessory {
clearTimeout(this.SwitchResetTimer); clearTimeout(this.SwitchResetTimer);
} }
// Start the reset timer, if paramter is greater than 0. // Start the reset timer, if parameter is greater than 0.
// Otherwise, do nothing. // Otherwise, do nothing.
if (this.State.triggerTimeout > 0) { if (this.State.triggerTimeout > 0) {
this.SwitchResetTimer = setTimeout(this.resetTrigger.bind(this), this.State.triggerTimeout * 1000); this.SwitchResetTimer = setTimeout(this.resetTrigger.bind(this), this.State.triggerTimeout * 1000);
@@ -174,44 +213,117 @@ export class DeviceAccessory {
this.resetTrigger(); this.resetTrigger();
} }
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> { async getOn(): Promise<CharacteristicValue> {
const isOn = this.SwitchOn; const isOn = this.SwitchOn;
this.Log.debug('Get Characteristic On ->', isOn); this.Log.debug('Get Switch State ->', isOn);
// if you need to return an error to show the device as "Not Responding" in the Home app: // 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); // throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
return isOn; return isOn;
} }
// NumScenes Handlers /**
* 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 setItemsToHandle(value: CharacteristicValue) { async setItemsToHandle(value: CharacteristicValue) {
const numItemsToHandle = value as number; let numItemsToHandle = value as number;
this.Log.debug('Set Characteristic NumScenes ->', numItemsToHandle);
this.State.numberConfiguredScenes = numItemsToHandle;
//this.updatePersistentData(); 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;
}
} }
this.State.numberConfiguredScenes = numItemsToHandle;
}
/**
* Report number of active switches back to HomeKit (for visualization)
*/
async getItemsToHandle(): Promise<CharacteristicValue> { async getItemsToHandle(): Promise<CharacteristicValue> {
const numItemsToHandle = this.State.numberConfiguredScenes; const numItemsToHandle = this.State.numberConfiguredScenes;
this.Log.debug('Get Characteristic NumScenes ->', numItemsToHandle); this.Log.debug('Get Number of Active Switches ->', numItemsToHandle);
return numItemsToHandle; return numItemsToHandle;
} }
// TriggerTimeout Handlers /**
* Set the timeout before resetting the trigger counter.
* @param value Time in seconds
*/
async setTriggerTimeout(value: CharacteristicValue) { async setTriggerTimeout(value: CharacteristicValue) {
const triggerTimeout = value as number; let triggerTimeout = value as number;
this.Log.debug('Set Characteristic TriggerTimeout ->', triggerTimeout);
this.State.triggerTimeout = triggerTimeout;
//this.updatePersistentData(); 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!');
}
this.State.triggerTimeout = triggerTimeout;
}
/**
* Report the timeout before resetting the trigger back to HomeKit (for visualization)
*/
async getTriggerTimeout(): Promise<CharacteristicValue> { async getTriggerTimeout(): Promise<CharacteristicValue> {
const triggerTimeout = this.State.triggerTimeout; const triggerTimeout = this.State.triggerTimeout;
this.Log.debug('Get Characteristic TriggerTimeout ->', triggerTimeout); this.Log.debug('Get Trigger Timeout ->', triggerTimeout);
return 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;
}
} }
+1 -2
View File
@@ -1,5 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires import package_config from '../package.json';
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 * This is the name of the platform that users will use to register the plugin in the Homebridge config.json
+13 -12
View File
@@ -1,22 +1,23 @@
import { Formats, Perms } from 'homebridge'; import { API, Characteristic, Formats, Perms, Service } from 'homebridge';
export = (homebridge) => { const DISPLAY_NAME = 'Configured Scenes';
const CustomCharacteristic = homebridge.hap.Characteristic; const UUID = 'A8E3F4DF-5E5D-6A7F-8FA7-A22FA5AE9F53';
const characteristicUUID = 'A8E3F4DF-5E5D-6A7F-8FA7-A22FA5AE9F53';
return class ConfiguredScenes extends CustomCharacteristic { export function attachCharacteristic_ConfiguredScenes(target: Service, api: API): Characteristic {
public static readonly UUID: string = characteristicUUID; let result: Characteristic;
constructor() { if (target.testCharacteristic(DISPLAY_NAME)) {
super('Configured Scenes', ConfiguredScenes.UUID, { result = target.getCharacteristic(DISPLAY_NAME)!; // Already tested it exists
} else {
result = target.addCharacteristic(new api.hap.Characteristic(DISPLAY_NAME, UUID, {
format: Formats.UINT8, format: Formats.UINT8,
//unit: '', //unit: '',
minValue: 0, minValue: 0,
maxValue: 0, maxValue: 0,
minStep: 1, minStep: 1,
perms: [Perms.PAIRED_WRITE, Perms.PAIRED_READ, Perms.NOTIFY], perms: [Perms.PAIRED_WRITE, Perms.PAIRED_READ, Perms.NOTIFY],
}); }));
this.value = this.getDefaultValue(); }
return result;
} }
};
};
+23
View File
@@ -0,0 +1,23 @@
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;
}
+13 -12
View File
@@ -1,22 +1,23 @@
import { Formats, Perms } from 'homebridge'; import { API, Characteristic, Formats, Perms, Service } from 'homebridge';
export = (homebridge) => { const DISPLAY_NAME = 'Trigger Timeout';
const CustomCharacteristic = homebridge.hap.Characteristic; const UUID = 'E2C19A02-1D2F-F592-A2BF-BF920014FB18';
const characteristicUUID = 'E2C19A02-1D2F-F592-A2BF-BF920014FB18';
return class TriggerTimeout extends CustomCharacteristic { export function attachCharacteristic_TriggerTimeout(target: Service, api: API): Characteristic {
public static readonly UUID: string = characteristicUUID; let result: Characteristic;
constructor() { if (target.testCharacteristic(DISPLAY_NAME)) {
super('Trigger Timeout', TriggerTimeout.UUID, { result = target.getCharacteristic(DISPLAY_NAME)!; // Already tested it exists
} else {
result = target.addCharacteristic(new api.hap.Characteristic(DISPLAY_NAME, UUID, {
format: Formats.UINT16, format: Formats.UINT16,
//unit: '', //unit: '',
minValue: 0, minValue: 0,
maxValue: 60, maxValue: 60,
minStep: 1, minStep: 1,
perms: [Perms.PAIRED_WRITE, Perms.PAIRED_READ, Perms.NOTIFY], perms: [Perms.PAIRED_WRITE, Perms.PAIRED_READ, Perms.NOTIFY],
}); }));
this.value = this.getDefaultValue(); }
return result;
} }
};
};
+10
View File
@@ -0,0 +1,10 @@
[
{
"id": 1,
"username": "test",
"name": "test",
"hashedPassword": "df121e72b850a058bd68f3d05b1f94dc985821a3885b5da38e263654f29843f98bb6d8e594f6042ff871a424602a9bff50dffa97f258bdc1dca4c79e87bac20c",
"salt": "64085e70da64670349f042d4c3d3cac75b20233ffeb71db68399973f60da077d",
"admin": true
}
]
+42
View File
@@ -0,0 +1,42 @@
{
"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,
"logging": true
}
]
}
]
}
+13 -12
View File
@@ -1,23 +1,24 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2018", // ~node10 "target": "ES2022",
"module": "commonjs",
"lib": [ "lib": [
"es2015", "DOM",
"es2016", "ES2022"
"es2017",
"es2018"
], ],
"declaration": true, "rootDir": "src",
"declarationMap": true, "module": "nodenext",
"sourceMap": true, "moduleResolution": "nodenext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true, "strict": true,
"declaration": true,
"outDir": "dist",
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true, "esModuleInterop": true,
"noImplicitAny": false "forceConsistentCasingInFileNames": true
}, },
"include": [ "include": [
"eslint.config.js",
"homebridge-ui",
"src/" "src/"
], ],
"exclude": [ "exclude": [