diff --git a/custom_components/opendisplay/services.py b/custom_components/opendisplay/services.py index 0a6b898..f347997 100644 --- a/custom_components/opendisplay/services.py +++ b/custom_components/opendisplay/services.py @@ -61,6 +61,7 @@ ATTR_REFRESH_MODE = "refresh_mode" ATTR_FIT_MODE = "fit_mode" ATTR_TONE_COMPRESSION = "tone_compression" +ATTR_USE_MEASURED_PALETTES = "measured_palette" def _str_to_int_enum(enum_class: type[IntEnum]) -> Callable[[str], Any]: @@ -123,6 +124,7 @@ def _refresh_type_value(value: Any) -> RefreshMode: vol.Optional(ATTR_TONE_COMPRESSION): vol.All( vol.Coerce(float), vol.Range(min=0.0, max=100.0) ), + vol.Optional(ATTR_USE_MEASURED_PALETTES, default=True): cv.boolean, } ) @@ -135,11 +137,12 @@ def _refresh_type_value(value: Any) -> RefreshMode: vol.Required("payload"): list, vol.Optional("background", default="white"): cv.string, vol.Optional("rotate", default=0): vol.All(vol.Coerce(int), vol.In([0, 90, 180, 270])), - vol.Optional("dither", default="ordered"): _dither_value, + vol.Optional("dither", default="burkes"): _dither_value, vol.Optional("refresh_type", default="full"): _refresh_type_value, vol.Optional(ATTR_TONE_COMPRESSION): vol.All( vol.Coerce(float), vol.Range(min=0.0, max=100.0) ), + vol.Optional(ATTR_USE_MEASURED_PALETTES, default=False): cv.boolean, vol.Optional("dry-run", default=False): cv.boolean, }, extra=vol.REMOVE_EXTRA, # silently drop legacy keys (ttl, preload_type, preload_lut, ...) @@ -281,6 +284,7 @@ async def _async_connect_and_run( hass: HomeAssistant, entry: "OpenDisplayConfigEntry", action: Callable[[OpenDisplayDevice], Awaitable[None]], + use_measured_palettes: bool = False, ) -> None: """Resolve BLE device, open a connection, run action, handle auth errors.""" address = entry.unique_id @@ -325,6 +329,7 @@ async def _async_connect_and_run( mac_address=address, ble_device=ble_device, config=entry.runtime_data.device_config, + use_measured_palettes=use_measured_palettes, encryption_key=encryption_key, ) as device: await action(device) @@ -351,6 +356,7 @@ async def _async_send_image( fit: FitMode = FitMode.CONTAIN, tone: float | str = "auto", rotate: Rotation = Rotation.ROTATE_0, + use_measured_palettes: bool = False, ) -> None: """Upload a PIL image to the device.""" # Split the upload into its heavy CPU half and its BLE-I/O half. The CPU @@ -396,7 +402,9 @@ async def _async_send_image( async def _upload(device: OpenDisplayDevice) -> None: await device.upload_prepared_image(prepared, refresh_mode=refresh_mode, state=state) - await _async_connect_and_run(hass, entry, _upload) + await _async_connect_and_run( + hass, entry, _upload, use_measured_palettes=use_measured_palettes + ) jpeg = await hass.async_add_executor_job(_pil_to_jpeg, img) async_dispatcher_send(hass, f"{SIGNAL_IMAGE_UPDATED}_{entry.unique_id}", jpeg) @@ -414,6 +422,7 @@ async def _async_upload_image(call: ServiceCall) -> None: tone_compression: float | str = ( tone_compression_pct / 100.0 if tone_compression_pct is not None else "auto" ) + use_measured_palettes: bool = call.data[ATTR_USE_MEASURED_PALETTES] # A plain URL (e.g. an automation pushing a rendered snapshot) must be # explicitly allowlisted; media-source items are already trusted. @@ -463,6 +472,7 @@ async def _async_upload_image(call: ServiceCall) -> None: fit=fit_mode, tone=tone_compression, rotate=rotation, + use_measured_palettes=use_measured_palettes, ) except asyncio.CancelledError: return @@ -654,6 +664,7 @@ async def _drawcustom_for_device( tone_compression: float | str = ( tone_compression_pct / 100.0 if tone_compression_pct is not None else "auto" ) + use_measured_palettes: bool = call.data[ATTR_USE_MEASURED_PALETTES] await _async_send_image( hass, @@ -663,6 +674,7 @@ async def _drawcustom_for_device( refresh_mode=refresh_mode, tone=tone_compression, rotate=Rotation(rotate), + use_measured_palettes=use_measured_palettes, ) diff --git a/custom_components/opendisplay/services.yaml b/custom_components/opendisplay/services.yaml index 15d38c8..d4f101c 100644 --- a/custom_components/opendisplay/services.yaml +++ b/custom_components/opendisplay/services.yaml @@ -69,6 +69,11 @@ upload_image: step: 1 mode: slider unit_of_measurement: "%" + measured_palette: + required: false + default: false + selector: + boolean: activate_led: fields: @@ -284,7 +289,7 @@ drawcustom: name: Dither mode description: Dithering algorithm required: true - default: "ordered" + default: "burkes" selector: select: translation_key: dither_mode @@ -321,6 +326,13 @@ drawcustom: step: 1 mode: slider unit_of_measurement: "%" + measured_palette: + name: Use measured palette + description: Use the panel's measured color palette when available. Turn off to dither against the theoretical color scheme. No effect on panels without measured data. + required: false + default: false + selector: + boolean: dry-run: name: Dry run description: Generate image without sending to device diff --git a/custom_components/opendisplay/strings.json b/custom_components/opendisplay/strings.json index 280b2d6..582c954 100644 --- a/custom_components/opendisplay/strings.json +++ b/custom_components/opendisplay/strings.json @@ -194,6 +194,10 @@ "tone_compression": { "description": "Dynamic range compression strength. Leave empty for automatic.", "name": "Tone compression" + }, + "measured_palette": { + "description": "Use the panel's measured color palette when available. Turn off to dither against the theoretical color scheme. No effect on panels without measured data.", + "name": "Use measured palette" } }, "name": "Upload image", @@ -322,6 +326,10 @@ "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" }, + "measured_palette": { + "description": "Use the panel's measured color palette when available. Turn off to dither against the theoretical color scheme. No effect on panels without measured data.", + "name": "Use measured palette" + }, "dry-run": { "description": "Generate image without uploading to the device.", "name": "Dry run" diff --git a/custom_components/opendisplay/translations/en.json b/custom_components/opendisplay/translations/en.json index f9c2181..b14c078 100644 --- a/custom_components/opendisplay/translations/en.json +++ b/custom_components/opendisplay/translations/en.json @@ -191,6 +191,10 @@ "tone_compression": { "description": "Dynamic range compression strength. Leave empty for automatic.", "name": "Tone compression" + }, + "measured_palette": { + "description": "Use the panel's measured color palette when available. Turn off to dither against the theoretical color scheme. No effect on panels without measured data.", + "name": "Use measured palette" } }, "name": "Upload image", @@ -319,6 +323,10 @@ "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" }, + "measured_palette": { + "description": "Use the panel's measured color palette when available. Turn off to dither against the theoretical color scheme. No effect on panels without measured data.", + "name": "Use measured palette" + }, "dry-run": { "description": "Generate image without uploading to the device.", "name": "Dry run"