docs: add README and Home Assistant integration guide
Build deb packages on release / build-deb (release) Successful in 3m29s
Build deb packages on release / build-deb (release) Successful in 3m29s
This commit is contained in:
@@ -0,0 +1,268 @@
|
|||||||
|
# ezcoo-usb-control
|
||||||
|
|
||||||
|
> MQTT bridge for the **EZCOO EZ-MX42HAS-ARC** 4×2 HDMI matrix, with Home Assistant auto-discovery.
|
||||||
|
|
||||||
|
`ezcoo-usb-control` is a small Go daemon that talks to an EZCOO HDMI matrix
|
||||||
|
over USB-UART using its ASCII command set and exposes the routing state to
|
||||||
|
an MQTT broker. It publishes Home Assistant MQTT discovery payloads so the
|
||||||
|
matrix shows up automatically as two `select` entities — one per output —
|
||||||
|
ready to be used in dashboards and automations.
|
||||||
|
|
||||||
|
Built for, and running on, a Raspberry Pi 4, but the binary is plain Go and
|
||||||
|
works on any Linux/arm64, Linux/armhf or Linux/amd64 host that can see the
|
||||||
|
matrix as a serial device.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
- [Features](#features)
|
||||||
|
- [Hardware](#hardware)
|
||||||
|
- [Quick start](#quick-start)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Debian / Raspberry Pi OS (.deb)](#debian--raspberry-pi-os-deb)
|
||||||
|
- [From source](#from-source)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Running](#running)
|
||||||
|
- [MQTT topics](#mqtt-topics)
|
||||||
|
- [Home Assistant](#home-assistant)
|
||||||
|
- [Verifying the serial protocol](#verifying-the-serial-protocol)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [Project layout](#project-layout)
|
||||||
|
- [Development](#development)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [License](#license)
|
||||||
|
- [Acknowledgements](#acknowledgements)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Two-way control of a 4-in / 2-out EZCOO HDMI matrix over USB-UART
|
||||||
|
- Home Assistant MQTT auto-discovery — zero manual entity wiring
|
||||||
|
- Periodic polling so external changes (front panel, IR remote) are reflected
|
||||||
|
in MQTT
|
||||||
|
- LWT-based availability (`online` / `offline`)
|
||||||
|
- Structured logging via `log/slog`, with `--debug` flag
|
||||||
|
- Hardened `systemd` unit and `.deb` packages for `arm64`, `armhf`, `amd64`
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
Tested with:
|
||||||
|
|
||||||
|
- **EZCOO EZ-MX42HAS-ARC** (4 HDMI inputs, 2 HDMI outputs, ARC) — exposed
|
||||||
|
as a USB CDC-ACM serial port (typically `/dev/ttyACM0`, 57600 8N1)
|
||||||
|
- Raspberry Pi 4 (Raspberry Pi OS, 64-bit) as the host
|
||||||
|
|
||||||
|
Other EZCOO matrices that share the same `EZG OUT0 VS` / `EZS OUTx VS INy`
|
||||||
|
ASCII protocol should work; if your unit speaks a slightly different dialect
|
||||||
|
see [Verifying the serial protocol](#verifying-the-serial-protocol).
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. Build a Raspberry Pi 4 (arm64) .deb on any Linux host with Go installed
|
||||||
|
make deb-arm64 VERSION=0.1.0
|
||||||
|
|
||||||
|
# 2. Copy and install on the Pi
|
||||||
|
scp dist/ezcoo-usb-control_0.1.0_arm64.deb pi@raspberrypi:~
|
||||||
|
ssh pi@raspberrypi 'sudo apt install ./ezcoo-usb-control_0.1.0_arm64.deb'
|
||||||
|
|
||||||
|
# 3. Configure and start
|
||||||
|
ssh pi@raspberrypi 'sudoedit /etc/ezcoo-usb-control/config.yaml'
|
||||||
|
ssh pi@raspberrypi 'sudo systemctl restart ezcoo-usb-control'
|
||||||
|
ssh pi@raspberrypi 'sudo journalctl -u ezcoo-usb-control -f'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Debian / Raspberry Pi OS (.deb)
|
||||||
|
|
||||||
|
Pre-built `.deb` packages are produced for each release; download the
|
||||||
|
matching architecture from the project's releases page and install:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt install ./ezcoo-usb-control_<version>_<arch>.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
The package installs:
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `/usr/bin/ezcoo-usb-control` | Binary |
|
||||||
|
| `/etc/ezcoo-usb-control/config.yaml` | Default config (marked `conffile`) |
|
||||||
|
| `/lib/systemd/system/ezcoo-usb-control.service` | systemd unit |
|
||||||
|
|
||||||
|
A dedicated `ezcoo` system user is created with `dialout` as a supplementary
|
||||||
|
group so the daemon can open the serial device without root.
|
||||||
|
|
||||||
|
To build a `.deb` yourself for any supported architecture:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make deb-arm64 VERSION=0.1.0 # Raspberry Pi 4 / 5 (64-bit)
|
||||||
|
make deb-armhf VERSION=0.1.0 # Raspberry Pi 2/3 / Zero 2 (32-bit)
|
||||||
|
make deb-amd64 VERSION=0.1.0 # x86_64
|
||||||
|
make deb-all VERSION=0.1.0 # all three
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `dpkg-dev` on the build host (`sudo apt install dpkg-dev`).
|
||||||
|
|
||||||
|
### From source
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# native build
|
||||||
|
make build
|
||||||
|
./build/ezcoo-usb-control --config cmd/ezcoo-usb-control/config.example.yaml
|
||||||
|
|
||||||
|
# or directly with go
|
||||||
|
go build -o ezcoo-usb-control ./cmd/ezcoo-usb-control
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Go ≥ 1.26 (see `go.mod`).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Copy `cmd/ezcoo-usb-control/config.example.yaml` to `/etc/ezcoo-usb-control/config.yaml`
|
||||||
|
(the `.deb` package does this for you) and edit:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
device:
|
||||||
|
port: /dev/ttyACM0 # serial device exposed by the matrix
|
||||||
|
baud: 57600 # EZCOO default
|
||||||
|
poll_interval: 15s # how often to query routing state
|
||||||
|
|
||||||
|
mqtt:
|
||||||
|
broker: tcp://192.168.1.10:1883
|
||||||
|
username: ""
|
||||||
|
password: ""
|
||||||
|
client_id: ezcoo-usb-control
|
||||||
|
base_topic: ezcoo # all state/set topics live under this
|
||||||
|
discovery_prefix: homeassistant # Home Assistant discovery root
|
||||||
|
```
|
||||||
|
|
||||||
|
All fields have sensible defaults — the minimum viable config is just
|
||||||
|
`mqtt.broker`. The config file path is passed via `--config`; if omitted,
|
||||||
|
defaults are used as-is.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ezcoo-usb-control --config /etc/ezcoo-usb-control/config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|---|---|
|
||||||
|
| `--config <path>` | Path to YAML config (optional) |
|
||||||
|
| `--debug` | Enable debug-level logging |
|
||||||
|
|
||||||
|
Under `systemd` the service is started with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo systemctl enable --now ezcoo-usb-control
|
||||||
|
sudo journalctl -u ezcoo-usb-control -f
|
||||||
|
```
|
||||||
|
|
||||||
|
To observe what the bridge publishes while running:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mosquitto_sub -h localhost -t 'homeassistant/#' -v
|
||||||
|
mosquitto_sub -h localhost -t 'ezcoo/#' -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## MQTT topics
|
||||||
|
|
||||||
|
Topic names assume the default `base_topic: ezcoo` and `discovery_prefix: homeassistant`.
|
||||||
|
|
||||||
|
| Topic | Direction | Payload | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `ezcoo/availability` | publish (retained, LWT) | `online` / `offline` | Bridge liveness |
|
||||||
|
| `ezcoo/output1/state` | publish (retained) | `IN1`..`IN4` | Current input routed to OUT1 |
|
||||||
|
| `ezcoo/output2/state` | publish (retained) | `IN1`..`IN4` | Current input routed to OUT2 |
|
||||||
|
| `ezcoo/output1/set` | subscribe | `IN1`..`IN4` | Switch OUT1 to the given input |
|
||||||
|
| `ezcoo/output2/set` | subscribe | `IN1`..`IN4` | Switch OUT2 to the given input |
|
||||||
|
| `homeassistant/device/ezcoo_matrix/config` | publish (retained) | JSON | HA device discovery payload |
|
||||||
|
|
||||||
|
## Home Assistant
|
||||||
|
|
||||||
|
If Home Assistant is connected to the same MQTT broker, the matrix appears
|
||||||
|
automatically as a single device with two `select` entities — no YAML
|
||||||
|
required. See [`docs/home-assistant.md`](docs/home-assistant.md) for
|
||||||
|
discovery details, example automations (including a "mirror both outputs"
|
||||||
|
recipe), and a Lovelace card snippet.
|
||||||
|
|
||||||
|
## Verifying the serial protocol
|
||||||
|
|
||||||
|
Before first deployment, probe the exact command set of your unit:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
picocom -b 57600 /dev/ttyACM0
|
||||||
|
# then type: EZH (dumps all supported commands)
|
||||||
|
# then type: EZG OUT0 VS (shows current routing of all outputs)
|
||||||
|
```
|
||||||
|
|
||||||
|
The bridge expects responses in the form `OUT1 VS IN3`. If your unit
|
||||||
|
replies differently, adjust the `reOutVS` regular expression in
|
||||||
|
[`pkg/ezcoo/utils.go`](pkg/ezcoo/utils.go).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **`permission denied: /dev/ttyACM0`** — the `ezcoo` user must be in the
|
||||||
|
group that owns the device (`dialout` on Debian/RPi OS). The `.deb` adds
|
||||||
|
this automatically; for custom installs, do it manually.
|
||||||
|
- **Entities don't appear in Home Assistant** — confirm the discovery prefix
|
||||||
|
matches HA's MQTT integration setting (default `homeassistant`), and that
|
||||||
|
the broker shows a retained payload on `homeassistant/device/ezcoo_matrix/config`.
|
||||||
|
- **State never changes** — run with `--debug` and watch for `state update`
|
||||||
|
log lines. If polling never returns matches, the regex likely needs
|
||||||
|
adjusting (see above).
|
||||||
|
- **Bridge reports `offline` after connecting** — check that the systemd
|
||||||
|
unit has access to the serial char device; the unit ships with
|
||||||
|
`DeviceAllow=char-ttyACM rw` and `DeviceAllow=char-ttyUSB rw`.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── cmd/ezcoo-usb-control/ # main entrypoint, config loader, example config
|
||||||
|
├── pkg/
|
||||||
|
│ ├── bridge/ # MQTT side: connect, subscribe, publish, HA discovery
|
||||||
|
│ └── ezcoo/ # serial side: device driver, protocol, polling loop
|
||||||
|
├── packaging/
|
||||||
|
│ ├── DEBIAN/ # control, postinst, prerm, postrm, conffiles
|
||||||
|
│ └── systemd/ # hardened service unit
|
||||||
|
├── .gitea/workflows/ # CI: build .deb packages on release
|
||||||
|
└── Makefile # build + multi-arch .deb targets
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go build ./...
|
||||||
|
go vet ./...
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Issues and pull requests are welcome. For non-trivial changes please open
|
||||||
|
an issue first so the design can be discussed. When submitting a PR:
|
||||||
|
|
||||||
|
1. Run `go vet ./...` and `go test ./...`.
|
||||||
|
2. Keep changes focused and the commit history clean.
|
||||||
|
3. Update this README if you add a flag, change a topic, or alter the
|
||||||
|
config schema.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Released under the [MIT License](LICENSE.md)
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
- The EZCOO ASCII command set documented in the EZ-MX42HAS-ARC manual.
|
||||||
|
- [`paho.mqtt.golang`](https://github.com/eclipse/paho.mqtt.golang) for the MQTT client.
|
||||||
|
- [`go.bug.st/serial`](https://github.com/bugst/go-serial) for cross-platform serial I/O.
|
||||||
|
- Home Assistant's
|
||||||
|
[MQTT discovery](https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery)
|
||||||
|
schema.
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Home Assistant integration
|
||||||
|
|
||||||
|
`ezcoo-usb-control` publishes an MQTT discovery payload, so the matrix
|
||||||
|
appears in Home Assistant as a single device with two `select` entities —
|
||||||
|
one per HDMI output — without any YAML.
|
||||||
|
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Discovery](#discovery)
|
||||||
|
- [Entities](#entities)
|
||||||
|
- [Example automations](#example-automations)
|
||||||
|
- [Route Apple TV to the living-room TV](#route-apple-tv-to-the-living-room-tv)
|
||||||
|
- [Mirror both outputs to the same input](#mirror-both-outputs-to-the-same-input)
|
||||||
|
- [Lovelace card](#lovelace-card)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Home Assistant is connected to the **same MQTT broker** that the bridge
|
||||||
|
is configured to use.
|
||||||
|
- The
|
||||||
|
[MQTT integration](https://www.home-assistant.io/integrations/mqtt/) is
|
||||||
|
enabled and its **Discovery prefix** matches `mqtt.discovery_prefix` in
|
||||||
|
the bridge config (default: `homeassistant`).
|
||||||
|
- `ezcoo-usb-control` is running and reports `online` on
|
||||||
|
`<base_topic>/availability`.
|
||||||
|
|
||||||
|
## Discovery
|
||||||
|
|
||||||
|
On startup the bridge publishes a retained device-level discovery payload to:
|
||||||
|
|
||||||
|
```
|
||||||
|
<discovery_prefix>/device/ezcoo_matrix/config
|
||||||
|
```
|
||||||
|
|
||||||
|
With defaults that resolves to `homeassistant/device/ezcoo_matrix/config`.
|
||||||
|
You can inspect it with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mosquitto_sub -h <broker> -t 'homeassistant/device/ezcoo_matrix/config' -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Home Assistant picks the payload up automatically and creates the device.
|
||||||
|
Removing the retained message (or stopping the bridge with the broker
|
||||||
|
configured to drop retained messages) makes the entities disappear.
|
||||||
|
|
||||||
|
## Entities
|
||||||
|
|
||||||
|
By default the bridge registers a single device, **EZCOO HDMI Matrix**, with
|
||||||
|
two entities:
|
||||||
|
|
||||||
|
| Entity ID | Type | Options | Source topic |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `select.ezcoo_hdmi_matrix_output_1` | `select` | `IN1`, `IN2`, `IN3`, `IN4` | `ezcoo/output1/state` |
|
||||||
|
| `select.ezcoo_hdmi_matrix_output_2` | `select` | `IN1`, `IN2`, `IN3`, `IN4` | `ezcoo/output2/state` |
|
||||||
|
|
||||||
|
Setting an option publishes to `ezcoo/output<N>/set`; the bridge then
|
||||||
|
issues the corresponding `EZS OUT<N> VS IN<M>` command to the matrix and
|
||||||
|
re-publishes the confirmed state.
|
||||||
|
|
||||||
|
> Entity IDs can be renamed in the Home Assistant UI. The examples below
|
||||||
|
> use the default slugs — adjust if you renamed yours.
|
||||||
|
|
||||||
|
## Example automations
|
||||||
|
|
||||||
|
### Route Apple TV to the living-room TV
|
||||||
|
|
||||||
|
Switch output 1 to input 2 whenever the Apple TV starts playing:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- alias: "Route Apple TV to living-room TV"
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: media_player.apple_tv
|
||||||
|
to: "playing"
|
||||||
|
action:
|
||||||
|
- service: select.select_option
|
||||||
|
target:
|
||||||
|
entity_id: select.ezcoo_hdmi_matrix_output_1
|
||||||
|
data:
|
||||||
|
option: "IN2"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mirror both outputs to the same input
|
||||||
|
|
||||||
|
Whenever either output is changed (via Home Assistant, the front panel, or
|
||||||
|
the IR remote), this automation routes the *other* output to the same
|
||||||
|
input — so both displays always show the same source. The template
|
||||||
|
condition prevents a feedback loop when the second `select` is already in
|
||||||
|
sync.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alias: EZCOO HDMI Matrix — sync outputs
|
||||||
|
description: ""
|
||||||
|
triggers:
|
||||||
|
- trigger: state
|
||||||
|
entity_id:
|
||||||
|
- select.ezcoo_hdmi_matrix_output_1
|
||||||
|
- select.ezcoo_hdmi_matrix_output_2
|
||||||
|
not_to:
|
||||||
|
- unknown
|
||||||
|
- unavailable
|
||||||
|
conditions: []
|
||||||
|
actions:
|
||||||
|
- variables:
|
||||||
|
target: |-
|
||||||
|
{% if trigger.entity_id == 'select.ezcoo_hdmi_matrix_output_1' %}
|
||||||
|
select.ezcoo_hdmi_matrix_output_2
|
||||||
|
{% else %}
|
||||||
|
select.ezcoo_hdmi_matrix_output_1
|
||||||
|
{% endif %}
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ states(target) != trigger.to_state.state }}"
|
||||||
|
- action: select.select_option
|
||||||
|
target:
|
||||||
|
entity_id: "{{ target }}"
|
||||||
|
data:
|
||||||
|
option: "{{ trigger.to_state.state }}"
|
||||||
|
mode: single
|
||||||
|
max_exceeded: silent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lovelace card
|
||||||
|
|
||||||
|
A minimal Lovelace entities card:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: entities
|
||||||
|
title: HDMI Matrix
|
||||||
|
entities:
|
||||||
|
- entity: select.ezcoo_hdmi_matrix_output_1
|
||||||
|
name: Output 1
|
||||||
|
- entity: select.ezcoo_hdmi_matrix_output_2
|
||||||
|
name: Output 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Entities don't appear.** Confirm the broker has a retained payload on
|
||||||
|
`homeassistant/device/ezcoo_matrix/config`, and that Home Assistant's
|
||||||
|
MQTT integration uses the same discovery prefix.
|
||||||
|
- **Entities are stuck on `unavailable`.** The bridge publishes `offline`
|
||||||
|
to `ezcoo/availability` as its Last Will. Check `journalctl -u ezcoo-usb-control`
|
||||||
|
for connection errors.
|
||||||
|
- **Changing a `select` does nothing.** Watch `ezcoo/output<N>/set` with
|
||||||
|
`mosquitto_sub` — if the message arrives, the failure is on the serial
|
||||||
|
side; run the bridge with `--debug` to see the command exchange.
|
||||||
|
- **Automations fire in a loop.** When reacting to `select` state changes,
|
||||||
|
always guard with a template condition that compares current vs target
|
||||||
|
state (see the *sync outputs* example above).
|
||||||
Reference in New Issue
Block a user