Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c57e587
Add light_command event and register_light command to the protocol
genericJE May 10, 2026
035130d
Add LEDLightEntity with peripheral-declared capabilities
genericJE May 10, 2026
43bfc0f
Wire register_light into the satellite and command dispatcher
genericJE May 10, 2026
5654ee3
Wait briefly for peripherals to register before HA enumerates
genericJE May 10, 2026
a0811b2
Register a Light entity with LVA and honor light_command
genericJE May 10, 2026
fd8bf17
Add Rainbow effect example
genericJE May 10, 2026
208be78
Document the Light entity protocol additions
genericJE May 11, 2026
5bcda27
Pass black, isort, and mypy
genericJE May 11, 2026
c2278a6
Build liblgpio from source in the 2-mic peripheral Dockerfile
genericJE May 11, 2026
c9ed765
Update 2-mic peripheral Dockerfile to Debian trixie base
genericJE May 11, 2026
f200720
Pass GPIO and SPI GIDs to the container instead of group names
genericJE May 11, 2026
48a82d8
Make /app writable so lgpio can initialise as non-root
genericJE May 11, 2026
3f1d676
Route LightCommandRequest to entities and recover NOT_READY on reconnect
genericJE May 11, 2026
fd50278
Toggle mute on idle single press (matches HAVPE centre button)
genericJE May 11, 2026
c265bc6
Rename Rainbow effect to Loop, cycle all LEDs in unison
genericJE May 11, 2026
705d82e
Don't emit empty event_type on subscribe (button press entity)
genericJE May 11, 2026
8ac7e31
Collapse implicit string concatenation in log messages
genericJE May 12, 2026
de99b73
Make satellite.py pass mypy/pylint cleanly
genericJE May 12, 2026
ab8f231
Emit volume_muted so peripherals can track mute state
genericJE May 12, 2026
a3d790d
Tighten comments on the LED Light additions
genericJE May 12, 2026
f949c83
Tighten the volume_muted and ButtonEventSensor comments
genericJE May 12, 2026
1f7cdfe
Wire the multipress handler so its sounds actually fire
genericJE May 12, 2026
aa2cdd7
Align button press timing with HAVPE
genericJE May 12, 2026
305d8f6
Defer the single-click action until the multipress window resolves
genericJE May 12, 2026
c7f63a6
Tighten the multipress comments
genericJE May 12, 2026
f038d48
Merge branch 'leds-and-buttons-events' into feat/peripheral-led-light…
omaramin-2000 May 16, 2026
414abad
Change idle state action from mute to start listening
omaramin-2000 May 18, 2026
fecec1d
Revise button control commands in DOCS.md
omaramin-2000 May 18, 2026
e7f3d5d
Refactor _set_muted to simplify state handling
omaramin-2000 May 30, 2026
d9f7418
Merge branch 'leds-and-buttons-events' into feat/peripheral-led-light…
omaramin-2000 Jun 8, 2026
b26c470
Trim ReSpeaker LED example to the VPE Voice Assistant effect
genericJE Jun 10, 2026
d1dc6fe
Match the Voice PE LED Ring idle behaviour
genericJE Jun 10, 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
18 changes: 18 additions & 0 deletions docs/peripheral_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,30 @@ These are all the events LVA emits. Your peripheral script receives them and rea
| `volume_changed` | `{"volume": float}` | The speaker volume changed (0.0–1.0). Update any volume display or indicator ring. |
| `volume_muted` | `{"muted": bool}` | The media player mute state changed. Distinct from microphone mute (`muted` event). |

### HA driven entity events

These events fire when a user changes a peripheral registered HA entity from Home Assistant. Use them to apply the user's preferences to your hardware in real time.

| Event | Data | Description |
|-------|------|-------------|
| `light_command` | `{"object_id": str, "state": bool, "brightness": float, "red": float, "green": float, "blue": float, "effect": str}` | An HA Light entity that a peripheral registered via `register_light` was changed. The event is broadcast to every connected peripheral, so filter on `object_id` to route it to the right hardware. `brightness` and RGB values are 0.0 to 1.0. `effect` is one of the strings declared when the Light was registered. |

