Compatibility update for Homebridge 2.0

This commit is contained in:
dennis committed 2026-02-08 13:58:38 +01:00
1 parent 08cbdc46b9
commit 904ad68f6d
9 files changed
+3761 -3758

No files matched your search

+3599 -3656
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.3.0", "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>"
+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
+9 -27
View File
@@ -4,24 +4,14 @@ 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[] = [];
@@ -40,25 +30,15 @@ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin {
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});
} }
/** /**
@@ -110,7 +90,7 @@ 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); knownUuids.push(uuid);
} else { } else {
@@ -128,7 +108,7 @@ 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]);
@@ -144,3 +124,5 @@ export class MultiTapSwitchPlatform implements DynamicPlatformPlugin {
} }
} }
} }
export default MultiTapSwitchPlatform
+75 -26
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,25 +45,6 @@ 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;
/**
* Construct the accessory services
* @param platform Parent Homebridge platform
* @param accessory Accessory to which these services will be attached
*/
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;
@@ -69,8 +76,7 @@ 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': 1, 'minValue': 1,
'maxValue': this.Config.NumOfScenes(), 'maxValue': this.Config.NumOfScenes(),
@@ -87,8 +93,7 @@ export class DeviceAccessory {
this.Log.debug('Current Trigger Timeout [' + this.State.triggerTimeout + '] differs from configured value [' + 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.'); this.Config.TriggerTimeout() + ']. It won\'t be updated until it\'s equal again.');
} }
(this.serviceIn.getCharacteristic(this.platform.Characteristic.TriggerTimeout) attachCharacteristic_TriggerTimeout(this.serviceIn, this.api)
|| this.serviceIn.addOptionalCharacteristic(this.platform.Characteristic.TriggerTimeout))
.setProps({ .setProps({
'minValue': 0, 'minValue': 0,
'maxValue': 30, 'maxValue': 30,
@@ -98,6 +103,17 @@ 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());
@@ -277,4 +293,37 @@ export class DeviceAccessory {
this.Log.debug('Get Trigger Timeout ->', 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;
}
} }
+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;
}
+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": [