Skip to content

feat: rewrite integration (3.0.0)#31

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

feat: rewrite integration (3.0.0)#31
g4bri3lDev wants to merge 49 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.8.0, odl-renderer 0.5.9

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

Since 3.0.0-beta.4

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"

teancom and others added 20 commits May 30, 2026 20:00
`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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants