Skip to content

feat: rewrite integration (3.0.0)#31

Draft
g4bri3lDev wants to merge 60 commits into
mainfrom
feat/clean-port
Draft

feat: rewrite integration (3.0.0)#31
g4bri3lDev wants to merge 60 commits into
mainfrom
feat/clean-port

Conversation

@g4bri3lDev

@g4bri3lDev g4bri3lDev commented May 24, 2026

Copy link
Copy Markdown
Member

Summary

3.0.0 is a ground-up rewrite. 2.x was built around the OpenEPaperLink access-point/tag and ATC protocols with the internal imagegen engine; 3.0 talks directly to OpenDisplay BLE devices via py-opendisplay and renders with odl-renderer. Many 2.x services carry over (reworked onto the new stack); OEPL and ATC support is removed. Breaking changes below.

New in 3.0

  • BLE OTA firmware install: in 2.x the firmware update entity could only show that an update was available; 3.0 adds installing it. Silabs EFR32BG22 devices are flashed over a direct BLE connection. nRF / ESP32 devices still show the available version but have no Install button (proxy-unreliable / no BLE OTA path)
  • upload_image service: uploads a media-source image or a direct image URL
  • Touch event entities for OpenDisplay touch controllers
  • activate_buzzer service (Flex)
  • Per-device BLE RSSI diagnostic sensor (disabled by default)
  • Per-device landing link for the "Visit device" button
  • Automatic re-sync on reboot: when a device reboots (e.g. after a firmware install or config change), the integration detects it from the advertised reboot flag and reloads to re-read the firmware version and configuration

Ported from 2.x (reworked onto the new stack)

  • drawcustom: now renders via odl-renderer with voluptuous schema validation at the service boundary
  • activate_led (was setled): redesigned with up to 3 independent RGB color steps, each with its own flash count and timing (Flex)
  • Firmware update entity: shows installed vs. latest GitHub-release version with release notes (display-only in 2.x; the new install capability is listed above)
  • Display content image entity: shows the last image rendered to each device, updated after every upload_image/drawcustom (including dry-run)
  • Font directory search: .ttf files in /config/www/fonts, /config/media/fonts, or /media/fonts, referenced by name in drawcustom
  • Diagnostic sensors: battery voltage/percentage, temperature, last-seen

Fixes

  • Upload error messages now include the underlying cause
  • BLE END ACK timeout raised to 90s, resolving false upload failures on slow displays
  • Buzzer command ID corrected (py-opendisplay 7.3.2)
  • Icon rendering uses Pillow native anchors (odl-renderer 0.5.9)
  • Accept legacy drawcustom field values from pre-3.0 configs
  • Defer BLE connect until discovery is confirmed
  • Drop board revision from the reported hardware version

Breaking changes

  • All drawcustom element types and field names follow the odl-renderer schema. See the odl-renderer docs
  • Removes OpenEPaperLink (OEPL) and ATC support

Requirements

  • Home Assistant 2026.4.0 or later
  • py-opendisplay[silabs-ota] 7.9.0, odl-renderer 0.5.10

Closes

Closes #27
Closes #28
Closes #29
Closes #40
Closes #43

Changelog by pre-release

3.0.0-beta.1

  • Replaced imagegen with odl-renderer
  • drawcustom service + service-boundary validation; font_dirs wired up
  • upload_image service
  • Touch event entities
  • Requires Home Assistant 2026.4.0+

3.0.0-beta.2

  • Firmware update entity (installed vs. latest GitHub release)
  • activate_led and activate_buzzer services (Flex)
  • RSSI and last-seen diagnostic sensors (all devices, disabled by default)
  • Fix: upload error messages include underlying details
  • Fix: BLE END ACK timeout raised to 90s

3.0.0-beta.3

  • Display content image entity (last rendered image, updates on dry-run too)
  • activate_led redesigned with 3 independent RGB color steps
  • Fix: buzzer command ID (py-opendisplay 7.3.2)
  • Fix: icon rendering via Pillow native anchors (odl-renderer 0.5.9)

3.0.0-beta.4

  • BLE OTA firmware install (Silabs EFR32BG22, direct-connection only; nRF intentionally not offered over a Bluetooth proxy)
  • upload_image accepts a direct image URL
  • Accept legacy drawcustom field values from pre-3.0 configs
  • Defer BLE connect until discovery is confirmed
  • Drop board revision from hardware version
  • Per-device landing link for the "Visit device" button
  • py-opendisplay bumped to 7.8.0

3.0.0-beta.5

3.0.0-beta.6

3.0.0-beta.7

  • drawcustom gains rotation (degrees, positive = clockwise), mirror (h/v/hv) and an optional pivot on every element, plus lenient string handling for visible — a templated "false" or empty string now hides the element (odl-renderer 0.5.10)
  • Docs: supported_types.md links the element/field reference to the odl-renderer README instead of duplicating it; only integration-specific sections remain (service call, colors, fonts, entity-backed plots, templating)

