Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
396b18f
feat: generic Zigbee 3.0 device driver
derek-miller Aug 25, 2026
e4eb35a
feat: real-time event binding via zbind
derek-miller Aug 25, 2026
8b9a2be
fix: address generic Zigbee 3.0 driver review findings
derek-miller Aug 25, 2026
c1567bc
fix: address second-round review of the generic Zigbee 3.0 driver
derek-miller Aug 25, 2026
9f57483
fix: close the resolve-path regressions from the PR approval notes
derek-miller Aug 25, 2026
1fa14cc
test: cover Device:resolve directly; stash model on restore too
derek-miller Aug 25, 2026
8621547
chore: apply review-driver conventions pass (code + metadata)
derek-miller Aug 25, 2026
0e959d1
docs: bring the driver docs up to conventions
derek-miller Aug 25, 2026
c113922
docs: regenerate README from the updated device documentation
derek-miller Aug 25, 2026
21637f4
fix: don't clobber Last Action on reload; guard GetDriverFileName
derek-miller Aug 25, 2026
9a573e3
fix: clear Last Action on re-pair; make the seed guard tombstone-aware
derek-miller Aug 25, 2026
691fded
chore: second review-driver pass - docs, connections, conventions
derek-miller Aug 25, 2026
27b27fb
fix: re-seed sensor state vars on re-pair; de-flake test_device_table
derek-miller Aug 25, 2026
6324606
fix: re-seed Last Action on re-pair too (not delete); test reset visi…
derek-miller Aug 25, 2026
ebb845e
fix: heal a stuck hidden Last Action placeholder from pre-fix builds
derek-miller Aug 25, 2026
2d822a6
docs: demo-data fallback + annotated Settings-tab screenshots (D30/D33)
derek-miller Aug 25, 2026
167a92e
fix: heal Last Action by the hidden attribute, not the persist record
derek-miller Aug 25, 2026
5046e65
docs: correct the Last Action comments - reset() reseeds, it doesn't …
derek-miller Aug 25, 2026
171c4f3
docs: scope the Last Action comment - arm 1 seeds fresh installs
derek-miller Aug 25, 2026
6112ed6
fix: expand the #embed directive so binding_keys actually ship
derek-miller Aug 26, 2026
7bf8a38
test: cover the #embed preprocessor directive
derek-miller Aug 26, 2026
2402157
fix: gitignore Python bytecode so CI's dirty-tree check passes
derek-miller Aug 26, 2026
83fc14e
feat: Bindings tab - confirm real-time binds and watch button presses
derek-miller Aug 27, 2026
689acec
fix: make real-time binds land reliably, and self-heal
derek-miller Aug 27, 2026
1c18c9d
feat: fire button hold once, and group the Bindings rows by endpoint
derek-miller Aug 27, 2026
4567f44
fix: don't surface the zigbee2mqtt name in the Settings UI
derek-miller Aug 27, 2026
35e486b
feat: show per-function bind state within each endpoint
derek-miller Aug 27, 2026
eb00e5d
feat: persist binds and reconcile on reload instead of re-binding
derek-miller Aug 27, 2026
a6687a1
feat: generator-driven binds, electrical readings, and writable settings
derek-miller Aug 29, 2026
9aec197
docs: refresh Settings UX, screenshots, and driver docs
derek-miller Aug 29, 2026
c91bc1f
feat: tunable per-reading reporting rate
derek-miller Aug 29, 2026
6e4f414
docs: reporting-rate cog on the Device tab
derek-miller Aug 29, 2026
a5bb6e1
feat: power factor reading, and split AC from battery voltage by unit
derek-miller Aug 30, 2026
fcff923
feat: Aqara FCC0 and Tuya EF00 electrical metering
derek-miller Aug 30, 2026
2e762c3
feat: tune reporting for CO2, PM2.5, and electrical readings
derek-miller Aug 30, 2026
953dec3
docs: regenerate README for the reporting-rate docs
derek-miller Aug 30, 2026
b611e53
fix: reporting-cog correctness (review findings)
derek-miller Aug 30, 2026
79899e2
fix: reject settings values that overflow the attribute width
derek-miller Aug 30, 2026
fc526c2
fix: harden ConfigureReporting encoding, and cover the wire format
derek-miller Aug 30, 2026
2fe9076
fix: reconcile judges binds by the live table, not the persisted set
derek-miller Aug 30, 2026
5ffaf2c
fix: treat Level Step as discrete and self-heal a lost hold Stop
derek-miller Aug 30, 2026
8774ae4
fix: Settings UI honors endpoints, live pushes, and generator names
derek-miller Aug 30, 2026
d5032d8
chore: drop dev-only screenshot scripts and the hardcoded controller IP
derek-miller Aug 30, 2026
d2b122b
docs: simplify the changelog and cover bindings and power factor
derek-miller Aug 30, 2026
81c34de
docs: normalize changelog line wrap to the formatter's output
derek-miller Aug 30, 2026
dd2dedd
fix: let a dozed reconcile resume across wakes instead of latching once
derek-miller Aug 30, 2026
892d732
fix: width-check bitmap settings (map8/16/32) like their unsigned peers
derek-miller Aug 30, 2026
d4d9619
fix: keep the reporting min interval from exceeding the max in the UI
derek-miller Aug 30, 2026
6998ba0
fix: pick the nearest legal interval when clamping, not the first match
derek-miller Aug 30, 2026
e7af4f0
feat: one reading card per channel, with C/F shown together
derek-miller Aug 30, 2026
5432034
fix: clean up the dual-unit reading tile
derek-miller Aug 30, 2026
27b09fd
fix: a partial reconcile must not unbind a target's only binding
derek-miller Aug 30, 2026
7b65a29
fix: decode Xiaomi int8 as signed and 32-bit fields at a fixed width
derek-miller Aug 30, 2026
669a946
fix: bound the reportable-change field to the attribute width
derek-miller Aug 30, 2026
08aeafe
fix: confirm ConfigureReporting per attribute, not by the first byte
derek-miller Aug 30, 2026
a7340b5
fix: bound the config-retry window and sanitize persisted values on load
derek-miller Aug 30, 2026
c694b4a
fix: cover the wide ztypes in the width guard and lengthen the hold t…
derek-miller Aug 30, 2026
baa3bb0
fix: honor a hold action delivered as a repeated Level Step
derek-miller Aug 30, 2026
7e0cac0
refactor: pass the channel via data-* instead of inline JS strings
derek-miller Aug 30, 2026
3ce6196
refactor: drop dead ELEC deadbands and pin the override-persistence path
derek-miller Aug 30, 2026
576fa40
feat: support the same measurement channel on multiple endpoints (run…
derek-miller Aug 30, 2026
7176cd3
feat: emit per-endpoint measurement specs so multi-probe sensors reso…
derek-miller Aug 30, 2026
8e52d3a
feat: multi-endpoint contacts too, so multi-zone motion sensors don't…
derek-miller Aug 30, 2026
9e3f4ad
fix: don't drop unresolvable-endpoint readings, and honor single-endp…
derek-miller Aug 30, 2026
60dfc6b
polish: natural-sort multi-zone readings and note multi-endpoint in t…
derek-miller Aug 30, 2026
008e1c7
fix: route same-endpoint binary sensors so a contact and a motion sen…
derek-miller Aug 30, 2026
69e2491
fix: restore the endpoint-tier contact fallback so no-cluster reports…
derek-miller Aug 30, 2026
47f6b03
fix: restore legacy contact slots before canonical ones so the curren…
derek-miller Aug 30, 2026
3ca68ca
test: pin replay order so [15] deterministically guards the legacy-sl…
derek-miller Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .copier-answers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ _commit: v0.9.16
_src_path: git@github.com:finitelabs/control4-driver-template.git
distributions: drivercentral oss
github_org: finitelabs
primary_color: '#5D3FD3'
primary_color: '#C8102E'
project_description: Integrate generic Zigbee 3.0 devices into Control4 (device definitions
generated from zigbee2mqtt)
project_name: control4-zigbee3
Expand Down
48 changes: 48 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
version: 2

# tools/generator is the only npm manifest in this repo. It builds the device
# database from the zigbee2mqtt definitions and never ships inside a .c4z, so
# these are build-time dependencies only. The root package.json was retired in
# the v0.9.11 template update (docs now render with Python and WeasyPrint), so
# there is nothing left to watch at "/".
#
# Bumping zigbee-herdsman-converters does not update the shipped device tables
# on its own. After merging one of these PRs run `make generate && make build`
# and commit the regenerated drivers/zigbee3_device/generated/* and
# SUPPORTED_DEVICES.md, otherwise the pinned converter version and the
# generated data drift apart.
#
# Both halves are required. `make generate` writes unformatted output, while the
# committed files are post-`make fmt`: stylua wraps devices.lua and mdformat pads
# the SUPPORTED_DEVICES.md tables. Committing raw generator output makes CI fail
# its dirty-tree check, because `make build` runs fmt and rewrites everything.
# It also makes the diff unreadable: raw output reads as roughly 10k insertions
# against 78k deletions, where the real change after formatting is a few
# thousand lines.
#
# CI cannot catch this drift. build.yml runs `make init` and `make build`, never
# `make generate`, so its dirty-tree check only proves the committed files are
# already formatted, not that they match the pinned converters version. A green
# build says nothing about whether this step was run. The local run is the
# verification.
#
# Read the formatted diff before merging, and check more than the channel list.
# A converters release can:
# * re-key a model. DEVICES lookup in src/zigbee3/device.lua is an exact,
# case-sensitive index and Device:restoreModel() re-resolves the persisted
# string on every reload, so a re-key silently drops an already-installed
# device to generic handling on the next driver update.
# * drop a model outright, same effect.
# * drop channels, settings or Tuya datapoint maps from a retained model.
# * drop button wire keys from a retained model, which silently kills any
# Composer programming bound to those actions.
# * narrow a binding key in binding_keys.xml from a ManufacturerName wildcard
# to specific strings, which changes whether Director claims the device at
# all on a fresh pair.
updates:
- package-ecosystem: npm
directory: /tools/generator
schedule:
interval: monthly
commit-message:
prefix: chore
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,12 @@
/.venv
/.lua
**/http-client.private.env.json

# Python bytecode (tools/ scripts and their tests import each other as modules)
__pycache__/
*.pyc

# Local screenshot-generation scripts. They drive the maintainer's review-driver
# skill (shoot_ui.py), which lives outside this repo, so they are kept on disk but
# not tracked here.
/shots/
41 changes: 40 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# <span style="color:#5D3FD3">Changelog</span>
# <span style="color:#C8102E">Changelog</span>

<!--
Template for a new release entry (copy below the heading, fill in, uncomment):
Expand All @@ -19,3 +19,42 @@ Template for a new release entry (copy below the heading, fill in, uncomment):
-->

## Unreleased

Initial release. One generic driver that speaks ZCL directly to any joined
Zigbee 3.0 device: adding a device is a data refresh from the zigbee2mqtt
database rather than new code, so thousands of devices are supported. The driver
resolves the joined model and builds its proxies, readings, and settings
automatically.

### Added

- Sensors: motion, contact, and similar security sensors enroll automatically,
and every reading is published as a Control4 variable for programming. A
device that reports the same reading on more than one endpoint (a multi-probe
sensor, a multi-zone presence sensor) surfaces each one as its own reading,
variable, and reporting control rather than collapsing them into one.
- Energy monitoring: metering plugs and switches publish power, voltage,
current, energy, and power factor, each scaled by the device's own multiplier
and divisor attributes.
- Real-time bindings: reporting and button clusters are bound to the coordinator
so the device pushes changes at once instead of waiting for a poll. Binds
persist across reloads, confirm on a sleepy device's next wake, and are
reconciled against the device's own table. A Bindings tab shows their state,
and a Re-establish Bindings action re-runs them.
- Tunable update rate: a cog on each Device-tab reading sets how often it
reports (minimum and maximum interval, and the change threshold).
- Settings: per-device configuration from a Settings tab, covering standard ZCL
and manufacturer-specific settings (including vendor custom clusters),
delivered reliably to sleepy devices and enforced across reloads.
- Alerts and actions: battery, trouble, and offline/online alerts, plus Identify
and other device actions from the Actions tab.
- Buttons and remotes: wireless remotes and scene controllers fire Control4
programming events, one per button action (e.g. "1 Single", "2 Hold"), decoded
from standard ZCL Scenes, On/Off, and Level commands.
- Companion drivers: a **Zigbee 3 Lock** and a **Zigbee 3 Light** bind to a
joined Zigbee 3 Device to present a deadbolt or lever, or a bulb or fixture,
as a standard Control4 lock or light. Lock, unlock, dimming, color
temperature, and color are enabled from what the device reports.
- Switches and relays: any switch is presented as a Control4 relay, one per gang
over ZCL On/Off, with multi-gang endpoints (l1/l2, left/right) resolved from
the zigbee2mqtt database.
23 changes: 22 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,30 @@ zip: $(VENV_STAMP) ## Zip the c4z/, docs/, and source/ subfolders per distributi
"$$repo.zip" c4z docs source); \
done

