Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Vendored frontend artifacts — hide from GitHub language / PR review noise.
custom_components/opendisplay/designer/frontend/vendor/** linguist-generated=true
2 changes: 2 additions & 0 deletions custom_components/opendisplay/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from .const import CONF_CACHED_STATE, CONF_ENCRYPTION_KEY, DOMAIN, SETUP_DEADLINE_S
from .coordinator import OpenDisplayCoordinator
from .delivery import DeliveryManager
from .designer import async_setup_designer
from .services import async_setup_services
from .sleep import SleepProfile

Expand Down Expand Up @@ -200,6 +201,7 @@ def _get_encryption_key(entry: OpenDisplayConfigEntry) -> bytes | None:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the OpenDisplay integration."""
async_setup_services(hass)
await async_setup_designer(hass)
return True


Expand Down
47 changes: 47 additions & 0 deletions custom_components/opendisplay/designer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""OpenDisplay image designer panel."""

from __future__ import annotations

import logging

from homeassistant.core import HomeAssistant

from ..const import DOMAIN
from .panel import (
DESIGNER_PANEL_PATH,
OpenDisplayDesignerStaticView,
async_get_panel_module_url,
)

_LOGGER = logging.getLogger(__name__)
_DESIGNER_KEY = "designer"


async def async_setup_designer(hass: HomeAssistant) -> None:
"""Register designer static assets and sidebar panel."""
hass.data.setdefault(DOMAIN, {})
designer_data = hass.data[DOMAIN].setdefault(_DESIGNER_KEY, {})

if not designer_data.get("views_registered"):
hass.http.register_view(OpenDisplayDesignerStaticView(hass))
designer_data["views_registered"] = True

if designer_data.get("panel_registered"):
return

try:
from homeassistant.components import panel_custom

await panel_custom.async_register_panel(
hass,
frontend_url_path=DESIGNER_PANEL_PATH,
webcomponent_name="opendisplay-designer-panel",
sidebar_title="OpenDisplay Designer",
sidebar_icon="mdi:monitor-edit",
module_url=await async_get_panel_module_url(hass),
require_admin=False,
)
designer_data["panel_registered"] = True
_LOGGER.info("OpenDisplay designer panel registered")
except (AttributeError, ImportError, RuntimeError, ValueError) as err:
_LOGGER.warning("Failed to register OpenDisplay designer panel: %s", err)
84 changes: 84 additions & 0 deletions custom_components/opendisplay/designer/capabilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Build designer-facing device capability payloads from runtime config."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from epaper_dithering import ColorPalette, ColorScheme
from opendisplay import Rotation
from opendisplay.display_palettes import get_palette_for_display

from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH

if TYPE_CHECKING:
from .. import OpenDisplayConfigEntry


def resolve_device_id_for_entry(
hass: HomeAssistant, entry: OpenDisplayConfigEntry
) -> str | None:
"""Resolve HA device registry id for a config entry."""
device_registry = dr.async_get(hass)
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
if devices:
return devices[0].id

mac = entry.unique_id
if not mac:
return None
for variant in {mac, mac.upper(), mac.lower()}:
device = device_registry.async_get_device(
connections={(CONNECTION_BLUETOOTH, variant)}
)
if device is not None:
return device.id
return None


def build_capabilities(
entry: OpenDisplayConfigEntry,
device_id: str,
*,
user_rotate_deg: int = 0,
) -> dict[str, Any]:
"""Serialize display capabilities for the designer mount API."""
display = entry.runtime_data.device_config.displays[0]
cs = display.color_scheme_enum
scheme = cs if isinstance(cs, ColorScheme) else ColorScheme.from_value(int(cs))
palette = get_palette_for_display(display.panel_ic_type, scheme)
colors = (
palette.colors
if isinstance(palette, ColorPalette)
else palette.palette.colors
)
color_map: dict[str, str] = {}
for name, rgb in colors.items():
if isinstance(rgb, (tuple, list)) and len(rgb) >= 3:
r, g, b = int(rgb[0]), int(rgb[1]), int(rgb[2])
color_map[str(name)] = f"#{r:02x}{g:02x}{b:02x}"

rotation = display.rotation_enum
base = int(rotation.value if isinstance(rotation, Rotation) else rotation) % 360
effective = (base + user_rotate_deg) % 360
pw, ph = int(display.pixel_width), int(display.pixel_height)
render_w, render_h = (ph, pw) if effective in (90, 270) else (pw, ph)
accent = (
palette.accent
if isinstance(palette, ColorPalette)
else scheme.accent_color
)
return {
"device_id": device_id,
"pixel_width": pw,
"pixel_height": ph,
"rotation_degrees": base,
"render_width": render_w,
"render_height": render_h,
"color_scheme": int(scheme.value),
"accent_color": str(accent),
"available_colors": list(color_map),
"color_map": color_map,
"palette_measured": isinstance(palette, ColorPalette),
}
Loading
Loading