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 +156 -0
- package/config.schema.json +63 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +44 -0
- package/dist/platform.js +134 -0
- package/dist/platform.js.map +1 -0
- package/dist/settings.d.ts +8 -0
- package/dist/settings.js +9 -0
- package/dist/settings.js.map +1 -0
- package/dist/virtualLightAccessory.d.ts +48 -0
- package/dist/virtualLightAccessory.js +150 -0
- package/dist/virtualLightAccessory.js.map +1 -0
- package/package.json +43 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
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
|
+
}
|
package/dist/platform.js
ADDED
|
@@ -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":"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"}
|
|
@@ -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";
|
package/dist/settings.js
ADDED
|
@@ -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":"AAgBA,MAAM,qBAAqB,GAAG,SAAS,CAAC;AAExC;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,qBAAqB;IAQb;IACA;IARF,KAAK,CAAc;IACnB,KAAK,CAAiB;IAE/B,YAAY,CAAW;IACvB,cAAc,GAAc,EAAE,CAAC;IAEvC,YACmB,QAAoC,EACpC,SAA4B;QAD5B,aAAQ,GAAR,QAAQ,CAA4B;QACpC,cAAS,GAAT,SAAS,CAAmB;QAE7C,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,KAAoB,CAAC;QAEpD,MAAM,aAAa,GAAG,SAAS,CAAC,OAAO,CAAC,KAA4C,CAAC;QACrF,IAAI,CAAC,KAAK,GAAG;YACX,KAAK,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;YAChC,EAAE,EAAE,aAAa,EAAE,EAAE,IAAI,KAAK;SAC/B,CAAC;QACF,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QAE1C,IAAI,CAAC,yBAAyB,EAAE,CAAC;QACjC,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC;IAEO,yBAAyB;QAC/B,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;QAClD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC;QAEzE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,oBAAoB,CAAE;aACrD,iBAAiB,CAAC,cAAc,CAAC,YAAY,EAAE,sBAAsB,CAAC;aACtE,iBAAiB,CAAC,cAAc,CAAC,KAAK,EAAE,kBAAkB,IAAI,CAAC,KAAK,CAAC,WAAW,IAAI,WAAW,EAAE,CAAC;aAClG,iBAAiB,CAAC,cAAc,CAAC,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACrE,CAAC;IAED,mGAAmG;IAC3F,UAAU;QAChB,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;QAElD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC;eAC3D,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAElD,IAAI,CAAC,YAAY,CAAC,iBAAiB,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAE1E,IAAI,CAAC,YAAY,CAAC,iBAAiB,CAAC,cAAc,CAAC,EAAE,CAAC;aACnD,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;aAC1B,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAEtC,iEAAiE;QACjE,IAAI,CAAC,YAAY,CAAC,oBAAoB,CAAC,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED;;;;;;;OAOG;IACK,aAAa;QACnB,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;QAClD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAElD,IAAI,YAAiC,CAAC;QACtC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC;mBACzD,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;YACrD,YAAY,CAAC,iBAAiB,CAC5B,cAAc,CAAC,qBAAqB,EACpC,cAAc,CAAC,qBAAqB,CAAC,eAAe,CACrD,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,sFAAsF;YACtF,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;YACtE,IAAI,aAAa,EAAE,CAAC;gBAClB,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;YAC9C,CAAC;QACH,CAAC;QAED,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC;QAEzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAChC,MAAM,OAAO,GAAG,GAAG,qBAAqB,GAAG,CAAC,EAAE,CAAC;YAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC,EAAE,CAAC;YAEjF,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,OAAO,CAAC,2BAA2B,EAAE,OAAO,CAAC;mBACtF,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,2BAA2B,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;YAEnF,OAAO,CAAC,iBAAiB,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACrD,OAAO,CAAC,iBAAiB,CAAC,cAAc,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC;YAE/D,0EAA0E;YAC1E,OAAO,CAAC,iBAAiB,CAAC,cAAc,CAAC,uBAAuB,CAAC;iBAC9D,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC,cAAc,CAAC,uBAAuB,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;YAEpF,YAAY,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;YAExC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpC,CAAC;QAED,0EAA0E;QAC1E,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;YAC9C,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,2BAA2B,CAAC,IAAI,EAAE,CAAC;gBAC9D,SAAS;YACX,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAC,CAAC;YACjF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,KAAK,EAAE,CAAC;gBAC1D,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YACxC,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;;;;;;;OAUG;IACK,KAAK,CAAC,WAAW,CAAC,KAA0B;QAClD,MAAM,SAAS,GAAG,KAAK,KAAK,IAAI,CAAC;QAEjC,IAAI,SAAS,EAAE,CAAC;YACd,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;YACtB,MAAM,WAAW,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC;YACxE,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;YACtD,MAAM,aAAa,GAAG,WAAW,KAAK,IAAI,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC;YAErE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CACpB,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,4BAA4B,IAAI,CAAC,KAAK,CAAC,KAAK,qBAAqB,WAAW,GAAG,CAAC,OAAO,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CACtI,CAAC;YAEF,YAAY,CAAC,oBAAoB,CAC/B,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,uBAAuB,EACpD,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,uBAAuB,CAAC,YAAY,CAClE,CAAC;YAEF,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC;YAErB,IAAI,aAAa,IAAI,IAAI,CAAC,KAAK,CAAC,sBAAsB,EAAE,CAAC;gBACvD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,2CAA2C,CAAC,CAAC;gBACtF,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,CAAC;gBAEtB,IAAI,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,CAAC;oBAC/B,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;gBACvB,CAAC;gBAED,mFAAmF;gBACnF,kFAAkF;gBAClF,IAAI,CAAC,YAAY,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YACjF,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC,CAAC;YAC1D,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,CAAC;YAEtB,IAAI,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,CAAC;gBAC/B,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;QAED,IAAI,CAAC,YAAY,EAAE,CAAC;IACtB,CAAC;IAEO,YAAY;QAClB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QAC1C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IAChE,CAAC;CACF"}
|
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
|
+
}
|