---

## Commands (your script → LVA)

Send these to control LVA from your peripheral hardware.

### Entity registration

A peripheral that wants Home Assistant to control its hardware declares its entities at connect time. LVA materialises matching ESPHome entities, HA enumerates and shows them as it would any other ESPHome device, and user changes flow back as events.

For HA to see your entity, your peripheral must register before HA enumerates the LVA API. LVA waits briefly at startup (see `--peripheral-startup-wait`, default 2 seconds) so peripherals have a window to connect and register. Peripherals that connect later still work, but the new entities only appear in HA after the integration is reloaded.

| Command | Data | Description |
|---------|------|-------------|
| `register_light` | `{"name": str, "object_id": str, "effects": [str], "supports_rgb": bool, "supports_brightness": bool}` | Register a Light entity for an LED strip, ring, or single LED. HA exposes it as `light.<satellite>_<object_id>` with on/off, brightness, RGB, and a selectable effect from the declared list. Subsequent HA changes are delivered as `light_command` events. Send once after connecting; repeat registrations for the same `object_id` are idempotent (no-op). Example: `{"command": "register_light", "data": {"name": "LEDs", "object_id": "leds", "effects": ["Voice Assistant"], "supports_rgb": true, "supports_brightness": true}}` |

### Voice pipeline

| Command | Data | Description |
Expand Down
34 changes: 28 additions & 6 deletions examples/ReSpeaker 2mic HAT/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ LED 0 sits above MIC_L and LED 2 sits above MIC_R. When the microphone is muted,
| LVA state | Animation | LEDs |
|---|---|---|
| Not ready / no HA connection | Dim red pulse | All 3 |
| Idle | Off | All 3 off |
| Wake word detected | Blue flash (×2) | All 3 |
| Listening | Cyan chase (bouncing left ↔ right) | 1 at a time |
| Idle | Solid user color when the LED light is on, else off | All 3 |
| Wake word detected | Flash (×2) in user color | All 3 |
| Listening | Chase (bouncing left ↔ right) in user color | 1 at a time |
| Thinking | Yellow pulse | All 3 |
| TTS speaking | Green breathe (slow sine) | All 3 |
| **Muted** | **Solid red** | **LEDs 0 & 2 only (mic positions)** |
Expand All @@ -76,6 +76,26 @@ LED 0 sits above MIC_L and LED 2 sits above MIC_R. When the microphone is muted,
| Timer ticking | Dim cyan, brightness ∝ time left | All 3 |
| Media playing | Dim green steady | All 3 |

"User color" comes from the Home Assistant Light entity described below (default: HAVPE-style blue). HA brightness scales every animation in this table. Semantic colors (Thinking yellow, Speaking green, Muted red, Timer cyan/blue) are hardcoded so they remain recognisable across user customisation.

---

## Home Assistant Light entity

On connect the controller registers a Light entity with LVA, which appears in Home Assistant as `light.<satellite>_leds`. It defaults off, matching the Voice PE LED Ring; turn it on for a solid idle glow and set its RGB color and brightness from the device page.

### Effects

| Effect | Behaviour |
|---|---|
| `Voice Assistant` (default) | Run the pipeline animations from the table above. Wake word and Listening are tinted with the HA color; brightness scales every animation. |

Like the Home Assistant Voice PE, this example exposes a single Voice Assistant effect: the pipeline animations always run and can't be switched off from HA. The peripheral protocol itself accepts any number of effects, so your own integration is free to declare more (a color loop, a static accent, and so on) — see the [peripheral API docs](../../docs/peripheral_api.md).

### Brightness, on/off, and color

Matching the HA Voice PE LED Ring, the Light defaults off, so the LEDs stay dark while idle until you turn it on; once on, idle shows the solid color, and turning it off again just removes that idle glow. The voice animations always run either way (on/off only gates the idle glow, it does not disable them), so the effect can't be switched off completely. Brightness scales linearly across every animation, and RGB color is the solid idle color and tints the Wake word / Listening animations.

