Skip to content

Replace gpiozero with a minimal /dev/gpiomem GPIO helper - #170

Open
zigpy-review-bot wants to merge 5 commits into
devfrom
zigpy-bot/pizigate-gpiomem
Open

zigpy-review-bot wants to merge 5 commits into
devfrom
zigpy-bot/pizigate-gpiomem

Conversation

@zigpy-review-bot

Copy link
Copy Markdown
Collaborator

Summary

Fixes #168, as an alternative to #169: gpiozero is replaced with a small helper that writes the Raspberry Pi's GPIO registers through /dev/gpiomem, following puddly's suggestion in #168, and gpiozero is dropped from dependencies. That removes gpiozero → colorzero → setuptools from zigpy-zigate's runtime dependencies, and with them from zha's. (Home Assistant's image still installs gpiozero for its remote_rpi_gpio integration, but ZHA no longer needs it or imports it.)

On Home Assistant, gpiozero already drives the PiZiGate this way. The image installs gpiozero 1.6.2 (pinned by remote_rpi_gpio) and the pigpio client, but none of the libraries gpiozero tries first (RPi.GPIO, lgpio, RPIO). The pigpio client only works with a running pigpio daemon, so unless the user runs one, gpiozero falls back to its built-in native backend. That backend (the same in 1.6.2 and 2.0.1) maps /dev/gpiomem and writes the same BCM2835 GPIO registers. This PR does that directly, so for the existing Raspberry Pi 1–4 users the PiZiGate is driven the same way as before, with the same device access.

Changes

  • New zigpy_zigate/gpio.py, standard library only. The first commit is puddly's GPIOMemory class from Avoid the setuptools runtime dependency pulled in through gpiozero #168 (co-authored): it maps /dev/gpiomem, switches a pin to output by replacing only that pin's 3-bit field in its function-select register, and drives it through the set/clear registers.
  • set_pizigate_running_mode() / set_pizigate_flashing_mode() use it with the same pin sequence and 0.5 s delays as before. UnclosableOutputDevice is gone: the registers keep their values after the mapping is closed, so the pins stay outputs and stay driven, including after the process exits, as with gpiozero's native backend.
  • Before mapping, the helper reads the board revision code, from the same places as gpiozero (/proc/device-tree/system/linux,revision, then the Revision line of /proc/cpuinfo), and only continues for the BCM2835, BCM2836, BCM2837 and BCM2711 (Pi 1–4, Zero, Compute Module 1–4), whose GPIO block has this register layout. Otherwise it raises GPIOError instead of writing these offsets into a different chip's registers: on a Pi 5, or on a non-Pi board whose serial port matches is_pizigate() (it also matches /dev/ttyS*). gpiozero's native backend also refuses to start without a revision code, but does not reject the Pi 5.
  • A /dev/gpiomem that cannot be opened also raises GPIOError, with the reason. Nothing catches these, so as before the connection attempt fails with the exception.
  • README: which boards are supported and that /dev/gpiomem access is needed.

Compared with #169

#169 drives the pins through the GPIO character device (/dev/gpiochipN) instead. That interface is the same on every Pi, so it may also work on a Pi 5, but it changes more for current users: it needs /dev/gpiochip* access instead of /dev/gpiomem (a Docker setup that only passes /dev/gpiomem through breaks), Linux 5.10 or later, and whether the pins stay driven after the process exits depends on the kernel. It is also more code (a 196-line helper that searches the GPIO chips for the right line). This PR keeps the current behaviour and leaves the Pi 5 for later: it has been out for three years without a PiZiGate request.

Differences for current users

None expected on Home Assistant (Raspberry Pi 1–4). Corner cases compared with gpiozero:

  • /dev/mem fallback. When /dev/gpiomem cannot be opened, gpiozero tries /dev/mem at the GPIO address from the device tree, which needs root and a kernel that permits it. This PR doesn't, and fails with GPIOError instead. Raspberry Pi kernels provide /dev/gpiomem.
  • Installs where gpiozero uses another backend. gpiozero prefers RPi.GPIO or lgpio when they can be imported (e.g. a Raspberry Pi OS install that uses the system's Python packages), and pigpio when a pigpio daemon is reachable. On a Pi 1–4 this PR drives the same pins through /dev/gpiomem instead, so such a setup now needs access to /dev/gpiomem (lgpio used /dev/gpiochip*, pigpio went through its daemon). On a Pi 5, lgpio currently makes the PiZiGate work there, and this PR raises GPIOError instead. gpiozero's GPIOZERO_PIN_FACTORY and PIGPIO_ADDR environment variables no longer have an effect.
  • When a pin is first used, gpiozero briefly makes it an input and disables its pull resistor before switching it to an output. This PR switches it to an output directly. The pull setting has no effect while the pin is driven.

Testing

  • tests/test_gpio.py: the board check (device tree and /proc/cpuinfo codes, old-style and new-style, Pi 5 and unknown codes, non-Pi), and GPIOMemory against a file standing in for /dev/gpiomem (only the pin's own function-select field changes, the set/clear writes, the values persist after closing, re-opening works).
  • tests/test_uart.py now asserts the exact pin sequence and delays of both mode switches.
  • Full test suite passes (76 tests), also with gpiozero and colorzero not importable. black, flake8, isort, mypy and pyupgrade pass via pre-commit; the autoflake8 hook was left out because its repository no longer exists (also affects dev, see Replace black, isort, flake8 and pyupgrade with ruff #167).
  • Not tested on hardware: no PiZiGate here. A tester with a PiZiGate on a Pi 3 or Pi 4 would be very welcome, ideally checking that the ZiGate starts in running mode and that the flasher's --gpio mode still works.

zigpy-review-bot and others added 5 commits October 13, 2026 21:23
The PiZiGate code only switches GPIO 17 and 27 to outputs and drives
them. On Home Assistant, gpiozero does this with its native backend,
which maps /dev/gpiomem and writes the BCM2835 GPIO registers. Do the
same directly with a small standard-library class, and drop gpiozero,
which removes gpiozero, colorzero and setuptools from the runtime
dependencies.

Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
gpiozero's native backend refuses to start unless it finds a Raspberry
Pi board revision code. Check the same code (from the device tree, or
/proc/cpuinfo) before mapping /dev/gpiomem, and only continue for the
SoCs whose GPIO block has the BCM2835 register layout (BCM2835, BCM2836,
BCM2837, BCM2711). This keeps the BCM2835 register offsets from being
written to a different GPIO block, e.g. on a Pi 5 or another board.

Failures raise GPIOError with the reason, including when /dev/gpiomem
cannot be opened.
Run GPIOMemory against a file standing in for /dev/gpiomem, and assert
the exact pin sequence and delays of both PiZiGate mode switches.
@codecov

codecov Bot commented Oct 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 49.08%. Comparing base (3d82221) to head (a738508).

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #170      +/-   ##
==========================================
+ Coverage   46.46%   49.08%   +2.62%     
==========================================
  Files           8        9       +1     
  Lines        1300     1367      +67     
==========================================
+ Hits          604      671      +67     
  Misses        696      696              

☔ 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