-
-
Notifications
You must be signed in to change notification settings - Fork 37.8k
Add samsungtv zeroconf discovery #35773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 33 commits
42b1745
965becd
53454a3
a8c6a6f
d115cba
4deffb2
9ee5b11
6b1b832
78fdb62
3958f39
a87d6e2
70f6b81
62f21fd
f5877ab
d26a940
5608d9b
ad3a951
a4c38fd
1b3ec69
71f500f
62d63bf
2007542
4c5c6f6
282c5dc
1435350
fc3116d
0a17c28
334cbde
164d9bd
5a81c5a
7b73c70
fa4c0a8
8d43b53
4d370d8
b27b3c0
854fb47
e13cb71
3ab3dad
d2f830a
7b4d75f
295558b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,20 +1,22 @@ | ||||||
| """Config flow for Samsung TV.""" | ||||||
| import socket | ||||||
| from socket import gethostbyname | ||||||
| from urllib.parse import urlparse | ||||||
|
|
||||||
| import voluptuous as vol | ||||||
|
|
||||||
| from homeassistant import config_entries | ||||||
| from homeassistant import config_entries, data_entry_flow | ||||||
| from homeassistant.components.ssdp import ( | ||||||
| ATTR_SSDP_LOCATION, | ||||||
| ATTR_UPNP_MANUFACTURER, | ||||||
| ATTR_UPNP_MODEL_NAME, | ||||||
| ATTR_UPNP_UDN, | ||||||
| ) | ||||||
| from homeassistant.components.zeroconf import ATTR_PROPERTIES | ||||||
| from homeassistant.const import ( | ||||||
| CONF_HOST, | ||||||
| CONF_ID, | ||||||
| CONF_IP_ADDRESS, | ||||||
| CONF_MAC, | ||||||
| CONF_METHOD, | ||||||
| CONF_NAME, | ||||||
| CONF_PORT, | ||||||
|
|
@@ -32,19 +34,15 @@ | |||||
| METHOD_WEBSOCKET, | ||||||
| RESULT_AUTH_MISSING, | ||||||
| RESULT_NOT_SUCCESSFUL, | ||||||
| RESULT_NOT_SUPPORTED, | ||||||
| RESULT_SUCCESS, | ||||||
| WEBSOCKET_PORTS, | ||||||
| ) | ||||||
|
|
||||||
| DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}) | ||||||
| SUPPORTED_METHODS = [METHOD_LEGACY, METHOD_WEBSOCKET] | ||||||
|
|
||||||
|
|
||||||
| def _get_ip(host): | ||||||
| if host is None: | ||||||
| return None | ||||||
| return socket.gethostbyname(host) | ||||||
|
|
||||||
|
|
||||||
| class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||||||
| """Handle a Samsung TV config flow.""" | ||||||
|
|
||||||
|
|
@@ -56,19 +54,19 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | |||||
| def __init__(self): | ||||||
| """Initialize flow.""" | ||||||
| self._host = None | ||||||
| self._ip = None | ||||||
| self._mac = None | ||||||
| self._manufacturer = None | ||||||
| self._model = None | ||||||
| self._name = None | ||||||
| self._title = None | ||||||
| self._id = None | ||||||
| self._bridge = None | ||||||
| self._device_info = None | ||||||
|
|
||||||
| def _get_entry(self): | ||||||
| data = { | ||||||
| CONF_HOST: self._host, | ||||||
| CONF_ID: self._id, | ||||||
| CONF_IP_ADDRESS: self._ip, | ||||||
| CONF_MAC: self._mac, | ||||||
| CONF_MANUFACTURER: self._manufacturer, | ||||||
| CONF_METHOD: self._bridge.method, | ||||||
| CONF_MODEL: self._model, | ||||||
|
|
@@ -77,17 +75,61 @@ def _get_entry(self): | |||||
| } | ||||||
| if self._bridge.token: | ||||||
| data[CONF_TOKEN] = self._bridge.token | ||||||
| return self.async_create_entry(title=self._title, data=data,) | ||||||
| return self.async_create_entry(title=self._title, data=data) | ||||||
|
|
||||||
| def _abort_if_already_configured(self): | ||||||
| device_ip = gethostbyname(self._host) | ||||||
| for entry in self._async_current_entries(): | ||||||
|
MartinHjelmare marked this conversation as resolved.
|
||||||
|
|
||||||
| # update user configured or unique_id=ip entries | ||||||
| if self._id and not entry.unique_id or device_ip == entry.unique_id: | ||||||
| data = { | ||||||
| key: value | ||||||
| for key, value in entry.data.items() | ||||||
| # clean up old entries | ||||||
| if key not in (CONF_ID, CONF_IP_ADDRESS) | ||||||
| } | ||||||
| if self._manufacturer and not data.get(CONF_MANUFACTURER): | ||||||
| data[CONF_MANUFACTURER] = self._manufacturer | ||||||
| if self._model and not data.get(CONF_MODEL): | ||||||
| data[CONF_MODEL] = self._model | ||||||
| self.hass.config_entries.async_update_entry( | ||||||
|
MartinHjelmare marked this conversation as resolved.
|
||||||
| entry, unique_id=self._id, data=data | ||||||
| ) | ||||||
| raise data_entry_flow.AbortFlow("already_configured") | ||||||
|
escoand marked this conversation as resolved.
|
||||||
|
|
||||||
| if ( | ||||||
| self._host == entry.data[CONF_HOST] | ||||||
| or (self._id and self._id == entry.unique_id) | ||||||
| or (self._mac and self._mac == entry.data.get(CONF_MAC)) | ||||||
| ): | ||||||
| raise data_entry_flow.AbortFlow("already_configured") | ||||||
|
|
||||||
| def _try_connect(self): | ||||||
| """Try to connect and check auth.""" | ||||||
| for method in SUPPORTED_METHODS: | ||||||
| self._bridge = SamsungTVBridge.get_bridge(method, self._host) | ||||||
| result = self._bridge.try_connect() | ||||||
| if result == RESULT_SUCCESS: | ||||||
| return | ||||||
| if result != RESULT_NOT_SUCCESSFUL: | ||||||
| return result | ||||||
| raise data_entry_flow.AbortFlow(result) | ||||||
|
escoand marked this conversation as resolved.
|
||||||
| LOGGER.debug("No working config found") | ||||||
| return RESULT_NOT_SUCCESSFUL | ||||||
| raise data_entry_flow.AbortFlow(RESULT_NOT_SUCCESSFUL) | ||||||
|
|
||||||
| def _get_and_check_device_info(self): | ||||||
| """Try to get the device info.""" | ||||||
| for port in WEBSOCKET_PORTS: | ||||||
| self._device_info = SamsungTVBridge.get_bridge( | ||||||
| METHOD_WEBSOCKET, self._host, port | ||||||
| ).device_info() | ||||||
| if self._device_info: | ||||||
| device_type = self._device_info.get("device", {}).get("type") | ||||||
| if device_type and device_type != "Samsung SmartTV": | ||||||
| raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) | ||||||
| self._model = self._device_info.get("device", {}).get("modelName") | ||||||
| return | ||||||
| raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) | ||||||
|
|
||||||
| async def async_step_import(self, user_input=None): | ||||||
| """Handle configuration by yaml file.""" | ||||||
|
|
@@ -96,81 +138,84 @@ async def async_step_import(self, user_input=None): | |||||
| async def async_step_user(self, user_input=None): | ||||||
| """Handle a flow initialized by the user.""" | ||||||
| if user_input is not None: | ||||||
| ip_address = await self.hass.async_add_executor_job( | ||||||
| _get_ip, user_input[CONF_HOST] | ||||||
| ) | ||||||
|
|
||||||
| await self.async_set_unique_id(ip_address) | ||||||
| self._abort_if_unique_id_configured() | ||||||
|
|
||||||
| self._host = user_input.get(CONF_HOST) | ||||||
| self._ip = self.context[CONF_IP_ADDRESS] = ip_address | ||||||
| self._name = user_input.get(CONF_NAME) | ||||||
| self._host = user_input[CONF_HOST] | ||||||
| self._name = user_input[CONF_NAME] | ||||||
| self._title = self._name | ||||||
|
|
||||||
| result = await self.hass.async_add_executor_job(self._try_connect) | ||||||
| self._abort_if_already_configured() | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where do we set the unique id for the flow in the user step?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that only done when discovering a flow, ssdp or zeroconf, and updating an existing entry created in a user step before aborting?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, exactly. In a user step we have no unique_id, especially on older legacy devices.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. We still need to guard from setting up the same device more than once in the user flow somehow. Can we at least check the existing host addresses and compare with the user input for host? |
||||||
|
|
||||||
| await self.hass.async_add_executor_job(self._try_connect) | ||||||
|
|
||||||
| if result != RESULT_SUCCESS: | ||||||
| return self.async_abort(reason=result) | ||||||
| return self._get_entry() | ||||||
|
|
||||||
| return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) | ||||||
|
|
||||||
| async def async_step_ssdp(self, discovery_info): | ||||||
| """Handle a flow initialized by discovery.""" | ||||||
| host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname | ||||||
| ip_address = await self.hass.async_add_executor_job(_get_ip, host) | ||||||
|
|
||||||
| self._host = host | ||||||
| self._ip = self.context[CONF_IP_ADDRESS] = ip_address | ||||||
| self._manufacturer = discovery_info.get(ATTR_UPNP_MANUFACTURER) | ||||||
| self._model = discovery_info.get(ATTR_UPNP_MODEL_NAME) | ||||||
| self._name = f"Samsung {self._model}" | ||||||
| self._id = discovery_info.get(ATTR_UPNP_UDN) | ||||||
| self._title = self._model | ||||||
| async def async_step_ssdp(self, user_input=None): | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| """Handle a flow initialized by ssdp discovery.""" | ||||||
| self._host = urlparse(user_input[ATTR_SSDP_LOCATION]).hostname | ||||||
| self._id = user_input.get(ATTR_UPNP_UDN) | ||||||
|
|
||||||
| # probably access denied | ||||||
| if self._id is None: | ||||||
| return self.async_abort(reason=RESULT_AUTH_MISSING) | ||||||
| if self._id.startswith("uuid:"): | ||||||
| self._id = self._id[5:] | ||||||
|
escoand marked this conversation as resolved.
|
||||||
|
|
||||||
| await self.async_set_unique_id(ip_address) | ||||||
| self._abort_if_unique_id_configured( | ||||||
| { | ||||||
| CONF_ID: self._id, | ||||||
| CONF_MANUFACTURER: self._manufacturer, | ||||||
| CONF_MODEL: self._model, | ||||||
| } | ||||||
| ) | ||||||
| await self.async_set_unique_id(self._id) | ||||||
| await self.hass.async_add_executor_job(self._get_and_check_device_info) | ||||||
|
|
||||||
| self._manufacturer = user_input.get(ATTR_UPNP_MANUFACTURER) | ||||||
| if not self._model: | ||||||
| self._model = user_input.get(ATTR_UPNP_MODEL_NAME) | ||||||
| self._name = f"{self._manufacturer} {self._model}" | ||||||
| self._title = self._model | ||||||
|
|
||||||
| await self.hass.async_add_executor_job(self._abort_if_already_configured()) | ||||||
|
escoand marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| self.context["title_placeholders"] = {"model": self._model} | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it ok to have model be |
||||||
| return await self.async_step_confirm() | ||||||
|
|
||||||
| async def async_step_zeroconf(self, user_input=None): | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| """Handle a flow initialized by zeroconf discovery.""" | ||||||
| self._host = user_input[CONF_HOST] | ||||||
| self._id = user_input[ATTR_PROPERTIES].get("serialNumber") | ||||||
|
|
||||||
| if self._id: | ||||||
| await self.async_set_unique_id(self._id) | ||||||
| await self.hass.async_add_executor_job(self._get_and_check_device_info) | ||||||
|
|
||||||
| self._mac = user_input[ATTR_PROPERTIES].get("deviceid") | ||||||
| self._manufacturer = user_input[ATTR_PROPERTIES].get("manufacturer") | ||||||
| if not self._model: | ||||||
| self._model = user_input[ATTR_PROPERTIES].get("model") | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use |
||||||
| self._name = f"{self._manufacturer} {self._model}" | ||||||
| self._title = self._model | ||||||
|
|
||||||
| await self.hass.async_add_executor_job(self._abort_if_already_configured()) | ||||||
|
|
||||||
| self.context["title_placeholders"] = {"model": self._model} | ||||||
| return await self.async_step_confirm() | ||||||
|
|
||||||
| async def async_step_confirm(self, user_input=None): | ||||||
| """Handle user-confirmation of discovered node.""" | ||||||
| if user_input is not None: | ||||||
| result = await self.hass.async_add_executor_job(self._try_connect) | ||||||
| await self.hass.async_add_executor_job(self._try_connect) | ||||||
|
|
||||||
| if result != RESULT_SUCCESS: | ||||||
| return self.async_abort(reason=result) | ||||||
| return self._get_entry() | ||||||
|
|
||||||
| return self.async_show_form( | ||||||
| step_id="confirm", description_placeholders={"model": self._model} | ||||||
| ) | ||||||
|
|
||||||
| async def async_step_reauth(self, user_input=None): | ||||||
| async def async_step_reauth(self, user_input): | ||||||
| """Handle configuration by re-auth.""" | ||||||
| self._host = user_input[CONF_HOST] | ||||||
| self._id = user_input.get(CONF_ID) | ||||||
| self._ip = user_input[CONF_IP_ADDRESS] | ||||||
| self._manufacturer = user_input.get(CONF_MANUFACTURER) | ||||||
| self._model = user_input.get(CONF_MODEL) | ||||||
| self._name = user_input.get(CONF_NAME) | ||||||
| self._title = self._model or self._name | ||||||
|
|
||||||
| await self.async_set_unique_id(self._ip) | ||||||
| await self.async_set_unique_id(self.unique_id) | ||||||
| self.context["title_placeholders"] = {"model": self._title} | ||||||
|
|
||||||
| return await self.async_step_confirm() | ||||||
Uh oh!
There was an error while loading. Please reload this page.