---

## Button behaviour
Expand All @@ -95,14 +115,16 @@ Context-aware command based on current state, mirroring the Home Assistant Voice

### Multi-press gestures

Detected via press timing within a detection window (500 ms between releases):
Detected via press timing within a detection window (250 ms between releases, matching the HAVPE centre button):

| Gesture | Timing | Command sent |
|---|---|---|
| **Double press** | 2 presses < 500 ms apart | `button_double_press` |
| **Triple press** | 3 presses < 500 ms apart | `button_triple_press` |
| **Double press** | 2 presses < 250 ms apart | `button_double_press` |
| **Triple press** | 3 presses < 250 ms apart | `button_triple_press` |
| **Long press** | Single press held > 1000 ms | `button_long_press` |

Each gesture plays its own short confirmation sound on the satellite speaker so the user knows it was detected. Sound paths are configurable via `--button-double-press-sound`, `--button-triple-press-sound`, and `--button-long-press-sound`.

Multi-press commands are useful for triggering custom Home Assistant automations. For example:
- Double press → start a specific routine or mode
- Triple press → access a menu or configuration option
Expand Down
29 changes: 26 additions & 3 deletions examples/ReSpeaker 2mic HAT/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.13-slim-bookworm
FROM python:3.13-slim-trixie
Comment thread
omaramin-2000 marked this conversation as resolved.

ENV LANG=C.UTF-8
ENV DEBIAN_FRONTEND=noninteractive
Expand All @@ -12,17 +12,33 @@ LABEL \
# ----------------------------------------------------------------------------
# System packages
#
# build-essential – compiles spidev C extension
# python3-dev – Python C headers
# The lgpio Python package wraps the system liblgpio C library, which is
# not in Debian's repos (Raspberry Pi OS ships it, upstream Debian does
# not). On aarch64 with no prebuilt wheel we compile the C library from
# source and then build the Python bindings against it.
#
# build-essential compiles the lgpio C library, spidev, and the
# Python lgpio extension
# python3-dev Python C headers for the Python lgpio extension
# swig generates the Python lgpio bindings at build time
# git clones the lgpio C library source
# ----------------------------------------------------------------------------
RUN apt-get update && \
apt-get install --yes --no-install-recommends \
build-essential \
python3-dev \
swig \
git \
ca-certificates && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# Build and install the lgpio C library so the lgpio Python package can
# link against it. Pinned to a tagged release (v0.2.2) for reproducibility.
RUN git clone --depth 1 --branch v0.2.2 https://github.com/joan2937/lg.git /tmp/lg && \
cd /tmp/lg && make && make install && ldconfig && \
rm -rf /tmp/lg

WORKDIR /app

# ----------------------------------------------------------------------------
Expand All @@ -38,5 +54,12 @@ RUN pip install --no-cache-dir -r requirements.txt

COPY respeaker_2mic_hat.py ./

# Make /app writable by the runtime user. lgpio creates notification
# files (.lgd-nfy-N) in the process working directory at startup, and
# compose runs this container as a non root user for device access.
# The default root owned /app blocks lgpio from initialising, which
# makes gpiozero fall through every pin factory.
RUN chmod a+w /app

ENTRYPOINT ["python3", "respeaker_2mic_hat.py"]
CMD []
14 changes: 12 additions & 2 deletions examples/ReSpeaker 2mic HAT/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,19 @@ services:
# -----------------------------------------------------------------------
user: "1000:1000"

# -----------------------------------------------------------------------
# SPI / GPIO group membership
#
# docker compose's group_add resolves names against /etc/group inside
# the container, but the python:slim base does not define gpio or
# spi. Pass numeric GIDs instead so they match the host's groups.
# The defaults below match a stock Raspberry Pi OS install. Check
# yours with "getent group gpio spi" and override in .env if they
# differ.
# -----------------------------------------------------------------------
group_add:
- gpio
- spi
- "${GPIO_GID:-986}"
- "${SPI_GID:-989}"

environment:
- PYTHONUNBUFFERED=1
Expand Down
Loading
Loading