Skip to content

Replace gpiozero with a built-in GPIO character device helper - #169

Open
zigpy-review-bot wants to merge 3 commits into
devfrom
zigpy-bot/drop-gpiozero
Open

zigpy-review-bot wants to merge 3 commits into
devfrom
zigpy-bot/drop-gpiozero

Conversation

@zigpy-review-bot

@zigpy-review-bot zigpy-review-bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes #168 (option 2): gpiozero is replaced with a small GPIO helper that drives the PiZiGate's two pins through the Linux GPIO character device, and gpiozero is dropped from dependencies. That removes gpiozero → colorzero → setuptools from every install, so zha no longer needs its Dependabot ignore for it and Home Assistant can drop the zha entries from hassfest's forbidden-package exceptions once a release with this lands.

Changes

  • New zigpy_zigate/gpio.py (standard library only): talks to /dev/gpiochipN with the GPIO v2 uAPI (ioctl). common.py imports it only inside the two PiZiGate functions, so importing the radio module no longer loads any GPIO code (and fcntl, which only exists on Unix, is never imported elsewhere).
  • No chip number is hardcoded. The line is found by its name (GPIO17 / GPIO27, as named by the Raspberry Pi device trees), searching every /dev/gpiochip* in numeric order. If no line carries that name, it falls back to a chip labelled pinctrl-bcm2835, pinctrl-bcm2711 or pinctrl-rp1 (Pi 1–3, Pi 4, Pi 5), where the line offset is the BCM GPIO number. On the Pi 5 this covers both kernel layouts: Raspberry Pi kernels from 6.6 put the header on gpiochip0 with GPIO17-style names, while the 6.1 launch kernel had it on gpiochip4 with lines named after the header pins (PIN11 for GPIO17), which the label fallback handles. On non-Pi hardware (is_pizigate() also matches /dev/ttyS*), a line is only driven if it is literally named GPIO17 / GPIO27; otherwise it fails, as gpiozero did there.
  • Each line is requested as an output on its first write, with that write's value as the initial output value, so no other level is driven onto the pin. gpiozero was used with initial_value=None, which switched both pins to outputs with whatever value their output latch held.
  • The line requests are kept open for the lifetime of the process, replacing UnclosableOutputDevice: the pins stay driven after the mode switch, and later calls (a reconnect, or the flasher switching back to running mode) reuse the held request instead of requesting the line again.
  • set_pizigate_running_mode() / set_pizigate_flashing_mode() keep the exact pin sequence and 0.5 s delays.
  • Failures raise GPIOError with the reason: no /dev/gpiochip*, no matching line (listing chips that could not be read, e.g. permission denied, or a kernel without the v2 uAPI), or the line request failing (e.g. the line is busy). Callers are unchanged, so as before the connection attempt fails with that exception.
  • README: the PiZiGate needs access to /dev/gpiochip* (Linux 5.10 or later).

Behaviour differences to be aware of:

  • Requires Linux 5.10 or later (GPIO v2 uAPI). There is no sysfs fallback: the sysfs GPIO interface is deprecated and older kernels are long out of support.
  • Access needs /dev/gpiochip* instead of /dev/gpiomem. Home Assistant OS (and Supervised) runs the Home Assistant container privileged, so nothing changes there; a Docker setup that only passed through /dev/gpiomem has to pass through the GPIO chip devices instead.
  • When the process exits, the kernel releases the lines. With gpiozero's native backend the pins stayed outputs after the process exited (e.g. after flasher --gpio); now that depends on the kernel. Raspberry Pi kernels since mid-2024 (6.6 and later) keep a released output driven (persist_gpio_outputs, on by default for both the BCM283x/BCM2711 and the RP1 pin controller). Other kernels may return a released line to an input; GPIO17 is the ZiGate's reset line and may then be pulled low, holding the ZiGate in reset until zigpy-zigate next starts and drives both pins again.
  • gpiozero's native backend only knows the GPIO registers of the Pi 1–4; the character device is the same on every Pi, so this may also make the PiZiGate work on a Pi 5 (untested).

Testing

  • New unit tests in tests/test_gpio.py run the helper against a fake character device (mocked os.open / fcntl.ioctl) that also checks the padding and limits the kernel validates: the line lookup by name, by chip label (including the Pi 5 6.1 layout) and in numeric chip order, the request layout, holding and reusing the request, and the error paths. The struct sizes and ioctl numbers are checked against values printed by a small C program compiled against linux/gpio.h (the v2 structs are padded to the same layout on 32- and 64-bit).
  • tests/test_uart.py now asserts the exact pin sequence of both mode switches.
  • Full test suite passes. black, flake8, isort, mypy and pyupgrade pass via pre-commit; the autoflake8 hook was left out of that run because its repository no longer exists, so pre-commit cannot install it (this also affects dev).
  • Not tested on hardware: no PiZiGate here, and no GPIO chip on this machine (the gpio-sim module needs root). Testers with a PiZiGate on a Pi 4 and on a Pi 5 would be very welcome, ideally checking that the ZiGate starts in running mode and that the flasher's --gpio mode still works.

Alternative

#170 takes the approach puddly suggested in #168 instead: a minimal helper that writes the GPIO registers through /dev/gpiomem, as gpiozero's native backend normally does on Home Assistant today. It keeps the current device access and pin behaviour for Raspberry Pi 1–4 users on Home Assistant and leaves the Pi 5 unsupported. Only one of the two PRs should be merged.

gpiozero pulls in colorzero, which declares setuptools as a runtime
dependency. The PiZiGate only needs to drive two output pins, so do that
directly through /dev/gpiochipN with the GPIO v2 uAPI and drop the
dependency.
- gpio.py needs fcntl (Linux only); import it only when switching PiZiGate modes
- point at the kernel version when the GPIO v2 uAPI is missing
- the Pi 5 launch kernel names the RP1 header lines PINnn, test the label
  fallback for it
- the fake kernel checks the padding/limits the real one validates
@codecov

codecov Bot commented Oct 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.03846% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 49.71%. Comparing base (3d82221) to head (b90a369).

Files with missing lines Patch % Lines
zigpy_zigate/gpio.py 98.93% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #169      +/-   ##
==========================================
+ Coverage   46.46%   49.71%   +3.24%     
==========================================
  Files           8        9       +1     
  Lines        1300     1386      +86     
==========================================
+ Hits          604      689      +85     
- Misses        696      697       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code quality Improvement to code quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Avoid the setuptools runtime dependency pulled in through gpiozero

1 participant