# ─── Generate ─────────────────────────────────────────────────────────────────
# Project-specific: regenerate driver device data from the zigbee2mqtt database.
# Runs before `preprocess`; the generated fragments are #embed-ed into the
# hand-authored driver sources. Kept out of `build` so drivers/ stays reviewable
# source, regenerated deliberately when refreshing device support.

GEN := tools/generator

$(GEN)/node_modules: $(GEN)/package.json
cd $(GEN) && npm install
@touch $@

.PHONY: generate
generate: $(GEN)/node_modules ## Regenerate drivers/ device data from the zigbee2mqtt database
cd $(GEN) && node extract-z2m.mjs && node map-c4.mjs && node gen-drivers.mjs && node gen-devices.mjs && node gen-compat.mjs

# ─── Test ─────────────────────────────────────────────────────────────────────

.PHONY: test
# test/ is on LUA_PATH and c4_shim is preloaded, matching the environment
# test/run_test.sh sets up. Existing suites are written against that shim and
# fail on a bare `luajit <file>` with "attempt to index global 'C4'". A test that
# defines its own C4 still wins, since it assigns after the preload has run.
test: ## Run the Lua test suite (test/test_*.lua)
test: $(VENV_STAMP) ## Run the Lua test suite (test/test_*.lua) + the Python tool tests
@found=0; \
for f in test/test_*.lua; do \
[ -e "$$f" ] || continue; \
Expand All @@ -250,6 +266,11 @@ test: ## Run the Lua test suite (test/test_*.lua)
luajit -e "require('c4_shim')" "$$f" || exit 1; \
done; \
if [ "$$found" = "0" ]; then echo "No test/test_*.lua files found; nothing to run."; fi
@for f in tools/test_*.py; do \
[ -e "$$f" ] || continue; \
echo "==> $$f"; \
$(VENV_PY) "$$f" || exit 1; \
done

# ─── Build ────────────────────────────────────────────────────────────────────

Expand Down
Loading
Loading