From 9a98508d8228fb95980e564981fa823b52a10801 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Tue, 10 Sep 2024 10:24:13 +0300 Subject: [PATCH 1/6] Create better entity names --- custom_components/vinx/__init__.py | 3 ++- custom_components/vinx/media_player.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/custom_components/vinx/__init__.py b/custom_components/vinx/__init__.py index 42b8d83..eb88c31 100644 --- a/custom_components/vinx/__init__.py +++ b/custom_components/vinx/__init__.py @@ -16,6 +16,7 @@ class DeviceInformation: mac_address: str product_name: str + device_label: str device_info: DeviceInfo @@ -44,7 +45,7 @@ async def get_device_information(lw3: LW3) -> DeviceInformation: configuration_url=f"http://{ip_address}/", ) - return DeviceInformation(mac_address, product_name, device_info) + return DeviceInformation(mac_address, product_name, device_label, device_info) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/custom_components/vinx/media_player.py b/custom_components/vinx/media_player.py index 55d89eb..f1cb4ed 100644 --- a/custom_components/vinx/media_player.py +++ b/custom_components/vinx/media_player.py @@ -53,7 +53,16 @@ def device_info(self) -> DeviceInfo: @property def name(self): - return "Media Player" + # Use increasingly less descriptive names depending on what information is available + device_label = self._device_information.device_label + serial_number = self._device_information.device_info.get("serial_number") + + if device_label: + return f"{self._device_information.device_label} media player" + elif serial_number: + return f"VINX {serial_number} media player" + else: + return "VINX media player" class VinxEncoder(AbstractVinxDevice): From 7e19faff6515ad2373a9e1a053f39aeb37810fe2 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Tue, 10 Sep 2024 10:24:48 +0300 Subject: [PATCH 2/6] Rename base class for media players, using "device" was confusing Device != Entity --- custom_components/vinx/media_player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/vinx/media_player.py b/custom_components/vinx/media_player.py index f1cb4ed..54e92e5 100644 --- a/custom_components/vinx/media_player.py +++ b/custom_components/vinx/media_player.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.warning("Unknown device type, no entities will be added") -class AbstractVinxDevice(MediaPlayerEntity): +class AbstractVinxMediaPlayerEntity(MediaPlayerEntity): def __init__(self, lw3: LW3, device_information: DeviceInformation) -> None: self._lw3 = lw3 self._device_information = device_information @@ -65,11 +65,11 @@ def name(self): return "VINX media player" -class VinxEncoder(AbstractVinxDevice): +class VinxEncoder(AbstractVinxMediaPlayerEntity): pass -class VinxDecoder(AbstractVinxDevice): +class VinxDecoder(AbstractVinxMediaPlayerEntity): def __init__(self, lw3: LW3, device_information: DeviceInformation) -> None: super().__init__(lw3, device_information) From e9d6c0a32b6f3ebc8414c4f218b50b73183bb460 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Tue, 10 Sep 2024 10:25:05 +0300 Subject: [PATCH 3/6] Configure a unique ID for the config flow to prevent adding duplicate devices --- custom_components/vinx/config_flow.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/custom_components/vinx/config_flow.py b/custom_components/vinx/config_flow.py index 04af8b2..0fc69d4 100644 --- a/custom_components/vinx/config_flow.py +++ b/custom_components/vinx/config_flow.py @@ -3,6 +3,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.helpers.device_registry import format_mac from .const import CONF_HOST, CONF_PORT, DOMAIN from .lw3 import LW3 @@ -23,13 +24,21 @@ 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: - # Query the device for enough information to make an entry title + # Verify that the device is connectable lw3 = LW3(user_input["host"], user_input["port"]) async with lw3.connection(): + # Query information for the entry title and entry unique ID product_name = await lw3.get_property("/.ProductName") device_label = await lw3.get_property("/SYS/MB.DeviceLabel") + mac_address = await lw3.get_property("/.MacAddress") title = f"{device_label} ({product_name})" + + unique_id = format_mac(str(mac_address)) + await self.async_set_unique_id(unique_id) + + # Abort the configuration if the device is already configured + self._abort_if_unique_id_configured() except (BrokenPipeError, ConnectionError, OSError): # all technically OSError errors["base"] = "cannot_connect" else: From 206062a963b21c3a5b7d09e02fb763a55b146520 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Tue, 10 Sep 2024 10:51:12 +0300 Subject: [PATCH 4/6] Store the config flow schema within the config flow --- custom_components/vinx/config_flow.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/custom_components/vinx/config_flow.py b/custom_components/vinx/config_flow.py index 0fc69d4..3958de9 100644 --- a/custom_components/vinx/config_flow.py +++ b/custom_components/vinx/config_flow.py @@ -8,17 +8,19 @@ from .const import CONF_HOST, CONF_PORT, DOMAIN from .lw3 import LW3 -STEP_USER_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): str, - vol.Required(CONF_PORT, default=6107): int, - } -) - class VinxConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + @property + def schema(self): + return vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=6107): int, + } + ) + async def async_step_user(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle user initiated configuration""" errors: dict[str, str] = {} @@ -44,4 +46,4 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Con else: return self.async_create_entry(title=title, data=user_input) - return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors) + return self.async_show_form(step_id="user", data_schema=self.schema, errors=errors) From af7ac9b8244e14c7eaafb2b8e98b4e7fdc2ff47e Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Tue, 10 Sep 2024 10:57:57 +0300 Subject: [PATCH 5/6] Use the default values stored in the config flow instance --- custom_components/vinx/config_flow.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/custom_components/vinx/config_flow.py b/custom_components/vinx/config_flow.py index 3958de9..7b1cbac 100644 --- a/custom_components/vinx/config_flow.py +++ b/custom_components/vinx/config_flow.py @@ -12,12 +12,17 @@ class VinxConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self): + # Potentially prepopulated values (e.g. during auto-discovery) + self.host: str | None = None + self.port: int = 6107 + @property def schema(self): return vol.Schema( { - vol.Required(CONF_HOST): str, - vol.Required(CONF_PORT, default=6107): int, + vol.Required(CONF_HOST, default=self.host): str, + vol.Required(CONF_PORT, default=self.port): int, } ) From 8995c574d3ef57b6608456eb263452c59c1590fd Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Tue, 10 Sep 2024 10:58:40 +0300 Subject: [PATCH 6/6] Implement Zeroconf auto-discovery Untested since I'm not able to access the network --- custom_components/vinx/config_flow.py | 20 ++++++++++++++++++++ custom_components/vinx/manifest.json | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/custom_components/vinx/config_flow.py b/custom_components/vinx/config_flow.py index 7b1cbac..e3ca8fd 100644 --- a/custom_components/vinx/config_flow.py +++ b/custom_components/vinx/config_flow.py @@ -1,13 +1,18 @@ +import logging + from typing import Any import voluptuous as vol +from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.helpers.device_registry import format_mac from .const import CONF_HOST, CONF_PORT, DOMAIN from .lw3 import LW3 +_LOGGER = logging.getLogger(__name__) + class VinxConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 @@ -52,3 +57,18 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Con return self.async_create_entry(title=title, data=user_input) return self.async_show_form(step_id="user", data_schema=self.schema, errors=errors) + + async def async_step_zeroconf(self, discovery_info: ZeroconfServiceInfo) -> ConfigFlowResult: + _LOGGER.info(f"Zeroconf discovery info: {discovery_info}") + + # Abort if the device is already configured + unique_id = format_mac(discovery_info.properties.get("mac")) + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + # Pre-populate the form + self.host = discovery_info.ip_address + self.port = discovery_info.port + + # Trigger the user configuration flow + return await self.async_step_user() diff --git a/custom_components/vinx/manifest.json b/custom_components/vinx/manifest.json index 026f0dd..8fccf7a 100644 --- a/custom_components/vinx/manifest.json +++ b/custom_components/vinx/manifest.json @@ -13,5 +13,5 @@ "iot_class": "local_polling", "requirements": [], "ssdp": [], - "zeroconf": [] + "zeroconf": ["_lwr3._tcp.local."] }