homebridge-mq77-customizer
v2.1.1
Published
Expose MQTT devices to HomeKit and link them together, configured from a web interface instead of a config form.
Maintainers
Readme
MQ77 Customizer 2.1.1
This Homebridge plugin has been 100% vibe coded using Claude Code.
A Homebridge plugin that exposes MQTT devices to HomeKit and links them together, configured from a web interface instead of a config form. Devices and their functions are discovered from the broker, so nothing has to be typed out by hand.
Requirements
- Node.js 20.19, 22.12 or 24 and up
- Homebridge 1.8 or up
- An MQTT broker, for example Mosquitto
- Zigbee2MQTT, for the Zigbee2MQTT source
Installation
Install it the way you install any Homebridge plugin, from the Plugins screen of the Homebridge UI or from the command line:
hb-service add homebridge-mq77-customizerSettings
Everything is set in the plugin's settings form in the Homebridge UI, and each field says what it is for. There are three things to fill in:
- MQTT Broker, where your broker is and what it wants to be called
- Sources, one per publisher on that broker. A Zigbee2MQTT source needs its base topic,
zigbee2mqttunless you changed it. With no sources at all, that is what is assumed - Web Interface, a port and a password. The interface can switch your devices, so it refuses to start without one
Two more are optional: Location, for rules that follow the sun, and ntfy notifications, a topic on ntfy.sh for rules that send a message to your phone.
Everything else, which devices reach HomeKit and what they do between them, is set in the web interface itself.
Sources
Two kinds. Zigbee2MQTT needs nothing but its base topic: it describes its own devices, so they arrive complete. Flat JSON topics reads any publisher that puts flat JSON on a topic per device, working the functions out from the keys it sees. Those accumulate across messages, so a partial update carrying one key does not redefine the device.
Recognised keys are state, level, speed, swing, temperature, humidity and co2_levels. Anything else still becomes a function, typed from its value, and stays available to the rules engine.
Rules only keeps a source's devices out of HomeKit while leaving them usable as rule triggers and targets. Set it where another plugin already publishes those devices, so they can't appear twice.
Topic filter narrows what a flat JSON source listens to, as an MQTT filter, + standing for one level and # for the rest. Everything under the base topic by default.
Command topic suffix is what is added to a device's topic to write to it, usually set. Without one the source is read only.
Describing a device
A flat JSON topic carries no schema, so a function is only known once it has turned up in a payload. A fan that reports nothing but state until somebody changes its speed has no speed to tick. Name the missing functions under Described devices and they are there from the moment the device first reports.
A device is named by the part of its topic after the base topic: one publishing on <base topic>/kitchen_fan is written as kitchen_fan, though the whole topic is accepted too. Only the recognised keys above can be named, since a name on its own says nothing about the kind of value it carries. A function the device does report is left exactly as reported.
What was described is logged at startup, so a topic that matches no device is visible rather than silent.
Usage
Open http://<your-homebridge-host>:8888, or whichever port you set, and sign in with the password from the settings.
Keep it on your own network. Anybody who gets past that one password can switch every device the plugin publishes, so the port does not belong on the open internet or behind a port forward. Reach it from outside through a VPN, the way you would reach Homebridge itself.
Every device found on the broker is listed with all of its functions, grouped into functions, settings and diagnostics and listed by name within each. Tick a function to publish it to HomeKit. Changes take effect immediately, with no Homebridge restart.
Per device you can also:
- choose the HomeKit tile: Switch, Outlet, Lightbulb or Fan
- publish each endpoint as its own accessory, for multi channel switches
- give it a name, a room and a kind, which this interface uses to label and group it
Functions with no HomeKit equivalent are still listed and marked, and stay available to the rules engine rather than being hidden.
Accessory names are corrected to what HomeKit accepts, which must start and end with a letter or number. A name in brackets, Hall light (front door), is published as Hall light front door. The name shown here is left as you wrote it.
What reaches HomeKit
| Function | Becomes |
| --- | --- |
| on/off | Switch, Outlet, Lightbulb or Fan, your choice |
| brightness | Brightness on a Lightbulb |
| temperature, humidity | their sensor services |
| contact | a Contact sensor, closed while the two halves touch |
| smoke | a Smoke sensor. Heat alarms report smoke too, so they become one as well |
| occupancy | a Motion sensor by default, or an Occupancy sensor if you pick that under HomeKit sensor. A PIR and an mmWave sensor both report occupancy, so which one a device is has to be picked by hand |
| presence | an Occupancy sensor, which stays on while somebody sits still |
| battery | a battery reading on the accessory, with a low warning |
| battery low | the low warning alone, for a device that raises the flag with no percentage |
| tamper | shown as tampered on the device's sensors |
| child lock | the physical controls lock on the tile |
| climate | a Thermostat, using the temperature range the device declares |
| speed, swing | a Fan with rotation speed and swing |
| button actions | one button per physical button, mapped to single, double and long press |
Button names and gestures are worked out from the action names the device publishes, so a double rocker becomes three buttons without anything being typed out. Gestures HomeKit has no equivalent for, such as triple press, stay available to the rules engine.
Naming and grouping
Every device takes a name, a room and a kind, set in its panel. They are for this interface: HomeKit keeps rooms in the Home app, where no accessory can set or read them.
Marking a device as a Controller puts its button presses in the Activity tab, with their own filter. Only marked devices: every remote in the house reporting in would bury the rules.
The name reaches HomeKit only where the source names nothing itself, which is the flat JSON publishers. Zigbee2MQTT owns its own names, so one set here stays in this interface. Renaming never changes an accessory's identity, so nothing is lost in the Home app either way.
Last seen and retainment
A device card says when the device was last heard, and its panel says the timestamp behind that along with whether the broker keeps its messages.
Both come from the source rather than from this plugin. When a message reached the broker is not an answer to when a device spoke: a retained message is replayed the moment the plugin connects, which would have every retaining device looking as though it had just reported. Zigbee2MQTT publishes the real answer once advanced.last_seen is set to something other than disable, and a flat JSON publisher that puts a last_seen in its payload is taken at its word the same way. A device that publishes no such time shows none, and sorting by Last seen puts those last.
Whether messages are retained is read from Zigbee2MQTT's own configuration, where three things decide it: the device's own retain, the device_options defaults it otherwise inherits, and mqtt.force_disable_retain, which overrides both. A source that keeps no such configuration says nothing rather than guessing.
Controllers
The Controllers tab lists every device marked as a Controller, one table per remote, saying what each of its buttons sets off. A button no rule answers reads as none, or In HomeKit where the press reaches HomeKit as well, said under any rules it does answer. The Unused buttons and HomeKit buttons ticks in the header say which of those lines are wanted, and a button neither tick keeps is left out. Download writes the whole overview to controller-config.md, which follows the tick: hidden buttons are left out of the file too.
Map
The Map tab draws the Zigbee network: what reaches the hub directly and what reaches it through something else. Every link found is drawn, and the route each device uses back to the hub is picked out.
A scan questions every device in turn, so it takes minutes on a mesh of any size and only runs when asked for. Zigbee2MQTT only: a flat JSON source has no network to describe.
Activity
The Activity tab lists what the rules have been doing, newest first, including the ones that decided not to run and why. Every line says what set the rule off, in the same shape whatever it was: the device and the button pressed on it, the device and the reading that moved, or the time of day. A press and the rule it set off read as one line, and each entry says which kind of rule it was. Presses that set nothing off have a filter of their own, off by default.
Rules
Rules live in three tabs: Automation, Mirror devices and Sliders. Automation is the general one: when something happens on one device, send something to another, straight away or after a wait. All three work across sources, so a Zigbee button can drive an infrared blaster, and apply the moment they are saved.
Across all four:
- anything readable can set a rule off, including functions that never reach HomeKit, and anything writable can be acted on
- an action can copy whatever set the rule off, restated in the target's own terms, so a switch that says
ONcan drive one that expectstrueand a dimmer counting to 254 can drive one counting to 100 - an action on a number can increase or decrease it instead of setting it, so a button can say half a degree warmer without the rule knowing what warm is at the time. The value is read when the action is sent, so a delayed one counts from what it finds then, and nothing is trimmed to the device's own range: a thermostat already at its highest stays where it is
- a rule with several outcomes runs the first whose condition holds and skips the rest
- picking what sets a rule off marks any value another rule already uses with a
*, since two rules on one button press is a mistake nobody sees until both of them run - rules never run on retained messages, so reconnecting to the broker cannot replay yesterday's button press
- a rule will not run more often than its rate limit, one second by default, and one that runs more than twenty times in ten seconds is switched off and logged on the assumption it is setting itself off
An automation can wait between the trigger and everything else: Optional wait time (mm:ss), empty on most rules. A light coming on, thirty seconds, the light going out again.
The conditions are asked when the wait runs out rather than when the trigger fired, so "in ten minutes, unless somebody is home by then" asks about ten minutes from now. The wait is called off the moment what started it stops being true: told to run when a light came on, it stops caring once the light is off, however that happened. The same thing happening again starts the wait from the top, a different trigger of the same rule takes it over, and a rule's own doing is not read as either. A wait running when Homebridge restarts is forgotten.
An action's own delay is a different thing: it comes after the conditions and cannot be called off. Timers were a tab of their own until the wait arrived; anything one of those did is an automation with a wait now.
An automation can be run by hand with the Trigger button beside Save, whether or not it is switched on. Only what has been saved can be run. A rule with a wait acts at once when it is run that way, since trying a rule while building it is not sitting through its wait.
Times
An automation can be set off by a time of day rather than by a device. Current time sits at the bottom of the trigger picker, under the devices, and a rule set off that way carries a clock where a device carries its kind, in the rule list and in the activity log. The same pick is offered among the conditions, where it asks what the clock says now.
Only automations. A mirror and a slider are driven by their devices.
The clock is whatever the machine running Homebridge thinks it is, and a rule fires during the minute it names. Four things worth knowing:
- A time that passed while Homebridge was down does not fire late. A reboot at 22:59 swallows a rule set for 23:00, which is the price of never having an evening's worth of rules run at once when the machine comes back.
- A condition asks about now, so it takes no days of its own. A rule that should only run on weekdays says so where it is set off.
- On the day the clocks go forward, a rule set for a time in the missing hour does not fire, since that time never happens. On the day they go back, a rule set for the repeated hour fires once.
- Days of the week can narrow when it runs. All seven, or none ticked, both mean every day.
As a condition it is said as a side rather than a range: is before 04:00, or is after sunset -30. Both include the minute they name and run to that end of the day, so before four o'clock means midnight through four. A night is the two of them in one or group: after 22:00 or before 06:00.
A time can also be one the sun decides: sunrise, sunset, dawn or dusk, with an offset in minutes either side, so sunset -30 is half an hour before the sun goes down. Dawn and dusk are civil twilight, which is when it is actually dark rather than the moment the sun crosses the horizon, and for lights that is usually what is meant.
These need a location, set under Location in the Homebridge settings. Without one they are not offered at all, and a rule that already uses one keeps saying so rather than being quietly rewritten: it fails once a day and says in the activity log that the location is missing. Filling the coordinates back in makes it work again with nothing re-edited.
Far enough north there are days when the sun never reaches one of these points, dawn and dusk long before sunrise and sunset. Those days are skipped and said in the log.
Notifications
With a topic set under ntfy notifications, an action can send a message instead of setting a device: Send notification, at the bottom of the action picker. It takes a title and a message, and both may name what set the rule off:
<trigger>the device, room first, or the time when the clock did it<property>the function on it that moved<value>what it said, with its unit<rule>the rule's own name
So <rule>: <trigger> <property> is <value> arrives as Brand: Gang Rookmelder Smoke is true. Subscribe to the same topic in the ntfy app to receive them. Anyone who knows the topic can read it, so pick one nobody would guess.
With a topic set, a device with a battery also gets Low battery warning in its panel, on unless unticked. A battery is low when the device says so (battery_low or low_battery) or when it drops below 10%, and the warning comes again at 5% and at 1%. It says <device> is low on battery, with the percentage where the device counts it. A restart with a battery still low sends the warning once more.
Whether or not a topic is set, a low battery shows as a red, empty battery at the start of the device's line in the Devices list.
Mirror devices
Which devices, and which of their functions, should stay in step. Every member is both a trigger and a target. Functions are matched on meaning rather than on name, so a socket calling its on/off state mirrors a two channel switch calling the same thing state_l1.
After a write the group is left to settle, one and a half seconds by default and set per rule between 0.25 and 60 seconds. A device reporting its old state once more is indistinguishable from somebody flipping a switch, so without the pause a group that disagrees would send itself back and forth for ever. The cost is that flipping a mirrored device again inside the window is ignored.
Sliders
A dimmer driven from buttons. One press moves one step. Down from the first step switches the device off rather than leaving a light at zero brightness and still on, and either stepping button switches it on when it is off.
Coming on from off lands where the device says it should, which Zigbee2MQTT keeps as level_config.on_level. "On at" sets it for a device that has no such setting, and without either it comes on at the first step.
Cycle is one button for the whole range: up to the top, back down to off, and up again from there. It starts upward whenever the level was last set by something other than the slider, and ignores a second press within a second, since it is a button to press rather than to hold.
Each button takes several triggers, so one slider can be driven by more than one remote. Stepping counts from what the slider last sent for a couple of seconds rather than from what the device last reported, so a held button that sends faster than the light can answer still climbs.
Backup settings
Everything set here lives in state.json under the Homebridge storage path, alongside a backups folder holding the last ten dated copies. One is taken when the plugin starts, before anything is touched, and at most once an hour after that.
The footer offers back up: download / upload, along with when the last copy was taken and a way to take one now. Download hands you the lot as a file, which is the only copy that survives losing the machine it runs on. Upload takes one back, after copying what it replaces. The session secret is left out of the download and kept on upload, so a settings file is safe to keep somewhere else and putting one back does not sign you out.
A run that starts with nothing will not write over a file that has something in it. Somebody deleting their last rule is entitled to an empty file, but a run that began empty and is about to stamp on one that is not has misread something, and the file is worth more than the write. It says so in the log and in the interface rather than carrying on.
