Skip to content

Avoid the setuptools runtime dependency pulled in through gpiozero #168

Description

@zigpy-review-bot

zigpy-zigate depends on gpiozero to drive the PiZiGate's GPIO pins. gpiozero requires colorzero, which declares setuptools as a runtime dependency:

zigpy-zigate 0.14.0
└── gpiozero 2.0.1
    └── colorzero 2.0
        └── setuptools

We ran into this while fixing Dependabot for zha (zigpy/zha#935): it's the only reason setuptools is in zha's uv.lock, and zigpy/zha#937 had to add a Dependabot ignore for it (Dependabot sees setuptools both as zha's build requirement and as this transitive lock entry, and fails, see dependabot/dependabot-core#14118).

Why it matters

Upgrading doesn't help

Options

  1. Get it fixed in colorzero. A one-line change in its setup.cfg and a new release. That would also let Home Assistant drop the remote_rpi_gpio exceptions. The issue has had no reply from the maintainer since it was opened, but the repo isn't dormant (a PR was merged in March 2026), so a PR might get further than the issue.
  2. Replace gpiozero with a small built-in GPIO helper. The PiZiGate code only switches two output pins (GPIO 17 and 27) and keeps driving them afterwards (UnclosableOutputDevice). A pure-Python implementation on the Linux GPIO character device (/dev/gpiochipN with ioctl) needs no dependencies; gpiozero was chosen in Switch from RPi.GPIO to gpiozero #125 because it's pure Python, unlike RPi.GPIO (RPi.GPIO dependency is no more installed in Home Assistant Core 2022.7.7 #124). It might also help on the Raspberry Pi 5: Home Assistant installs none of gpiozero's other backends (lgpio, RPi.GPIO, pigpio; remote_rpi_gpio's pigpio client needs a separate daemon), so gpiozero falls back to its built-in native backend, which is built for the BCM283x-style GPIO registers of the Pi 1–4; the Pi 5's GPIO sits on its RP1 chip instead. Costs: new code to own; it has to find the right GPIO chip by label or line name (the Pi 5's header moved between gpiochip4 and gpiochip0 across kernel versions), keep the line request open so the pins stay driven, and it's hard to test without the hardware (Switch from RPi.GPIO to gpiozero #125 was also written without a PiZiGate, with the pin sequence checked on an oscilloscope).
  3. Make gpiozero optional. Move the gpiozero import and the UnclosableOutputDevice class into the PiZiGate functions (set_pizigate_running_mode() / set_pizigate_flashing_mode()), and move gpiozero to an extra (e.g. zigpy-zigate[pizigate]). Installs without it lose all three packages. But zha, and with it Home Assistant, would then have to either depend on the extra (bringing the chain back) or stop supporting the PiZiGate out of the box.
  4. Leave it. Home Assistant's exceptions and zha's Dependabot ignore stay.

Questions

  1. Which route: the upstream fix, a built-in GPIO helper, or an optional dependency?
  2. Can anyone test a PiZiGate, ideally on both a Pi 4 and a Pi 5? Does it work on a Pi 5 today?

Activity

  1. TheJulianJES commented on Oct 12, 2026

    @TheJulianJES
    Contributor

    @zigpy-review-bot Create PR for option 2, run review rounds before.

  2. zigpy-review-bot commented on Oct 12, 2026

    @zigpy-review-bot
    CollaboratorAuthor

    Opened #169 for option 2: a small built-in helper on the Linux GPIO character device (v2 uAPI, Linux 5.10+) replaces gpiozero, so gpiozero, colorzero and setuptools drop out of the dependency tree. It finds the PiZiGate lines by name (GPIO17/GPIO27) across all gpiochips, falling back to the Pi pinctrl chip labels (needed on the Pi 5's launch kernel), and keeps the line requests open so the pins stay driven. The pin sequence and timings are unchanged. Tests pass; the PR went through three review rounds and the last one was clean.

    It's not tested on PiZiGate hardware yet. The PR body lists the behaviour changes (needs access to /dev/gpiochip* instead of /dev/gpiomem; pins on non-Raspberry-Pi kernels may stop being driven after the process exits) and asks for testers on a Pi 4 and a Pi 5.

    Separately: pre-commit currently can't install the autoflake8 hook (its upstream repo is gone), which also affects dev, so the PR's pre-commit CI step may fail for that reason.

  3. puddly commented on Oct 12, 2026

    @puddly
    Collaborator

    One other alternative would be writing a minimal module for /dev/gpiomem access and leave Pi 5 compatibility for the future. So far, it's been over three years since the Pi 5 was released and this has not come up. I don't think the ZiGate GPIO module is really that popular. Something like:

    import mmap
    import os
    import time
    
    RESET_PIN = 17
    BOOT_PIN = 27
    
    
    class GPIOMemory:
        # Register byte offsets, from the BCM2835 ARM Peripherals datasheet, section 6.1
        GPFSEL0 = 0x00  # function select: 3 bits per pin, 10 pins per register
        GPSET0 = 0x1C  # write 1 to drive a pin high: 1 bit per pin, 32 pins per register
        GPCLR0 = 0x28  # write 1 to drive a pin low: 1 bit per pin, 32 pins per register
    
        FUNCTION_WIDTH = 3
        FUNCTION_MASK = 0b111
        FUNCTION_OUTPUT = 0b001
    
        def __enter__(self) -> "GPIOMemory":
            fd = os.open("/dev/gpiomem", os.O_RDWR | os.O_SYNC)
            try:
                self._mmap = mmap.mmap(fd, 4096)
            finally:
                os.close(fd)
            self._regs = memoryview(self._mmap).cast("I")  # native 32-bit register access
            return self
    
        def __exit__(self, *exc_info) -> None:
            self._regs.release()
            self._mmap.close()
    
        def _index(self, base: int, register: int) -> int:
            return base // 4 + register
    
        def set_output(self, pin: int) -> None:
            register, field = divmod(pin, 10)
            shift = field * self.FUNCTION_WIDTH
            index = self._index(self.GPFSEL0, register)
    
            # Other pins share this register, so only replace this pin's field
            value = self._regs[index]
            value &= ~(self.FUNCTION_MASK << shift)
            value |= self.FUNCTION_OUTPUT << shift
            self._regs[index] = value
    
        def write(self, pin: int, value: bool) -> None:
            register, bit = divmod(pin, 32)
            base = self.GPSET0 if value else self.GPCLR0
    
            # Zero bits are ignored, so this only affects `pin`
            self._regs[self._index(base, register)] = 1 << bit
    
    
    with GPIOMemory() as gpio:
        gpio.set_output(RESET_PIN)
        gpio.set_output(BOOT_PIN)
    
        gpio.write(BOOT_PIN, True)  # False for flashing mode
        time.sleep(0.5)
    
        gpio.write(RESET_PIN, False)
        time.sleep(0.5)
    
        gpio.write(RESET_PIN, True)
        time.sleep(0.5)
  4. zigpy-review-bot commented on Oct 13, 2026

    @zigpy-review-bot
    CollaboratorAuthor

    A related data point from home-assistant/core#185946. To be clear, that one is not a zigpy-zigate (or gpiozero) bug: a single bit had flipped in the user's local copy of gpiozero/spi_devices.py, turning a newline into a 0x08 byte. The published 2026.10.1 image has the correct file.

    It does show the cost of the module-level import described above, though. The user has no ZiGate, but zha imports zigpy_zigate.zigbee.application at startup, which pulls in zigpy_zigate.common → gpiozero → gpiozero.spi_devices. The resulting SyntaxError failed the whole homeassistant.components.zha import, so ZHA didn't load at all, because of a file that only matters to PiZiGate users.

    Any of the options here that drops gpiozero (#169, a minimal /dev/gpiomem module) or moves the import into the PiZiGate functions takes it off ZHA's startup path.

  5. zigpy-review-bot commented on Oct 13, 2026

    @zigpy-review-bot
    CollaboratorAuthor

    Opened #170 for puddly's suggestion: his /dev/gpiomem helper (first commit, co-authored) replaces gpiozero, so gpiozero, colorzero and setuptools drop out of the runtime dependencies, as with #169.

    On Home Assistant, gpiozero normally drives the PiZiGate through its native backend, which writes the same GPIO registers through /dev/gpiomem, so Pi 1–4 users on Home Assistant keep the same device access and the pins stay driven after exit as before. A follow-up commit adds a board check (the Raspberry Pi revision code, read like gpiozero does): it refuses to write the registers on a Pi 5 or a non-Pi board instead of hitting the wrong chip. The Pi 5 stays unsupported, as it is today with gpiozero's native backend.

    The PR body compares it with #169 and lists the corner cases where it behaves differently from gpiozero (no /dev/mem fallback; setups where gpiozero uses lgpio instead, which is what could make a PiZiGate work on a Pi 5 today). Only one of the two PRs should be merged. Not tested on PiZiGate hardware yet.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    None yet

    Size

    None yet

    Area

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions