mirror of
https://github.com/home-assistant/core.git
synced 2026-05-31 12:44:04 +01:00
d766aae436
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: frenck <195327+frenck@users.noreply.github.com>
168 lines
5.4 KiB
Python
168 lines
5.4 KiB
Python
"""Integration for OpenDisplay BLE e-paper displays."""
|
|
|
|
import asyncio
|
|
import contextlib
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
from opendisplay import (
|
|
AuthenticationFailedError,
|
|
AuthenticationRequiredError,
|
|
BLEConnectionError,
|
|
BLETimeoutError,
|
|
GlobalConfig,
|
|
OpenDisplayDevice,
|
|
OpenDisplayError,
|
|
)
|
|
|
|
from homeassistant.components.bluetooth import async_ble_device_from_address
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import Platform
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
|
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH
|
|
from homeassistant.helpers.typing import ConfigType
|
|
|
|
if TYPE_CHECKING:
|
|
from opendisplay.models import FirmwareVersion
|
|
|
|
from .const import CONF_ENCRYPTION_KEY, DOMAIN
|
|
from .coordinator import OpenDisplayCoordinator
|
|
from .services import async_setup_services
|
|
|
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
|
|
|
_BASE_PLATFORMS: list[Platform] = []
|
|
_FLEX_PLATFORMS = [Platform.EVENT, Platform.SENSOR]
|
|
|
|
|
|
@dataclass
|
|
class OpenDisplayRuntimeData:
|
|
"""Runtime data for an OpenDisplay config entry."""
|
|
|
|
coordinator: OpenDisplayCoordinator
|
|
firmware: FirmwareVersion
|
|
device_config: GlobalConfig
|
|
is_flex: bool
|
|
upload_task: asyncio.Task | None = None
|
|
|
|
|
|
type OpenDisplayConfigEntry = ConfigEntry[OpenDisplayRuntimeData]
|
|
|
|
|
|
def _get_encryption_key(entry: OpenDisplayConfigEntry) -> bytes | None:
|
|
"""Return the encryption key bytes from entry data, or None."""
|
|
raw = entry.data.get(CONF_ENCRYPTION_KEY)
|
|
if raw is None:
|
|
return None
|
|
if len(raw) != 32:
|
|
raise ConfigEntryAuthFailed(
|
|
"Stored OpenDisplay encryption key is invalid; reauthentication required"
|
|
)
|
|
try:
|
|
return bytes.fromhex(raw)
|
|
except ValueError as err:
|
|
raise ConfigEntryAuthFailed(
|
|
"Stored OpenDisplay encryption key is invalid; reauthentication required"
|
|
) from err
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
"""Set up the OpenDisplay integration."""
|
|
async_setup_services(hass)
|
|
return True
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: OpenDisplayConfigEntry) -> bool:
|
|
"""Set up OpenDisplay from a config entry."""
|
|
address = entry.unique_id
|
|
if TYPE_CHECKING:
|
|
assert address is not None
|
|
|
|
ble_device = async_ble_device_from_address(hass, address, connectable=True)
|
|
if ble_device is None:
|
|
raise ConfigEntryNotReady(
|
|
f"Could not find OpenDisplay device with address {address}"
|
|
)
|
|
|
|
encryption_key = _get_encryption_key(entry)
|
|
|
|
try:
|
|
async with OpenDisplayDevice(
|
|
mac_address=address, ble_device=ble_device, encryption_key=encryption_key
|
|
) as device:
|
|
fw = await device.read_firmware_version()
|
|
is_flex = device.is_flex
|
|
except (AuthenticationFailedError, AuthenticationRequiredError) as err:
|
|
raise ConfigEntryAuthFailed(
|
|
f"Encryption key rejected by OpenDisplay device: {err}"
|
|
) from err
|
|
except (BLEConnectionError, BLETimeoutError, OpenDisplayError) as err:
|
|
raise ConfigEntryNotReady(
|
|
f"Failed to connect to OpenDisplay device: {err}"
|
|
) from err
|
|
device_config = device.config
|
|
if TYPE_CHECKING:
|
|
assert device_config is not None
|
|
|
|
coordinator = OpenDisplayCoordinator(hass, address)
|
|
|
|
manufacturer = device_config.manufacturer
|
|
display = device_config.displays[0]
|
|
color_scheme_enum = display.color_scheme_enum
|
|
color_scheme = (
|
|
str(color_scheme_enum)
|
|
if isinstance(color_scheme_enum, int)
|
|
else color_scheme_enum.name
|
|
)
|
|
size = (
|
|
f'{display.screen_diagonal_inches:.1f}"'
|
|
if display.screen_diagonal_inches is not None
|
|
else f"{display.pixel_width}x{display.pixel_height}"
|
|
)
|
|
dr.async_get(hass).async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
connections={(CONNECTION_BLUETOOTH, address)},
|
|
manufacturer=manufacturer.manufacturer_name,
|
|
model=f"{size} {color_scheme}",
|
|
sw_version=f"{fw['major']}.{fw['minor']}",
|
|
hw_version=(
|
|
f"{manufacturer.board_type_name or manufacturer.board_type}"
|
|
f" rev. {manufacturer.board_revision}"
|
|
)
|
|
if is_flex
|
|
else None,
|
|
configuration_url="https://opendisplay.org/firmware/config/"
|
|
if is_flex
|
|
else None,
|
|
)
|
|
|
|
entry.runtime_data = OpenDisplayRuntimeData(
|
|
coordinator=coordinator,
|
|
firmware=fw,
|
|
device_config=device_config,
|
|
is_flex=is_flex,
|
|
)
|
|
|
|
await hass.config_entries.async_forward_entry_setups(
|
|
entry, _FLEX_PLATFORMS if is_flex else _BASE_PLATFORMS
|
|
)
|
|
entry.async_on_unload(coordinator.async_start())
|
|
|
|
return True
|
|
|
|
|
|
async def async_unload_entry(
|
|
hass: HomeAssistant, entry: OpenDisplayConfigEntry
|
|
) -> bool:
|
|
"""Unload a config entry."""
|
|
if (task := entry.runtime_data.upload_task) and not task.done():
|
|
task.cancel()
|
|
with contextlib.suppress(asyncio.CancelledError):
|
|
await task
|
|
|
|
return await hass.config_entries.async_unload_platforms(
|
|
entry, _FLEX_PLATFORMS if entry.runtime_data.is_flex else _BASE_PLATFORMS
|
|
)
|