g4bri3lDev added 29 commits May 24, 2026 14:12
… odl-renderer, bump py-opendisplay to 7.2.5)
Pass HA font search directories (/config/www/fonts, /config/media/fonts,
/media/fonts) to generate_image() so users can reference custom fonts by
name without absolute paths. Requires odl-renderer 0.5.8 which adds the
font_dirs parameter to FontManager and generate_image().
- SCHEMA_DRAWCUSTOM validates payload, background, rotate, dither,
  refresh_type and dry-run at the service boundary rather than at
  runtime inside the renderer
- _async_send_image centralises BLE connection, encryption key parsing
  and error handling; used by both upload_image and drawcustom
ImageGen was replaced by odl-renderer; rendering logic is now tested
in the odl-renderer package itself.
Delete empty tests/ dir, requirements_test.txt, tests workflow, and
pytest config now that all drawcustom tests have been removed.
Remove .claude/, .run/, ha_data_provider.md, and uv.lock from
version control; add them to .gitignore.
The upload_error exception swallowed the original OpenDisplayError message,
making it hard to diagnose failures. Pass str(err) as a translation placeholder
so the underlying cause is shown in the HA notification.
Both sensors are diagnostic and disabled by default. All devices now
load the sensor platform (moved Platform.SENSOR to _BASE_PLATFORMS),
so non-flex devices get temperature, RSSI, and last-seen; battery
sensors remain gated on power mode as before.

The sensor value_fn signature is widened from AdvertisementData to
OpenDisplayUpdate to give access to coordinator-level fields (RSSI,
last_seen) alongside advertisement payload fields.

Also syncs en.json with strings.json (adds translations for the new
sensors, update entity, no_leds/no_buzzers exceptions, and the
activate_led/activate_buzzer/drawcustom services that were missing).
- Replace single flat color/flash_count/loop_delay/inter_delay fields
  with three step groups (color1-3, flash_count1-3, loop_delay1-3,
  inter_delay1-3); steps 2 and 3 are skipped when flash_count is 0
- Use RGB color picker selector; voluptuous converts [R,G,B] to the
  firmware's packed 3R-3G-2B byte at validation time
- Expose loop_delay and inter_delay in milliseconds (×100ms units)
  with slider selectors; voluptuous converts to firmware units
- Brightness and repeats now use sliders
Adds a Display content image entity that updates via dispatcher signal
after every successful upload_image or drawcustom call (including dry-run).
name: Draw Custom Image
description: Draws a custom image on one or more E-Paper displays
target:
upload_image:

@LordMike LordMike May 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense for upload_image to (also) take a url?
Or perhaps thats a new service.

My use case is that I have puppet set up with the ability to snapshot HA pages, so I have a URL that can produce the image I want. I want to push that URL.

My current approach is an ODL with a single image entity that covers the whole screen.. which is also still an option.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be implemented now in (ef91895)

action: opendisplay.upload_image
  data:
    device_id: <your device>
    image: "http://homeassistant.local:10000/home?viewport=1000x1000&format=jpeg"

One requirement: the URL host has to be in allowlist_external_urls (this keeps the service from being tricked into fetching arbitrary/internal targets). Since puppet serves on a direct port (10000, no ingress) and handles its own HA auth, a one-time allowlist entry is all it needs:

homeassistant:
  allowlist_external_urls:
    - "http://homeassistant.local:10000"

g4bri3lDev and others added 19 commits June 3, 2026 01:37
`except AuthenticationFailedError, AuthenticationRequiredError:` is invalid
Python 3 syntax; use `except (..., ...):` so a wrong/missing encryption key
surfaces as invalid_auth instead of erroring out.
async_install downloads the firmware asset and flashes over BLE:
- nRF52: trigger DFU, find the MAC+1 DFU device, perform_nrf_dfu.
- EFR32BG22: clear the proxy GATT cache, trigger the AppLoader, then
  perform_silabs_ota; tolerate the device already being in the AppLoader
  (flash directly) so an interrupted OTA can be retried/recovered from HA.
- Wrap OTAError/BLEConnectionError as HomeAssistantError for clean UI errors.

Note: depends on OpenDisplayDevice.clear_gatt_cache + BLEConnectionError from
the in-progress py-opendisplay; the manifest requirement pin is intentionally
left for the py-opendisplay release (kept as a local git-pin for testing).
Two changes, both validated end-to-end through ESPHome proxies:

- Resume from DFU: if the device is already in the bootloader (a prior update
  was interrupted — it then advertises at MAC+1, so its app address resolves to
  None, or the app-mode connect fails), skip the trigger and flash the DFU
  device directly instead of failing hard. Mirrors the EFR32 AppLoader recovery.

