homebridge-cycle-light-switches
v1.1.0
Published
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.
Maintainers
Readme
homebridge-cycle-light-switches
A Homebridge plugin that adds any number of virtual on/off lights. Every light is paired with a configurable number of virtual, single-press switches. Each time the virtual light is turned on — from the Home app, Siri, or an automation — the plugin fires the next switch in a round-robin rotation. Point HomeKit automations at each switch's "Single Press" trigger to chain further actions off a light being turned on, cycling through a different action every time.
Built from the official Homebridge plugin template, using the Homebridge Plugin API.
Why?
The basic problem to solve is using a trigger (e.g. a wall switch) to trigger different actions (e.g. different light scenes/scenarios) in sequence. The first time you hit the a switch you might want to set a mood, the second time blaze all lights max, and finally turn everything off. And the following time start the cycle again.
I used to achieve this by nested if/else in Shortcuts in Apple Home, but after switching to Matter over Thread, the delay of seconds for running a shortcut became very apparent, and annoying. So this plugin runs the logic without having to slow everything down with Shortcuts.
How?
This plugin implements these HAP services:
Light → a HAP
Lightbulbservice exposing only theOncharacteristic (no brightness/color) — an on/off-only light.Switch → by default, a HAP
StatelessProgrammableSwitchservice, restricted to only ever report aSINGLE_PRESSevent — HomeKit's stateless "button" accessory type, which is what Home/Homebridge calls a Generic Switch, configured for single press only. When a light has more than one switch, they're grouped under aServiceLabelservice, which is the standard HomeKit pattern for exposing several buttons on one accessory. These switches are read-only: you can react to them, but nothing can trigger one directly, since a stateless switch has no "on" state to set.Stateful switch (
statefulSwitches: true) → an ordinary HAPSwitchservice instead — the same service type used for a plain HomeKit switch accessory. Because it's a regular settable on/off characteristic, a scene, automation, or a tap in the Home app can turn a specific switch on directly, jumping the rotation to it. This is how you let something other than "turn the light on" pick which step comes next. Exactly one switch per light is ever on — whichever one the rotation is currently sitting on — and it stays on until the rotation moves elsewhere, rather than pulsing back off a moment later. That matters for scenes in particular: a Home scene can only capture and re-apply explicit target states, so a switch that reverted itself a moment later made the scene that had just set it look "off" again, even though the jump it triggered had already worked — staying on keeps the scene's own displayed state honest too. As a bonus, each one is also individually renameable in the Home app, unlike the grouped "Button 1, Button 2, ..." stateless switches (which Home hardcodes those labels for, regardless of the name configured here).Stateful switches live on their own standalone accessory each — one accessory per switch, not shared with the light or with each other. That's deliberate, for two reasons. First, Home collapses any accessory exposing more than one controllable service into a secondary picker screen instead of a direct-tap tile, so the switches can't share the light's own accessory without dragging the light into that screen too. Second — and this is the part that isn't obvious until you hit it — a
Switchservice has no naming characteristic beyond its plainName. If several switches shared one accessory instead (as an earlier version of this plugin did), Home's automation picker falls back to labelling every one of them with that shared accessory's name, making them indistinguishable when you go to pick "which switch" for an action. A standalone accessory per switch has no such ambiguity: the accessory's name is the switch's name, everywhere in Home. The tradeoff is one extra paired accessory per switch, which does clutter up the room view. If that bothers you, each switch accessory can be individually hidden from Home's main screen — its own accessory settings have a "Add to Home View" toggle — without affecting its availability to automations or Siri; you just won't see a tile for it the Home View. An option to also keep them out of room views: assign all of a light's switches to a dedicated room (e.g. "Hidden" or "Automation") that has no other accessories shown in the Home View — a room with nothing visible in it doesn't get listed as a room there at all, so the whole room (and everything in it) disappears from the tab bar rather than needing each accessory hidden individually. They're still fully there for automations either way. Stateless switches don't have either problem: aStatelessProgrammableSwitchnever renders as a controllable tile in the first place, so it's harmless to leave several of them nested inside the light's own accessory.
Installation
npm install
npm run buildThen either:
npm linkand add it to a local Homebridge instance'snode_modules, or- publish it to npm and install it normally (
npm install -g homebridge-cycle-light-switches).
Configuration
Add a platform block to Homebridge's config.json (or use the plugin's settings UI,
which reads config.schema.json):
{
"platforms": [
{
"platform": "CycleLightSwitches",
"name": "Cycle Light Switches",
"lights": [
{
"name": "Kitchen Scene",
"switchCount": 3,
"resetCountOnOff": false
},
{
"name": "Spotlights",
"switchCount": 3,
"resetCountOnOff": true,
"turnOffOnCycleComplete": true
},
{
"name": "Front Door Chime",
"switchCount": 2,
"resetCountOnOff": true,
"switchNames": ["Front Door Chime First", "Front Door Chime Second"]
},
{
"name": "Evening Routine",
"switchCount": 3,
"resetCountOnOff": true,
"statefulSwitches": true
}
]
}
]
}Options (per light)
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| 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. |
| switchCount | integer | 1 | How many switches accompany this light. |
| resetCountOnOff | boolean | false | See below. |
| turnOffOnCycleComplete | boolean | false | See below. |
| statefulSwitches | boolean | false | See below. |
| switchNames | string[] | — | Optional custom names for each switch, in order. Falls back to "<Light Name> Switch <n>" (stateless) or "<Light Name> Cycle <n>" (statefulSwitches: true) — "Switch" is avoided there since these are settable HomeKit switches, easily confused with a real wall switch for the same light. |
Behavior
Turning the light on — regardless of whether it was already on (e.g. an automation
re-sends "turn on" while it's already on) — advances an internal counter and fires
exactly one switch: the counter modulo switchCount. Turning the light off never
fires a switch.
resetCountOnOff: false (default) — the rotation never resets. With 3 switches,
turning the light on four times in a row fires switches 1, 2, 3, 1, in that order,
no matter how many times the light was turned off in between.
resetCountOnOff: true — turning the light off resets the rotation back to the
start. With 3 switches, turning on twice fires 1, 2; turning the light off; turning
it on twice more fires 1, 2 again — i.e. the full sequence is 1, 2, (off), 1, 2.
turnOffOnCycleComplete: true — as soon as the switch that completes a full
rotation fires (e.g. switch 3 of 3), the plugin turns the virtual light off on
its own, immediately, as if it were a momentary trigger rather than something you
leave on. This is useful to combine an automation, e.g. a physical switch trigger
to cycle scenes, with being able to control the lights with buttons in the
Home app (see Advanced cycle below). This combines with resetCountOnOff: if
both are enabled, both manual and automatic off resets the rotation, so the next
"on" whether from trigger or button press, always starts the cycle from switch 1.
statefulSwitches: true — switches become settable, so something other than the
light itself can pick which step fires next, and each one moves to its own standalone
accessory, named for that switch specifically (e.g. "Kitchen Scene Cycle 2") — see
"How?" above for why they can't share an accessory. They default to "Cycle" rather than
"Switch" in their name, since a settable, named Switch accessory reads too much like
an actual wall switch for the same light — "Kitchen Scene Switch 2" next to a real
"Kitchen Switch" invites mix-ups that "Cycle" avoids (override it per switch with
switchNames if you'd rather call them something else). Turning a specific switch on
directly (from a scene, an automation action, or a tap in the Home app) jumps the
rotation to it — regardless of what the rotation was doing before — so the next time
the light turns on, it fires the switch that follows the one you triggered. Triggering
a switch directly also turns the light itself on, if it wasn't already — the same as
if the rotation had reached that step on its own — so a scene that jumps to a step
also brings the light along with it, rather than leaving it looking off while a
switch quietly claims to be "active". For example, with 3 switches, triggering Cycle 1
directly turns the light on and fires nothing further by itself; turning the light on
again afterward is what then fires Cycle 2, even if the rotation had already moved
past Cycle 1 long ago. Whether
a switch fired because the light turned on or because it was triggered directly, it
stays on — and every other switch for that light turns off — until the rotation moves
on again, so whichever switch is "on" at any moment tells you exactly which step the
rotation is sitting on. If resetCountOnOff also fires (the light turns off with that
enabled), every switch turns off too, since no step is "current" until the next "on".
The reverse also holds: turning off the switch currently on — directly, the same way
you'd turn it on — turns the light off too, since that's the one saying "there's an
active step"; turning off any other (already-off) switch does nothing, since it was
never the one representing the current step. This also honors resetCountOnOff the
same way a direct light-off would.
Toggling statefulSwitches for a light you've already set up in Home doesn't lose the
light's own history: the light keeps its accessory (and its rotation position) either
way, and only the switches move in or out of their own standalone accessories.
Other than a reset, the rotation position is kept indefinitely: it's stored in
Homebridge's accessory cache on disk, so it survives Homebridge restarts. It only
resets to zero if you explicitly enable resetCountOnOff, or if the accessory is
removed (e.g. by renaming the light or deleting it from the config).
Example automations
Simple cycle
- In the Home app, create an automation (or a button effect) that turns on "Kitchen Scene".
- On "Kitchen Scene" there are Button 1, Button 2, Button 3. On Button 1, run scene A or set lights as required. Repeat for Switch 2 → scene B, Switch 3 → scene C.
- Every time you (or an automation) turn on "Kitchen Scene", it cycles through scenes A, B, C, A, B, C, ...
Advanced on/off scenes
Used for light scenes when the last step is always turn off, and both automation triggers and manual control is used in combination.
- In the Home app, set one or several automation triggers to turn on "Spotlights".
- On "Spotlights" there are Button 1, Button 2, Button 3. Use Button 1 and Button 2 to control lights (or any devices) for two distinct scenes.
- Leave Button 3 empty, instead create an automation that turns off your scene/lights when "Spotlights" is turned off.
Since turnOffOnCycleComplete is true, the scene/lights will turn off anyway on the third
"turn on". They will also turn off when manually turning off "Spotlights" in the app, and
resetCountOnOff prepares for the next automated trigger or manual "turn on" of "Spotlights"
in the app to trigger the first switch (i.e. Button 1).
Jumping to a specific step
Set statefulSwitches: true on a light to let an automation or scene pick which step
runs next, instead of always advancing to "whatever's next". For example, with a
3-switch "Kitchen Scene" light:
- Enable
statefulSwitchesfor "Kitchen Scene". - Create an automation: some condition (e.g. a specific wall switch, time of day, or sensor) sets "Kitchen Scene Cycle 2" to on — it's its own accessory, listed separately from "Kitchen Scene" itself and from the other switches. This also turns "Kitchen Scene" itself on, the same as if the rotation had reached step 2 normally.
- The next time anything turns "Kitchen Scene" on — a different automation, or the Home app — it fires Cycle 3, not whatever the rotation would otherwise have been on. Triggering a switch directly always determines what the following "on" does.
This is useful when you want an external condition to steer the cycle (e.g. "if it's after sunset, the next toggle should jump straight to the night scene") without having to also fire that scene's own action a second time. Since the switch you jump to stays on until the rotation moves past it, the scene that set it reads back as staying on/active too — it won't flip back to "off" moments later the way a momentary trigger would.
Development
npm run watch— build,npm linkthe plugin, and run Homebridge withnodemonfor live reload (requires a local Homebridge config; seehomebridge-plugin-template's docs for setting uptest/hbConfig).npm run lint— lint with ESLint.
