feat: rewrite integration (3.0.0)#31
Conversation
… 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.
…acker, recorder dependency)
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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"`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.
…age_upload. Tested.
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.
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
imagegenengine; 3.0 talks directly to OpenDisplay BLE devices viapy-opendisplayand renders withodl-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
EFR32BG22devices 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_imageservice: uploads a media-source image or a direct image URLactivate_buzzerservice (Flex)Ported from 2.x (reworked onto the new stack)
drawcustom: now renders viaodl-rendererwith voluptuous schema validation at the service boundaryactivate_led(wassetled): redesigned with up to 3 independent RGB color steps, each with its own flash count and timing (Flex)upload_image/drawcustom(including dry-run).ttffiles in/config/www/fonts,/config/media/fonts, or/media/fonts, referenced by name indrawcustomFixes
drawcustomfield values from pre-3.0 configsBreaking changes
drawcustomelement types and field names follow theodl-rendererschema. See the odl-renderer docsRequirements
py-opendisplay[silabs-ota]7.9.0,odl-renderer0.5.10Closes
Closes #27
Closes #28
Closes #29
Closes #40
Closes #43
Changelog by pre-release
3.0.0-beta.1
imagegenwithodl-rendererdrawcustomservice + service-boundary validation;font_dirswired upupload_imageservice3.0.0-beta.2
activate_ledandactivate_buzzerservices (Flex)3.0.0-beta.3
activate_ledredesigned with 3 independent RGB color steps3.0.0-beta.4
EFR32BG22, direct-connection only; nRF intentionally not offered over a Bluetooth proxy)upload_imageaccepts a direct image URLdrawcustomfield values from pre-3.0 configs3.0.0-beta.5
drawcustom90/270 rotation now transposes the canvas instead of scaling/centering it; regression from theimagegen→odl-rendererrewrite (in drawcustom, the rotate function does not work properly. Image is "scaled" and centered instead of just transposed on the x/y axis #43)3.0.0-beta.6
tone_compressionoption fordrawcustom, mirroringupload_image(Add tone compression to drawcustom #46)3.0.0-beta.7
drawcustomgainsrotation(degrees, positive = clockwise),mirror(h/v/hv) and an optionalpivoton every element, plus lenient string handling forvisible— a templated"false"or empty string now hides the element (odl-renderer 0.5.10)supported_types.mdlinks 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)