Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions custom_components/opendisplay/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
GlobalConfig,
OpenDisplayDevice,
OpenDisplayError,
PartialState,
)

from homeassistant.components.bluetooth import (
Expand Down Expand Up @@ -55,6 +56,10 @@ class OpenDisplayRuntimeData:
# library, so drawcustom/upload_image, LED, buzzer and OTA must not open
# overlapping connections or they race and surface a confusing upload_error.
ble_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
# Tracks the last uploaded frame + etag for differential partial updates
# (0x76). Replaced with a fresh instance on every full/fast refresh so the
# next partial diffs against the frame actually on the panel.
partial_state: PartialState = field(default_factory=PartialState)


type OpenDisplayConfigEntry = ConfigEntry[OpenDisplayRuntimeData]
Expand Down
2 changes: 1 addition & 1 deletion custom_components/opendisplay/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
"iot_class": "local_push",
"issue_tracker": "https://github.com/OpenDisplay/Home_Assistant_Integration/issues",
"loggers": ["opendisplay"],
"requirements": ["py-opendisplay[silabs-ota]==7.9.0", "odl-renderer==0.5.10"],
"requirements": ["py-opendisplay[silabs-ota]==7.11.1", "odl-renderer==0.5.10"],
"version": "3.0.0-beta.7"
}
45 changes: 30 additions & 15 deletions custom_components/opendisplay/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
BuzzerActivateConfig,
OpenDisplayDevice,
OpenDisplayError,
PartialState,
RefreshMode,
Rotation,
prepare_image,
Expand Down Expand Up @@ -87,26 +88,24 @@ def _dither_value(value: Any) -> DitherMode:


def _refresh_type_value(value: Any) -> RefreshMode:
"""Accept names ("full"/"fast") and legacy numeric values.
"""Accept names ("full"/"fast"/"partial") and legacy numeric values.