- Stay available during install: the device leaves app mode (nRF re-advertises
  at a different address) during the update, so the passive-BLE tracker would
  mark the entity unavailable mid-flash and hide the progress, making a working
  ~5-minute update look like a silent failure. Keep the entity available until
  the install finishes; the transfer runs on its own connection regardless.
nRF Legacy DFU over an ESPHome Bluetooth proxy is verifiably unreliable: the
device receives the complete, CRC-valid image (validate succeeds) but the final
activate/commit write fails over the proxy and strands it in the bootloader. The
exact same flow commits + boots reliably over a direct connection (confirmed on
a direct adapter, including a 10-minute transfer), so it's the proxy path — not
the device, library, or firmware. HA OS is almost always proxy-only, so offering
the install just bricks the tag into DFU and demands manual recovery.

So only EFR32BG22 (Silabs AppLoader, which works over a proxy) offers OTA
install now; nRF (and ESP) expose release-note visibility only. nRF firmware
must be flashed over a direct connection / USB-UF2. Drops the now-unused nRF
branch, resume-from-DFU, and imports from async_install.
The drawcustom service is now registered with a strict voluptuous schema, which rejected service calls written for the main-branch version: numeric dither/refresh_type values and removed keys (ttl, preload_type, preload_lut) all caused validation errors.

  - dither: accept legacy numeric values (0/1/2) alongside the new names;
    values map cleanly since DitherMode ints match the old scheme
  - refresh_type: switch to named values (full/fast) like upload_image, while
    still accepting legacy ints; legacy partial modes (2/3) fall back to fast
    since partial is not implemented yet
  - drop unknown legacy keys via extra=vol.REMOVE_EXTRA instead of erroring
  - give background and refresh_type a UI default of white/full so they show
    preselected (payload element formats were already unchanged)
The upload_image `image` field now accepts a plain http(s) URL alongside the existing media-source picker. This lets an automation push an already-rendered image by URL — e.g. a dashboard snapshot from the puppet add-on (http://homeassistant.local:10000/...) — without first routing it through a media source.
Don't interrogate newly discovered devices automatically; only connect
when the user confirms, so flashing displays no longer requires powering
down BLE proxies.
Set configuration_url to device.landing_url() (opendisplay.org/l/?...), the
per-device deep link the firmware also renders as an on-screen QR code, instead
of the generic config page. Closes #40.
Devices advertise a reboot flag (status byte bit 1) that is set on boot
and cleared on the first BLE connection. The coordinator now detects the
False -> True edge and reloads the config entry, which reconnects (clearing
the flag), re-reads firmware + config, and rebuilds device info/platforms.

The reload defers to any in-progress image upload so an unrelated reboot
does not abort it. Reaction is wired via an async_subscribe_reboot()
callback (mirroring homekit_controller's c# config-change subscription)
to keep the coordinator free of ConfigEntry/runtime_data coupling.
Add tone compression to drawcustom
Bump odl-renderer to 0.5.10, which adds generic rotation, mirror and pivot
fields on every element, plus lenient string coercion for `visible` (a
templated "false" or empty string now hides an element).

Restructure docs/drawcustom/supported_types.md to link the element/field
reference to the odl-renderer README as the single source of truth instead
of duplicating it (the stale tables predated the icon-anchor rewrite, font
dirs, and the new transforms). Only integration-specific content remains:
the service call, colors, fonts, entity-backed plots, and templating examples.
Replace leftover OpenEPaperLink-era wording (OEPL AP, ESL, tag) with
OpenDisplay device terminology, matching the 3.0 rewrite which targets
OpenDisplay BLE devices directly.
Sync the Service Options table with SCHEMA_DRAWCUSTOM / services.yaml: add
refresh_type, tone_compression and the target selector; correct dither to
the named modes (default "ordered"); document rotate's allowed values; drop
the removed ttl option (legacy keys are still accepted but ignored).
Rewrite the Font support section to match 3.0: the search dirs are
/config/www/fonts, /config/media/fonts and /media/fonts (per
_font_search_dirs); default fonts are bundled with odl-renderer. Remove
the 'configure custom font directories' section and the imagegen assets
path, both dropped in the rewrite. Also correct the halftone-color note
to use the named dither modes instead of the legacy dither=2.
Remove the misplaced color-variants sentence from Basic Usage, normalize
the icon example to the mdi: prefix, and fix two section headings that
were H1 instead of H2.
Add dkgray/ltgray gray levels and the corrected half_white/hw (light gray)
to the color list and the per-element values table.

Document the #RGBA and #RRGGBBAA hex forms as accepted, while noting the
alpha component is not blended during rendering and has no visible effect.

Add an IMPORTANT note that hex colors must be quoted in YAML: an unquoted
'#' starts a comment, so 'fill: #FF0000' parses as null and the color is
silently dropped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants