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"chore bump py-opendisplay to 7.4.1
`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.
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.8.0,odl-renderer0.5.9Closes
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 configsSince 3.0.0-beta.4
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)