`partial` is not implemented yet, so it is not offered and any partial-ish
input (legacy 2/3, or an explicit "partial") falls back to fast.
Legacy value 3 ("partial2"/full-frame partial) maps to PARTIAL: the library
reads the panel's partial_update_support config and expands the region to
the full frame automatically where required, so the distinction is obsolete.
"""
if isinstance(value, (int, float)) or (
isinstance(value, str) and value.lstrip("-").isdigit()
):
n = int(value)
if n in (2, 3): # legacy partial / partial2 -> fast (partial not implemented)
return RefreshMode.FAST
if n == 3: # legacy partial2 / full-frame partial
return RefreshMode.PARTIAL
try:
mode = RefreshMode(n)
except ValueError as err:
raise vol.Invalid(f"Invalid refresh_type: {value}") from err
else:
mode = _str_to_int_enum(RefreshMode)(value)
if mode is RefreshMode.PARTIAL: # reserved for the future, not implemented yet
return RefreshMode.FAST
return mode
return mode
return _str_to_int_enum(RefreshMode)(value)


SCHEMA_UPLOAD_IMAGE = vol.Schema(
Expand All @@ -119,7 +118,7 @@ def _refresh_type_value(value: Any) -> RefreshMode:
vol.Coerce(int), vol.Coerce(Rotation)
),
vol.Optional(ATTR_DITHER_MODE, default="burkes"): _str_to_int_enum(DitherMode),
vol.Optional(ATTR_REFRESH_MODE, default="full"): _str_to_int_enum(RefreshMode),
vol.Optional(ATTR_REFRESH_MODE, default="full"): _refresh_type_value,
vol.Optional(ATTR_FIT_MODE, default="contain"): _str_to_int_enum(FitMode),
vol.Optional(ATTR_TONE_COMPRESSION): vol.All(
vol.Coerce(float), vol.Range(min=0.0, max=100.0)
Expand Down Expand Up @@ -362,9 +361,14 @@ async def _async_send_image(
config = entry.runtime_data.device_config
display_cfg = config.displays[0] if config and config.displays else None
# Match upload_image(): only ask prepare_image() to build compressed data
# when the panel actually supports zip. upload_prepared_image() then falls
# back to the uncompressed protocol when compressed_data is None.
supports_compression = display_cfg.supports_zip if display_cfg else True
# when the panel accepts compressed uploads (plain ZIP bit or the
# streaming-decompression bit). upload_prepared_image() then falls back to
# the uncompressed protocol when compressed_data is None.
supports_compression = (
(display_cfg.supports_zip or display_cfg.supports_streaming_decompression)
if display_cfg
else True
)
prepared = await hass.async_add_executor_job(
functools.partial(
prepare_image,
Expand All @@ -378,8 +382,19 @@ async def _async_send_image(
)
)

# Partial refreshes diff against the entry's tracked frame; full/fast
# refreshes re-baseline the panel, so start a fresh state that this upload
# seeds (etag + frame) for the next partial. The library handles all
# fallbacks (unsupported panel, etag mismatch, firmware NACK) and expands
# the region to the full frame on panels that require it
# (partial_update_support=2, see OpenDisplay/Firmware#80).
runtime = entry.runtime_data
if refresh_mode is not RefreshMode.PARTIAL:
runtime.partial_state = PartialState()
state = runtime.partial_state

async def _upload(device: OpenDisplayDevice) -> None:
await device.upload_prepared_image(prepared, refresh_mode=refresh_mode)
await device.upload_prepared_image(prepared, refresh_mode=refresh_mode, state=state)

await _async_connect_and_run(hass, entry, _upload)
jpeg = await hass.async_add_executor_job(_pil_to_jpeg, img)
Expand Down
2 changes: 2 additions & 0 deletions custom_components/opendisplay/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ upload_image:
options:
- "full"
- "fast"
- "partial"
fit_mode:
required: false
default: "contain"
Expand Down Expand Up @@ -308,6 +309,7 @@ drawcustom:
options:
- "full"
- "fast"
- "partial"
tone_compression:
name: Tone compression
description: Tone compression strength (0–100%). Omit to use automatic tone mapping.
Expand Down
7 changes: 4 additions & 3 deletions custom_components/opendisplay/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@
"refresh_mode": {
"options": {
"fast": "Fast",
"full": "Full"
"full": "Full",
"partial": "Partial"
}
}
},
Expand All @@ -183,7 +184,7 @@
"name": "Image"
},
"refresh_mode": {
"description": "The display refresh mode. Full refresh clears ghosting but is slower. Fast refresh is not supported on all displays.",
"description": "The display refresh mode. Full refresh clears ghosting but is slower. Fast refresh is not supported on all displays. Partial updates only the changed region without flashing (B/W panels with partial support; falls back to a full refresh automatically when not possible).",
"name": "Refresh mode"
},
"rotation": {
Expand Down Expand Up @@ -318,7 +319,7 @@
"name": "Dither mode"
},
"refresh_type": {
"description": "Display refresh mode.",
"description": "Display refresh mode. Partial updates only the changed region without flashing; falls back to a full refresh automatically when not possible.",
"name": "Refresh type"
},
"dry-run": {
Expand Down
7 changes: 4 additions & 3 deletions custom_components/opendisplay/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@
"refresh_mode": {
"options": {
"fast": "Fast",
"full": "Full"
"full": "Full",
"partial": "Partial"
}
}
},
Expand All @@ -180,7 +181,7 @@
"name": "Image"
},
"refresh_mode": {
"description": "The display refresh mode. Full refresh clears ghosting but is slower. Fast refresh is not supported on all displays.",
"description": "The display refresh mode. Full refresh clears ghosting but is slower. Fast refresh is not supported on all displays. Partial updates only the changed region without flashing (B/W panels with partial support; falls back to a full refresh automatically when not possible).",
"name": "Refresh mode"
},
"rotation": {
Expand Down Expand Up @@ -315,7 +316,7 @@
"name": "Dither mode"
},
"refresh_type": {
"description": "Display refresh mode.",
"description": "Display refresh mode. Partial updates only the changed region without flashing; falls back to a full refresh automatically when not possible.",
"name": "Refresh type"
},
"dry-run": {
Expand Down
Loading