Skip to content

feat: rewrite integration (3.0.0)#31

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

feat: rewrite integration (3.0.0)#31
g4bri3lDev wants to merge 74 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 15 commits June 18, 2026 23:03
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.
…ing for draw_Custom to burkes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Aligns the send_image action defaults with the UI-facing
services.yaml default.
BLE access to a tag was not serialized across operations. Only
_async_upload_image self-serialized (cancelling a prior upload_task);
drawcustom/send_image, LED, buzzer and the OTA flow each opened their own
OpenDisplayDevice connection with no coordination, and the library has no
per-address lock. Two automations hitting the same tag raced for the single
BLE link and surfaced a confusing HomeAssistantError("upload_error"). The
reboot-reload path also waited only on upload_task, so it could tear the
connection out from under an in-flight drawcustom/LED/buzzer/OTA.

Add an asyncio.Lock to OpenDisplayRuntimeData (one config entry == one MAC,
so this is a per-MAC lock) and hold it for the full connection lifetime of
every BLE operation: _async_connect_and_run (drawcustom/send_image, upload,
LED, buzzer) and the OTA install flow. Different MACs are not serialized
against each other. The upload_image latest-wins cancel is kept but its
cancel happens before the lock is taken, so it composes without deadlock.
_async_reload_after_reboot and async_unload_entry now drain the same lock
(acquire/release without holding across the reload) to wait for in-flight
BLE ops, replacing the upload_task-only wait.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
_async_send_image awaited device.upload_image(), which runs the library's
_prepare_image synchronously on the event loop: rotate + fit + dither +
encode + zlib on a full frame. Every upload_image/drawcustom call blocked
the HA event loop for the whole CPU stage before any BLE I/O started.

py-opendisplay 7.9.0 exposes the split deliberately: prepare_image() is the
heavy CPU work and needs only the device config (no BLE), and
device.upload_prepared_image() does the BLE transfer. Run prepare_image() in
the executor via async_add_executor_job and pass the same
dither/tone/fit/rotate options; then upload_prepared_image() on the loop.

compress is gated on display.supports_zip to mirror upload_image() (which
passes `compress and supports_compression` into _prepare_image); when the
panel has no zip support prepare_image() returns compressed_data=None and
upload_prepared_image() falls back to the uncompressed protocol, unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
- NEW-3: default _attr_release_notes at class level so async_release_notes()
  can't raise AttributeError before/without a successful GitHub fetch.
- NEW-4: import ColorScheme from opendisplay (re-exported) instead of the
  undeclared direct epaper_dithering dependency.
- NEW-5: add explicit aiohttp.ClientTimeout to all external HTTP calls
  (GitHub metadata 30s, firmware download 120s, media download 30s).
- NEW-6: run blocking os.path.isdir font-dir checks in the executor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
fix: Serialize BLE access per tag with a per-MAC lock
fix: Offload image prepare to executor, keep BLE transfer on the loop
Fix: minor issues in update and services (NEW-3..NEW-6)
@LordMike

LordMike commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@g4bri3lDev whats the timeline on 3.0.0 ?

I'd like to start using it sometime this week to use a battery powered esp-OD display, which occasionally comes online. So the queueing of images is essential.

Should I run with this branch as a custom component (and possibly do fixes), or is it about done and ready so I should just wait a bit? :)

g4bri3lDev and others added 6 commits July 6, 2026 17:17
Adds "partial" as a refresh mode for upload_image and drawcustom. A
PartialState per config entry tracks the last uploaded frame + etag;
partial refreshes diff against it and update flicker-free via the 0x76
protocol, while full/fast refreshes re-baseline the state so the next
partial diffs against the frame actually on the panel.

All panel handling lives in the library: automatic fallback to a full
upload when partial is not possible, automatic full-frame region
expansion on panels that require it (partial_update_support=2,
OpenDisplay/Firmware#80), and identical frames skip the transfer
entirely. Legacy numeric refresh_type 2/3 map to partial.

Also fixes the compression gate for streaming-decompression-only
configs (bit 0x01 without ZIP): such panels uploaded every frame
uncompressed. Requires py-opendisplay 7.11.1 (partial support,
compression gates, and the epaper-dithering 5.0.8 tone=auto NaN fix
without which bilevel drawcustom frames render all-black).
feat: partial refresh support via py-opendisplay 7.11.1
Expose use_measured_palettes to Home Assistant; default drawcustom dithering to Burkes
py-opendisplay 7.11.2: epaper-dithering 5.0.9 (FFI validation, tone-map
NaN fix line), partial support, compression gate fixes.
odl-renderer 0.5.12: plot crash/hang fixes, text color/anchor + O(n)
fitting, color coercion tolerance, transform/font/QR perf.
chore: bump py-opendisplay to 7.11.2 and odl-renderer to 0.5.12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

5 participants