diff --git a/custom_components/vinx/__init__.py b/custom_components/vinx/__init__.py index e7daa56..42b8d83 100644 --- a/custom_components/vinx/__init__.py +++ b/custom_components/vinx/__init__.py @@ -1,27 +1,69 @@ -from __future__ import annotations +from dataclasses import dataclass from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceInfo, format_mac +from custom_components.vinx.const import DOMAIN from custom_components.vinx.lw3 import LW3 PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER] +@dataclass +class DeviceInformation: + mac_address: str + product_name: str + device_info: DeviceInfo + + +@dataclass +class VinxRuntimeData: + lw3: LW3 + device_information: DeviceInformation + + +async def get_device_information(lw3: LW3) -> DeviceInformation: + async with lw3.connection(): + mac_address = str(await lw3.get_property("/.MacAddress")) + product_name = str(await lw3.get_property("/.ProductName")) + device_label = str(await lw3.get_property("/SYS/MB.DeviceLabel")) + firmware_version = str(await lw3.get_property("/.FirmwareVersion")) + serial_number = str(await lw3.get_property("/.SerialNumber")) + ip_address = str(await lw3.get_property("/MANAGEMENT/NETWORK.IpAddress")) + + device_info = DeviceInfo( + identifiers={(DOMAIN, format_mac(mac_address))}, + name=f"{device_label} ({product_name})", + manufacturer="Lightware", + model=product_name, + sw_version=firmware_version, + serial_number=serial_number, + configuration_url=f"http://{ip_address}/", + ) + + return DeviceInformation(mac_address, product_name, device_info) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - # Verify we can connect + if "host" in entry.data and "port" in entry.data: + lw3 = LW3(entry.data["host"], entry.data["port"]) + else: + raise KeyError("Config entry is missing required parameters") + try: - device = LW3(entry.data["host"], entry.data["port"]) - await device.connect() + # Store runtime information + async with lw3.connection(): + device_information = await get_device_information(lw3) + + # Store the lw3 as runtime data in the entry + entry.runtime_data = VinxRuntimeData(lw3, device_information) except ConnectionError as e: raise ConfigEntryNotReady("Unable to connect") from e - # Store the device as runtime data in teh entry - entry.runtime_data = device - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/custom_components/vinx/config_flow.py b/custom_components/vinx/config_flow.py index cd4b95d..04af8b2 100644 --- a/custom_components/vinx/config_flow.py +++ b/custom_components/vinx/config_flow.py @@ -23,16 +23,14 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Con errors: dict[str, str] = {} if user_input is not None: try: - # Try to connect to the device - lw3_device = LW3(user_input["host"], user_input["port"]) - await lw3_device.connect() - - # Make a title for the entry - title = str(await lw3_device.get_property("/.ProductName")) - - # Disconnect, this was just for validation - await lw3_device.disconnect() - except (ConnectionError, OSError): + # Query the device for enough information to make an entry title + lw3 = LW3(user_input["host"], user_input["port"]) + async with lw3.connection(): + product_name = await lw3.get_property("/.ProductName") + device_label = await lw3.get_property("/SYS/MB.DeviceLabel") + + title = f"{device_label} ({product_name})" + except (BrokenPipeError, ConnectionError, OSError): # all technically OSError errors["base"] = "cannot_connect" else: return self.async_create_entry(title=title, data=user_input) diff --git a/custom_components/vinx/lw3.py b/custom_components/vinx/lw3.py index ee228be..9418746 100644 --- a/custom_components/vinx/lw3.py +++ b/custom_components/vinx/lw3.py @@ -47,10 +47,13 @@ async def _read_until(self, phrase: str) -> str | None: if b.endswith(phrase.encode()): return b.decode() - async def connect(self): + def connection(self): + return LW3ConnectionContext(self) + + async def _connect(self): self._reader, self._writer = await asyncio.open_connection(self._hostname, self._port) - async def disconnect(self): + async def _disconnect(self): self._writer.close() await self._writer.wait_closed() @@ -99,3 +102,14 @@ async def get_property(self, path: str) -> PropertyResponse: async def set_property(self, path: str, value: str) -> PropertyResponse: return await asyncio.wait_for(self._run_set_property(path, value), self._timeout) + + +class LW3ConnectionContext: + def __init__(self, lw3: LW3): + self._lw3 = lw3 + + async def __aenter__(self): + await self._lw3._connect() + + async def __aexit__(self, *args): + await self._lw3._disconnect() diff --git a/custom_components/vinx/media_player.py b/custom_components/vinx/media_player.py index 3ef3d28..55d89eb 100644 --- a/custom_components/vinx/media_player.py +++ b/custom_components/vinx/media_player.py @@ -1,21 +1,67 @@ import logging -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player import MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState +from homeassistant.helpers.device_registry import DeviceInfo -from custom_components.vinx import LW3 +from custom_components.vinx import LW3, DeviceInformation, VinxRuntimeData _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): # Extract stored runtime data - lw3_device: LW3 = entry.runtime_data - _LOGGER.info(lw3_device) + runtime_data: VinxRuntimeData = entry.runtime_data + _LOGGER.info(f"Runtime data: {runtime_data}") + # Add entity to Home Assistant + product_name = runtime_data.device_information.product_name + if product_name.endswith("ENC"): + async_add_entities([VinxEncoder(runtime_data.lw3, runtime_data.device_information)]) + pass + elif product_name.endswith("DEC"): + async_add_entities([VinxDecoder(runtime_data.lw3, runtime_data.device_information)]) + pass + else: + _LOGGER.warning("Unknown device type, no entities will be added") -class VinxEncoder(MediaPlayerEntity): - pass +class AbstractVinxDevice(MediaPlayerEntity): + def __init__(self, lw3: LW3, device_information: DeviceInformation) -> None: + self._lw3 = lw3 + self._device_information = device_information + + self._device_class = "receiver" + self._state = MediaPlayerState.IDLE + + @property + def device_class(self): + return self._device_class + + @property + def unique_id(self) -> str | None: + mac_address = self._device_information.mac_address + + return f"vinx_{mac_address}_media_player" + + @property + def state(self): + return self._state -class VinxDecoder(MediaPlayerEntity): + @property + def device_info(self) -> DeviceInfo: + return self._device_information.device_info + + @property + def name(self): + return "Media Player" + + +class VinxEncoder(AbstractVinxDevice): pass + + +class VinxDecoder(AbstractVinxDevice): + def __init__(self, lw3: LW3, device_information: DeviceInformation) -> None: + super().__init__(lw3, device_information) + + _attr_supported_features = MediaPlayerEntityFeature.SELECT_SOURCE