homebridge-cycle-light-switches 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # homebridge-cycle-light-switches
2
+
3
+ A [Homebridge](https://homebridge.io) plugin that adds any number of virtual on/off
4
+ lights. Every light is paired with a configurable number of virtual, single-press
5
+ switches. Each time the virtual light is turned on — from the Home app, Siri, or an
6
+ automation — the plugin fires the *next* switch in a round-robin rotation. Point
7
+ HomeKit automations at each switch's "Single Press" trigger to chain further actions
8
+ off a light being turned on, cycling through a different action every time.
9
+
10
+ Built from the [official Homebridge plugin template](https://github.com/homebridge/homebridge-plugin-template),
11
+ using the [Homebridge Plugin API](https://developers.homebridge.io/#/).
12
+
13
+ ## Why?
14
+
15
+ The basic problem to solve is using a trigger (e.g. a wall switch) to trigger different
16
+ actions (e.g. different light scenes/scenarios) in sequence. The first time you hit the
17
+ a switch you might want to set a mood, the second time blaze all lights max, and finally
18
+ turn everything off. And the following time start the cycle again.
19
+
20
+ I used to achieved this by nested if/else in Shortcuts in Apple Home, but after switching
21
+ to Matter over Thread, the delay of several seconds for running a shortcut became very
22
+ apparent, and annoying. So this plugin runs the logic without having to slow everything
23
+ down with Shortcuts.
24
+
25
+ ## How?
26
+
27
+ This plugin implements HAP services:
28
+
29
+ - **Light** → a HAP `Lightbulb` service exposing only the `On` characteristic (no
30
+ brightness/color) — an on/off-only light.
31
+ - **Switch** → a HAP `StatelessProgrammableSwitch` service, restricted to only ever
32
+ report a `SINGLE_PRESS` event — HomeKit's stateless "button" accessory type, which is
33
+ what Home/Homebridge calls a Generic Switch, configured for single press only. When a
34
+ light has more than one switch, they're grouped under a `ServiceLabel` service, which
35
+ is the standard HomeKit pattern for exposing several buttons on one accessory.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install
41
+ npm run build
42
+ ```
43
+
44
+ Then either:
45
+
46
+ - `npm link` and add it to a local Homebridge instance's `node_modules`, or
47
+ - publish it to npm and install it normally (`npm install -g homebridge-cycle-light-switches`).
48
+
49
+ ## Configuration
50
+
51
+ Add a platform block to Homebridge's `config.json` (or use the plugin's settings UI,
52
+ which reads `config.schema.json`):
53
+
54
+ ```json
55
+ {
56
+ "platforms": [
57
+ {
58
+ "platform": "CycleLightSwitches",
59
+ "name": "Cycle Light Switches",
60
+ "lights": [
61
+ {
62
+ "name": "Kitchen Scene",
63
+ "switchCount": 3,
64
+ "resetCountOnOff": false
65
+ },
66
+ {
67
+ "name": "Spotlights",
68
+ "switchCount": 3,
69
+ "resetCountOnOff": true,
70
+ "turnOffOnCycleComplete": true
71
+ },
72
+ {
73
+ "name": "Front Door Chime",
74
+ "switchCount": 2,
75
+ "resetCountOnOff": true,
76
+ "switchNames": ["Front Door Chime First", "Front Door Chime Second"]
77
+ }
78
+ ]
79
+ }
80
+ ]
81
+ }
82
+ ```
83
+
84
+ ### Options (per light)
85
+
86
+ | Option | Type | Default | Description |
87
+ | --- | --- | --- | --- |
88
+ | `name` | string | — | Display name of the light. Must be unique. Renaming a light creates a new HomeKit accessory (and resets its switch counter), since the accessory identity is derived from the name. |
89
+ | `switchCount` | integer | `1` | How many switches accompany this light. |
90
+ | `resetCountOnOff` | boolean | `false` | See below. |
91
+ | `turnOffOnCycleComplete` | boolean | `false` | See below. |
92
+ | `switchNames` | string[] | — | Optional custom names for each switch, in order. Falls back to `"<Light Name> Switch <n>"`. |
93
+
94
+ ## Behavior
95
+
96
+ Turning the light **on** — regardless of whether it was already on (e.g. an automation
97
+ re-sends "turn on" while it's already on) — advances an internal counter and fires
98
+ exactly one switch: the counter modulo `switchCount`. Turning the light **off** never
99
+ fires a switch.
100
+
101
+ **`resetCountOnOff: false` (default)** — the rotation never resets. With 3 switches,
102
+ turning the light on four times in a row fires switches `1, 2, 3, 1`, in that order,
103
+ no matter how many times the light was turned off in between.
104
+
105
+ **`resetCountOnOff: true`** — turning the light off resets the rotation back to the
106
+ start. With 3 switches, turning on twice fires `1, 2`; turning the light off; turning
107
+ it on twice more fires `1, 2` again — i.e. the full sequence is `1, 2, (off), 1, 2`.
108
+
109
+ **`turnOffOnCycleComplete: true`** — as soon as the switch that completes a full
110
+ rotation fires (e.g. switch 3 of 3), the plugin turns the virtual light off on
111
+ its own, immediately, as if it were a momentary trigger rather than something you
112
+ leave on. This is useful to combine an automation, e.g. a physical switch trigger
113
+ to cycle scenes, with being able to control the lights with buttons in the
114
+ Home app (see Advanced cycle below). This combines with `resetCountOnOff`: if
115
+ both are enabled, both manual and automatic off resets the rotation, so the next
116
+ "on" whether from trigger or button press, always starts the cycle from switch 1.
117
+
118
+ Other than that reset, the rotation position is kept indefinitely: it's stored in
119
+ Homebridge's accessory cache on disk, so it survives Homebridge restarts. It only
120
+ resets to zero if you explicitly enable `resetCountOnOff`, or if the accessory is
121
+ removed (e.g. by renaming the light or deleting it from the config).
122
+
123
+ ## Example automations
124
+
125
+ ### Simple cycle
126
+
127
+ 1. In the Home app, create an automation (or a button effect) that turns on "Kitchen
128
+ Scene".
129
+ 2. On "Kitchen Scene" there are Button 1, Button 2, Button 3. On Button 1, run
130
+ scene A or set lights as required. Repeat for Switch 2 → scene B, Switch 3 →
131
+ scene C.
132
+ 3. Every time you (or an automation) turn on "Kitchen Scene", it cycles through
133
+ scenes A, B, C, A, B, C, ...
134
+
135
+ ### Advanced on/off scenes
136
+
137
+ Used for light scenes when the last step is always turn off, and both automation
138
+ triggers and manual control is used in combination.
139
+
140
+ 1. In the Home app, set one or several automation triggers to turn on "Spotlights".
141
+ 2. On "Spotlights" there are Button 1, Button 2, Button 3. Use Button 1 and
142
+ Button 2 to control lights (or any devices) for two distinct scenes.
143
+ 3. Leave Button 3 empty, instead create an automation that turns off your scene/lights
144
+ when "Spotlights" is turned off.
145
+
146
+ Since `turnOffOnCycleComplete` is true, the scene/lights will turn off anyway on the third
147
+ "turn on". They will also turn off when manually turning off "Spotlights" in the app, and
148
+ `resetCountOnOff` prepares for the next automated trigger or manual "turn on" of "Spotlights"
149
+ in the app to trigger the first switch (i.e. Button 1).
150
+
151
+ ## Development
152
+
153
+ - `npm run watch` — build, `npm link` the plugin, and run Homebridge with `nodemon` for
154
+ live reload (requires a local Homebridge config; see `homebridge-plugin-template`'s
155
+ docs for setting up `test/hbConfig`).
156
+ - `npm run lint` — lint with ESLint.
@@ -0,0 +1,63 @@
1
+ {
2
+ "pluginAlias": "CycleLightSwitches",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "strictValidation": true,
6
+ "headerDisplay": "Adds one or more virtual on/off lights. Every light triggers a configurable number of virtual single-press switches, one at a time in rotation, each time the light is turned on — whether from the Home app, Siri, or an automation. Point HomeKit automations at each switch's \"Single Press\" event to chain further actions.",
7
+ "footerDisplay": "See the plugin README for a full walkthrough of the round-robin switch behavior and example automations.",
8
+ "schema": {
9
+ "type": "object",
10
+ "properties": {
11
+ "name": {
12
+ "title": "Platform Name",
13
+ "type": "string",
14
+ "default": "Cycle Light Switches"
15
+ },
16
+ "lights": {
17
+ "title": "Lights",
18
+ "type": "array",
19
+ "default": [],
20
+ "items": {
21
+ "type": "object",
22
+ "title": "Light",
23
+ "required": ["name", "switchCount"],
24
+ "properties": {
25
+ "name": {
26
+ "title": "Light Name",
27
+ "type": "string",
28
+ "description": "Displayed name of the virtual light. Must be unique across all lights in this plugin."
29
+ },
30
+ "switchCount": {
31
+ "title": "Number of Switches",
32
+ "type": "integer",
33
+ "minimum": 1,
34
+ "maximum": 99,
35
+ "default": 1,
36
+ "description": "How many virtual single-press switches accompany this light."
37
+ },
38
+ "resetCountOnOff": {
39
+ "title": "Reset switch sequence when the light turns off",
40
+ "type": "boolean",
41
+ "default": false,
42
+ "description": "Off (default): the rotation never resets — turning the light on repeatedly keeps cycling through the switches no matter how many times it's turned off in between. On: turning the light off resets the rotation, so the next time it's turned on, switch 1 fires again."
43
+ },
44
+ "turnOffOnCycleComplete": {
45
+ "title": "Turn off the light when a cycle completes",
46
+ "type": "boolean",
47
+ "default": false,
48
+ "description": "Off (default): the light stays on after firing a switch. On: as soon as the last switch in the rotation fires, the light turns itself back off automatically (as if it were a momentary trigger). If \"Reset switch sequence when the light turns off\" is also enabled, this auto-off resets the rotation too."
49
+ },
50
+ "switchNames": {
51
+ "title": "Custom Switch Names (optional)",
52
+ "type": "array",
53
+ "items": {
54
+ "type": "string"
55
+ },
56
+ "description": "Optional display names for each switch, in order (first entry = switch 1). Leave empty to auto-name them \"<Light Name> Switch <n>\"."
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,6 @@
1
+ import type { API } from 'homebridge';
2
+ /**
3
+ * This method registers the platform with Homebridge
4
+ */
5
+ declare const _default: (api: API) => void;
6
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import { CycleLightSwitchesPlatform } from './platform.js';
2
+ import { PLATFORM_NAME } from './settings.js';
3
+ /**
4
+ * This method registers the platform with Homebridge
5
+ */
6
+ export default (api) => {
7
+ api.registerPlatform(PLATFORM_NAME, CycleLightSwitchesPlatform);
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C;;GAEG;AACH,eAAe,CAAC,GAAQ,EAAE,EAAE;IAC1B,GAAG,CAAC,gBAAgB,CAAC,aAAa,EAAE,0BAA0B,CAAC,CAAC;AAClE,CAAC,CAAC"}
@@ -0,0 +1,44 @@
1
+ import type { API, Characteristic, DynamicPlatformPlugin, Logging, PlatformAccessory, PlatformConfig, Service } from 'homebridge';
2
+ /**
3
+ * Normalized, validated configuration for a single virtual light.
4
+ */
5
+ export interface LightConfig {
6
+ name: string;
7
+ switchCount: number;
8
+ resetCountOnOff: boolean;
9
+ turnOffOnCycleComplete: boolean;
10
+ switchNames?: string[];
11
+ }
12
+ /**
13
+ * CycleLightSwitchesPlatform
14
+ *
15
+ * Reads the `lights` array from the plugin config and maintains one accessory per light.
16
+ * Each accessory exposes an on/off-only Lightbulb service plus `switchCount` stateless,
17
+ * single-press "Generic Switch" services. See `virtualLightAccessory.ts` for the
18
+ * round-robin firing logic.
19
+ */
20
+ export declare class CycleLightSwitchesPlatform implements DynamicPlatformPlugin {
21
+ readonly log: Logging;
22
+ readonly config: PlatformConfig;
23
+ readonly api: API;
24
+ readonly Service: typeof Service;
25
+ readonly Characteristic: typeof Characteristic;
26
+ readonly accessories: Map<string, PlatformAccessory>;
27
+ readonly discoveredCacheUUIDs: string[];
28
+ constructor(log: Logging, config: PlatformConfig, api: API);
29
+ /**
30
+ * Invoked when Homebridge restores a cached accessory from disk at startup.
31
+ */
32
+ configureAccessory(accessory: PlatformAccessory): void;
33
+ /** Whether the config has at least one entry in its `lights` array. */
34
+ private hasLightsConfigured;
35
+ /**
36
+ * Reads, validates and normalizes the `lights` array from the user's config.
37
+ */
38
+ private getLightConfigs;
39
+ /**
40
+ * Creates/updates one accessory per configured light, and removes accessories for
41
+ * lights that have been deleted from the config.
42
+ */
43
+ discoverDevices(): void;
44
+ }
@@ -0,0 +1,134 @@
1
+ import { VirtualLightAccessory } from './virtualLightAccessory.js';
2
+ import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
3
+ /**
4
+ * CycleLightSwitchesPlatform
5
+ *
6
+ * Reads the `lights` array from the plugin config and maintains one accessory per light.
7
+ * Each accessory exposes an on/off-only Lightbulb service plus `switchCount` stateless,
8
+ * single-press "Generic Switch" services. See `virtualLightAccessory.ts` for the
9
+ * round-robin firing logic.
10
+ */
11
+ export class CycleLightSwitchesPlatform {
12
+ log;
13
+ config;
14
+ api;
15
+ Service;
16
+ Characteristic;
17
+ // Tracks cached accessories restored from disk at startup, keyed by UUID.
18
+ accessories = new Map();
19
+ discoveredCacheUUIDs = [];
20
+ constructor(log, config, api) {
21
+ this.log = log;
22
+ this.config = config;
23
+ this.api = api;
24
+ this.Service = api.hap.Service;
25
+ this.Characteristic = api.hap.Characteristic;
26
+ this.log.debug('Finished initializing platform:', this.config.name);
27
+ // Dynamic Platform plugins should only register accessories after Homebridge has
28
+ // restored all cached accessories from disk, which is signalled by this event.
29
+ this.api.on('didFinishLaunching', () => {
30
+ this.log.debug('Executed didFinishLaunching callback');
31
+ // Per the Homebridge Verified plugin requirements ("The plugin must successfully
32
+ // install and not start unless it is configured" -
33
+ // https://github.com/homebridge/plugins/wiki/Verified-Plugins): if the user hasn't
34
+ // configured any lights yet, and there's nothing previously registered that needs
35
+ // cleaning up, don't do anything at all rather than running discovery for its own
36
+ // sake.
37
+ if (!this.hasLightsConfigured() && this.accessories.size === 0) {
38
+ this.log.info('No lights are configured yet. Add at least one entry to the "lights" array in '
39
+ + 'this plugin\'s settings to use it. Nothing will happen until then.');
40
+ return;
41
+ }
42
+ this.discoverDevices();
43
+ });
44
+ }
45
+ /**
46
+ * Invoked when Homebridge restores a cached accessory from disk at startup.
47
+ */
48
+ configureAccessory(accessory) {
49
+ this.log.info('Loading accessory from cache:', accessory.displayName);
50
+ this.accessories.set(accessory.UUID, accessory);
51
+ }
52
+ /** Whether the config has at least one entry in its `lights` array. */
53
+ hasLightsConfigured() {
54
+ return Array.isArray(this.config.lights) && this.config.lights.length > 0;
55
+ }
56
+ /**
57
+ * Reads, validates and normalizes the `lights` array from the user's config.
58
+ */
59
+ getLightConfigs() {
60
+ const rawLights = Array.isArray(this.config.lights) ? this.config.lights : [];
61
+ const seenNames = new Set();
62
+ const lights = [];
63
+ for (const raw of rawLights) {
64
+ const name = typeof raw?.name === 'string' ? raw.name.trim() : '';
65
+ if (!name) {
66
+ this.log.warn('Ignoring a light entry in the config with a missing or empty "name".');
67
+ continue;
68
+ }
69
+ if (seenNames.has(name)) {
70
+ this.log.warn(`Ignoring duplicate light "${name}" in the config; light names must be unique.`);
71
+ continue;
72
+ }
73
+ seenNames.add(name);
74
+ let switchCount = Number(raw?.switchCount);
75
+ if (!Number.isFinite(switchCount) || switchCount < 1) {
76
+ this.log.warn(`Light "${name}" has an invalid "switchCount"; defaulting to 1.`);
77
+ switchCount = 1;
78
+ }
79
+ switchCount = Math.floor(switchCount);
80
+ const switchNames = Array.isArray(raw?.switchNames)
81
+ ? raw.switchNames.filter((n) => typeof n === 'string' && n.trim().length > 0)
82
+ : undefined;
83
+ lights.push({
84
+ name,
85
+ switchCount,
86
+ resetCountOnOff: Boolean(raw?.resetCountOnOff),
87
+ turnOffOnCycleComplete: Boolean(raw?.turnOffOnCycleComplete),
88
+ switchNames,
89
+ });
90
+ }
91
+ return lights;
92
+ }
93
+ /**
94
+ * Creates/updates one accessory per configured light, and removes accessories for
95
+ * lights that have been deleted from the config.
96
+ */
97
+ discoverDevices() {
98
+ const lights = this.getLightConfigs();
99
+ if (lights.length === 0) {
100
+ this.log.warn('No lights are configured. Add at least one entry to the "lights" array in this plugin\'s config.');
101
+ }
102
+ for (const light of lights) {
103
+ // The UUID is derived from the light's name, so renaming a light in the config
104
+ // will create a new HomeKit accessory (and a fresh switch counter).
105
+ const uuid = this.api.hap.uuid.generate(`${PLUGIN_NAME}-light-${light.name}`);
106
+ const existingAccessory = this.accessories.get(uuid);
107
+ if (existingAccessory) {
108
+ this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
109
+ // Refresh the config-derived fields, but leave `context.state` (the persisted
110
+ // switch counter) untouched — that's handled inside VirtualLightAccessory.
111
+ existingAccessory.context.light = light;
112
+ existingAccessory.displayName = light.name;
113
+ this.api.updatePlatformAccessories([existingAccessory]);
114
+ new VirtualLightAccessory(this, existingAccessory);
115
+ }
116
+ else {
117
+ this.log.info('Adding new accessory:', light.name);
118
+ const accessory = new this.api.platformAccessory(light.name, uuid);
119
+ accessory.context.light = light;
120
+ new VirtualLightAccessory(this, accessory);
121
+ this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
122
+ }
123
+ this.discoveredCacheUUIDs.push(uuid);
124
+ }
125
+ // Remove any cached accessories for lights that are no longer in the config.
126
+ for (const [uuid, accessory] of this.accessories) {
127
+ if (!this.discoveredCacheUUIDs.includes(uuid)) {
128
+ this.log.info('Removing accessory no longer present in config:', accessory.displayName);
129
+ this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAa3D;;;;;;;GAOG;AACH,MAAM,OAAO,0BAA0B;IASnB;IACA;IACA;IAVF,OAAO,CAAiB;IACxB,cAAc,CAAwB;IAEtD,0EAA0E;IAC1D,WAAW,GAAmC,IAAI,GAAG,EAAE,CAAC;IACxD,oBAAoB,GAAa,EAAE,CAAC;IAEpD,YACkB,GAAY,EACZ,MAAsB,EACtB,GAAQ;QAFR,QAAG,GAAH,GAAG,CAAS;QACZ,WAAM,GAAN,MAAM,CAAgB;QACtB,QAAG,GAAH,GAAG,CAAK;QAExB,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,cAAc,GAAG,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC;QAE7C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,iCAAiC,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAEpE,iFAAiF;QACjF,+EAA+E;QAC/E,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE;YACrC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;YAEvD,iFAAiF;YACjF,mDAAmD;YACnD,mFAAmF;YACnF,kFAAkF;YAClF,kFAAkF;YAClF,QAAQ;YACR,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBAC/D,IAAI,CAAC,GAAG,CAAC,IAAI,CACX,gFAAgF;sBAC9E,oEAAoE,CACvE,CAAC;gBACF,OAAO;YACT,CAAC;YAED,IAAI,CAAC,eAAe,EAAE,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACH,kBAAkB,CAAC,SAA4B;QAC7C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,+BAA+B,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;QACtE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAClD,CAAC;IAED,uEAAuE;IAC/D,mBAAmB;QACzB,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IAC5E,CAAC;IAED;;OAEG;IACK,eAAe;QACrB,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9E,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;QACpC,MAAM,MAAM,GAAkB,EAAE,CAAC;QAEjC,KAAK,MAAM,GAAG,IAAI,SAAsC,EAAE,CAAC;YACzD,MAAM,IAAI,GAAG,OAAO,GAAG,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAElE,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;gBACtF,SAAS;YACX,CAAC;YAED,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,6BAA6B,IAAI,8CAA8C,CAAC,CAAC;gBAC/F,SAAS;YACX,CAAC;YACD,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAEpB,IAAI,WAAW,GAAG,MAAM,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;YAC3C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;gBACrD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,IAAI,kDAAkD,CAAC,CAAC;gBAChF,WAAW,GAAG,CAAC,CAAC;YAClB,CAAC;YACD,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YAEtC,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,WAAW,CAAC;gBACjD,CAAC,CAAE,GAAG,CAAC,WAAyB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;gBACzG,CAAC,CAAC,SAAS,CAAC;YAEd,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI;gBACJ,WAAW;gBACX,eAAe,EAAE,OAAO,CAAC,GAAG,EAAE,eAAe,CAAC;gBAC9C,sBAAsB,EAAE,OAAO,CAAC,GAAG,EAAE,sBAAsB,CAAC;gBAC5D,WAAW;aACZ,CAAC,CAAC;QACL,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;OAGG;IACH,eAAe;QACb,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QAEtC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,kGAAkG,CAAC,CAAC;QACpH,CAAC;QAED,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,+EAA+E;YAC/E,oEAAoE;YACpE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,WAAW,UAAU,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YAC9E,MAAM,iBAAiB,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAErD,IAAI,iBAAiB,EAAE,CAAC;gBACtB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,0CAA0C,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAC;gBAEzF,8EAA8E;gBAC9E,2EAA2E;gBAC3E,iBAAiB,CAAC,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;gBACxC,iBAAiB,CAAC,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC;gBAC3C,IAAI,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC;gBAExD,IAAI,qBAAqB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;YACrD,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,uBAAuB,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;gBAEnD,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;gBACnE,SAAS,CAAC,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;gBAEhC,IAAI,qBAAqB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;gBAE3C,IAAI,CAAC,GAAG,CAAC,2BAA2B,CAAC,WAAW,EAAE,aAAa,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;YAChF,CAAC;YAED,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvC,CAAC;QAED,6EAA6E;QAC7E,KAAK,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACjD,IAAI,CAAC,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC9C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,iDAAiD,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;gBACxF,IAAI,CAAC,GAAG,CAAC,6BAA6B,CAAC,WAAW,EAAE,aAAa,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;YAClF,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This is the name of the platform that users will use to register the plugin in the Homebridge config.json
3
+ */
4
+ export declare const PLATFORM_NAME = "CycleLightSwitches";
5
+ /**
6
+ * This must match the name of the plugin as defined in package.json's `name` property
7
+ */
8
+ export declare const PLUGIN_NAME = "homebridge-cycle-light-switches";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * This is the name of the platform that users will use to register the plugin in the Homebridge config.json
3
+ */
4
+ export const PLATFORM_NAME = 'CycleLightSwitches';
5
+ /**
6
+ * This must match the name of the plugin as defined in package.json's `name` property
7
+ */
8
+ export const PLUGIN_NAME = 'homebridge-cycle-light-switches';
9
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,oBAAoB,CAAC;AAElD;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,iCAAiC,CAAC"}
@@ -0,0 +1,48 @@
1
+ import type { PlatformAccessory } from 'homebridge';
2
+ import type { CycleLightSwitchesPlatform } from './platform.js';
3
+ /**
4
+ * VirtualLightAccessory
5
+ *
6
+ * Represents one virtual on/off light plus its `switchCount` stateless single-press
7
+ * switches. Every time the light's On characteristic is set to `true` — regardless of
8
+ * whether it was already on, e.g. because an automation re-fired "turn on" — the next
9
+ * switch in the rotation fires a single-press event. The rotation position is a
10
+ * monotonically increasing counter persisted in accessory.context; optionally it can be
11
+ * reset back to the start whenever the light is turned off (`resetCountOnOff`), and the
12
+ * light can optionally turn itself back off as soon as the last switch in the cycle fires
13
+ * (`turnOffOnCycleComplete`).
14
+ */
15
+ export declare class VirtualLightAccessory {
16
+ private readonly platform;
17
+ private readonly accessory;
18
+ private readonly light;
19
+ private readonly state;
20
+ private lightService;
21
+ private switchServices;
22
+ constructor(platform: CycleLightSwitchesPlatform, accessory: PlatformAccessory);
23
+ private setupAccessoryInformation;
24
+ /** Sets up the on/off-only "light" (HAP Lightbulb service exposing only the On characteristic). */
25
+ private setupLight;
26
+ /**
27
+ * Sets up `switchCount` stateless, single-press-only switches (HAP
28
+ * StatelessProgrammableSwitch, restricted to the SINGLE_PRESS event — HomeKit's closest
29
+ * equivalent to a Matter "Generic Switch" configured for single press).
30
+ *
31
+ * When more than one switch is configured, they're grouped under a Service Label, which
32
+ * is the standard HomeKit pattern for exposing several buttons on one accessory.
33
+ */
34
+ private setupSwitches;
35
+ /**
36
+ * Handles "set" requests for the light's On characteristic — from the Home app, Siri,
37
+ * or an automation. Turning the light on (from any starting state) advances the
38
+ * rotation and fires exactly one switch; turning it off never fires a switch, and only
39
+ * resets the rotation if `resetCountOnOff` is enabled.
40
+ *
41
+ * If `turnOffOnCycleComplete` is enabled and the switch that just fired was the last one
42
+ * in the rotation, the light is immediately switched back off on its own (as if a
43
+ * momentary trigger), applying the same `resetCountOnOff` behavior that a user-initiated
44
+ * off would.
45
+ */
46
+ private handleSetOn;
47
+ private persistState;
48
+ }
@@ -0,0 +1,150 @@
1
+ const SWITCH_SUBTYPE_PREFIX = 'switch-';
2
+ /**
3
+ * VirtualLightAccessory
4
+ *
5
+ * Represents one virtual on/off light plus its `switchCount` stateless single-press
6
+ * switches. Every time the light's On characteristic is set to `true` — regardless of
7
+ * whether it was already on, e.g. because an automation re-fired "turn on" — the next
8
+ * switch in the rotation fires a single-press event. The rotation position is a
9
+ * monotonically increasing counter persisted in accessory.context; optionally it can be
10
+ * reset back to the start whenever the light is turned off (`resetCountOnOff`), and the
11
+ * light can optionally turn itself back off as soon as the last switch in the cycle fires
12
+ * (`turnOffOnCycleComplete`).
13
+ */
14
+ export class VirtualLightAccessory {
15
+ platform;
16
+ accessory;
17
+ light;
18
+ state;
19
+ lightService;
20
+ switchServices = [];
21
+ constructor(platform, accessory) {
22
+ this.platform = platform;
23
+ this.accessory = accessory;
24
+ this.light = accessory.context.light;
25
+ const existingState = accessory.context.state;
26
+ this.state = {
27
+ count: existingState?.count ?? 0,
28
+ on: existingState?.on ?? false,
29
+ };
30
+ this.accessory.context.state = this.state;
31
+ this.setupAccessoryInformation();
32
+ this.setupLight();
33
+ this.setupSwitches();
34
+ }
35
+ setupAccessoryInformation() {
36
+ const { Service, Characteristic } = this.platform;
37
+ const switchLabel = this.light.switchCount === 1 ? 'Switch' : 'Switches';
38
+ this.accessory.getService(Service.AccessoryInformation)
39
+ .setCharacteristic(Characteristic.Manufacturer, 'Cycle Light Switches')
40
+ .setCharacteristic(Characteristic.Model, `On/Off Light + ${this.light.switchCount} ${switchLabel}`)
41
+ .setCharacteristic(Characteristic.SerialNumber, this.light.name);
42
+ }
43
+ /** Sets up the on/off-only "light" (HAP Lightbulb service exposing only the On characteristic). */
44
+ setupLight() {
45
+ const { Service, Characteristic } = this.platform;
46
+ this.lightService = this.accessory.getService(Service.Lightbulb)
47
+ || this.accessory.addService(Service.Lightbulb);
48
+ this.lightService.setCharacteristic(Characteristic.Name, this.light.name);
49
+ this.lightService.getCharacteristic(Characteristic.On)
50
+ .onGet(() => this.state.on)
51
+ .onSet(this.handleSetOn.bind(this));
52
+ // Reflect the restored on/off value immediately after a restart.
53
+ this.lightService.updateCharacteristic(Characteristic.On, this.state.on);
54
+ }
55
+ /**
56
+ * Sets up `switchCount` stateless, single-press-only switches (HAP
57
+ * StatelessProgrammableSwitch, restricted to the SINGLE_PRESS event — HomeKit's closest
58
+ * equivalent to a Matter "Generic Switch" configured for single press).
59
+ *
60
+ * When more than one switch is configured, they're grouped under a Service Label, which
61
+ * is the standard HomeKit pattern for exposing several buttons on one accessory.
62
+ */
63
+ setupSwitches() {
64
+ const { Service, Characteristic } = this.platform;
65
+ const count = Math.max(1, this.light.switchCount);
66
+ let labelService;
67
+ if (count > 1) {
68
+ labelService = this.accessory.getService(Service.ServiceLabel)
69
+ || this.accessory.addService(Service.ServiceLabel);
70
+ labelService.setCharacteristic(Characteristic.ServiceLabelNamespace, Characteristic.ServiceLabelNamespace.ARABIC_NUMERALS);
71
+ }
72
+ else {
73
+ // Only one switch: no grouping needed. Remove a label left over from a config change.
74
+ const existingLabel = this.accessory.getService(Service.ServiceLabel);
75
+ if (existingLabel) {
76
+ this.accessory.removeService(existingLabel);
77
+ }
78
+ }
79
+ this.switchServices = [];
80
+ for (let i = 1; i <= count; i++) {
81
+ const subtype = `${SWITCH_SUBTYPE_PREFIX}${i}`;
82
+ const name = this.light.switchNames?.[i - 1] || `${this.light.name} Switch ${i}`;
83
+ const service = this.accessory.getServiceById(Service.StatelessProgrammableSwitch, subtype)
84
+ || this.accessory.addService(Service.StatelessProgrammableSwitch, name, subtype);
85
+ service.setCharacteristic(Characteristic.Name, name);
86
+ service.setCharacteristic(Characteristic.ServiceLabelIndex, i);
87
+ // Restrict to single-press only, since that's all this plugin ever fires.
88
+ service.getCharacteristic(Characteristic.ProgrammableSwitchEvent)
89
+ .setProps({ validValues: [Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS] });
90
+ labelService?.addLinkedService(service);
91
+ this.switchServices.push(service);
92
+ }
93
+ // Clean up leftover switch services from a previously larger switchCount.
94
+ for (const service of this.accessory.services) {
95
+ if (service.UUID !== Service.StatelessProgrammableSwitch.UUID) {
96
+ continue;
97
+ }
98
+ const index = Number((service.subtype ?? '').replace(SWITCH_SUBTYPE_PREFIX, ''));
99
+ if (!Number.isFinite(index) || index < 1 || index > count) {
100
+ this.accessory.removeService(service);
101
+ }
102
+ }
103
+ }
104
+ /**
105
+ * Handles "set" requests for the light's On characteristic — from the Home app, Siri,
106
+ * or an automation. Turning the light on (from any starting state) advances the
107
+ * rotation and fires exactly one switch; turning it off never fires a switch, and only
108
+ * resets the rotation if `resetCountOnOff` is enabled.
109
+ *
110
+ * If `turnOffOnCycleComplete` is enabled and the switch that just fired was the last one
111
+ * in the rotation, the light is immediately switched back off on its own (as if a
112
+ * momentary trigger), applying the same `resetCountOnOff` behavior that a user-initiated
113
+ * off would.
114
+ */
115
+ async handleSetOn(value) {
116
+ const turningOn = value === true;
117
+ if (turningOn) {
118
+ this.state.count += 1;
119
+ const switchIndex = (this.state.count - 1) % this.switchServices.length;
120
+ const targetSwitch = this.switchServices[switchIndex];
121
+ const isLastInCycle = switchIndex === this.switchServices.length - 1;
122
+ this.platform.log.info(`${this.light.name}: turned on (activation #${this.state.count}) → firing switch ${switchIndex + 1} of ${this.switchServices.length}`);
123
+ targetSwitch.updateCharacteristic(this.platform.Characteristic.ProgrammableSwitchEvent, this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS);
124
+ this.state.on = true;
125
+ if (isLastInCycle && this.light.turnOffOnCycleComplete) {
126
+ this.platform.log.info(`${this.light.name}: cycle complete → turning light back off`);
127
+ this.state.on = false;
128
+ if (this.light.resetCountOnOff) {
129
+ this.state.count = 0;
130
+ }
131
+ // Push the auto-off to HomeKit. This does not re-enter handleSetOn/onSet — it only
132
+ // notifies controllers, the same as any accessory reporting its own state change.
133
+ this.lightService.updateCharacteristic(this.platform.Characteristic.On, false);
134
+ }
135
+ }
136
+ else {
137
+ this.platform.log.debug(`${this.light.name}: turned off`);
138
+ this.state.on = false;
139
+ if (this.light.resetCountOnOff) {
140
+ this.state.count = 0;
141
+ }
142
+ }
143
+ this.persistState();
144
+ }
145
+ persistState() {
146
+ this.accessory.context.state = this.state;
147
+ this.platform.api.updatePlatformAccessories([this.accessory]);
148
+ }
149
+ }
150
+ //# sourceMappingURL=virtualLightAccessory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"virtualLightAccessory.js","sourceRoot":"","sources":["../src/virtualLightAccessory.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "homebridge-cycle-light-switches",
3
+ "displayName": "Cycle Light Switches",
4
+ "type": "module",
5
+ "version": "1.0.4",
6
+ "description": "Homebridge plugin that adds configurable virtual on/off lights, each paired with a configurable number of virtual single-press switches that fire in a round-robin sequence every time the light is turned on.",
7
+ "author": "Anders Ljungh",
8
+ "license": "Apache-2.0",
9
+ "homepage": "https://github.com/USERNAME/homebridge-cycle-light-switches#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/USERNAME/homebridge-cycle-light-switches.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/USERNAME/homebridge-cycle-light-switches/issues"
16
+ },
17
+ "keywords": [
18
+ "homebridge-plugin",
19
+ "supports-hap"
20
+ ],
21
+ "main": "dist/index.js",
22
+ "engines": {
23
+ "node": "^18.20.4 || ^20.16.0 || ^22.0.0 || ^24.0.0 || ^26.0.0",
24
+ "homebridge": "^1.8.0 || ^2.0.0"
25
+ },
26
+ "scripts": {
27
+ "build": "rimraf ./dist && tsc",
28
+ "lint": "eslint . --max-warnings=0",
29
+ "prepublishOnly": "npm run lint && npm run build",
30
+ "watch": "npm run build && npm link && nodemon"
31
+ },
32
+ "devDependencies": {
33
+ "@eslint/js": "^9.9.0",
34
+ "@types/node": "^22.5.0",
35
+ "eslint": "^9.9.0",
36
+ "homebridge": "^1.8.0",
37
+ "nodemon": "^3.1.4",
38
+ "rimraf": "^6.0.1",
39
+ "ts-node": "^10.9.2",
40
+ "typescript": "^5.5.4",
41
+ "typescript-eslint": "^8.2.0"
42
+ }
43
+ }