Skip to content

Commit

Permalink
Merge pull request #25 from uvjim/discovery_fixup
Browse files Browse the repository at this point in the history
Discovery fixup
  • Loading branch information
uvjim authored May 2, 2022
2 parents d3c3f71 + d6ea9a9 commit c6c57ec
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 131 deletions.
12 changes: 7 additions & 5 deletions custom_components/hdhomerun/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
)

# region #-- set up the coordinators --#
async def _async_data_coordinator_update() -> HDHomeRunDevice:
async def _async_data_coordinator_update() -> bool:
""""""

try:
await hass.data[DOMAIN][config_entry.entry_id][CONF_DEVICE].async_rediscover()
hass.data[DOMAIN][config_entry.entry_id][CONF_DEVICE] = (
await hass.data[DOMAIN][config_entry.entry_id][CONF_DEVICE].async_rediscover()
)
except Exception as exc:
_LOGGER.warning(log_formatter.message_format("%s"), exc)
raise UpdateFailed(str(exc))

return hass.data[DOMAIN][config_entry.entry_id][CONF_DEVICE]
return True

async def _async_data_coordinator_tuner_status_update() -> HDHomeRunDevice:
async def _async_data_coordinator_tuner_status_update() -> bool:
""""""

previous_availability: bool = hass.data[DOMAIN][config_entry.entry_id][CONF_DEVICE].online
Expand All @@ -102,7 +104,7 @@ async def _async_data_coordinator_tuner_status_update() -> HDHomeRunDevice:
_LOGGER.debug(log_formatter.message_format("sending availability signal"))
async_dispatcher_send(hass, SIGNAL_HDHOMERUN_DEVICE_AVAILABILITY)

return hass.data[DOMAIN][config_entry.entry_id][CONF_DEVICE]
return True

# noinspection DuplicatedCode
coordinator_general: DataUpdateCoordinator = DataUpdateCoordinator(
Expand Down
6 changes: 4 additions & 2 deletions custom_components/hdhomerun/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ async def main():
_LOGGER.debug("entered")

discovery = Discover(mode=DiscoverMode.AUTO)
device = HDHomeRunDevice(host="192.168.123.72")
await discovery.rediscover(target=device)
device = HDHomeRunDevice(host="192.168.123.1")
device = await discovery.rediscover(target=device)
# device = await device.async_rediscover()
_LOGGER.debug("main, %s", device.__dict__)
devices: List[HDHomeRunDevice] = [device]
# devices: List[HDHomeRunDevice] = await discovery.discover(broadcast_address="192.168.123.255")
for dev in devices:
Expand Down
5 changes: 4 additions & 1 deletion custom_components/hdhomerun/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from .pyhdhr import HDHomeRunDevice
from .pyhdhr.discover import Discover
from .pyhdhr.exceptions import (
HDHomeRunDeviceNotFoundError,
HDHomeRunError,
HDHomeRunTimeoutError,
)
Expand Down Expand Up @@ -173,7 +174,9 @@ async def _async_task_discover_single(self) -> None:
if self._host:
hdhomerun_device: HDHomeRunDevice = HDHomeRunDevice(host=self._host)
try:
await Discover.rediscover(target=hdhomerun_device)
hdhomerun_device = await Discover.rediscover(target=hdhomerun_device)
if not hdhomerun_device.online:
raise HDHomeRunDeviceNotFoundError(device=hdhomerun_device.ip)
except HDHomeRunError as err:
if type(err) == HDHomeRunTimeoutError:
err_msg = "timeout_error"
Expand Down
68 changes: 60 additions & 8 deletions custom_components/hdhomerun/pyhdhr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ def __init__(self, host: str) -> None:
self._host: str = host

self._created_session: bool = False
self._http_discovery_attempted: bool = False
self._session: Optional[aiohttp.ClientSession] = None

self._available_firmware: Optional[str] = None
Expand Down Expand Up @@ -106,6 +105,7 @@ async def _async_tuner_refresh_http(self, timeout: Optional[float] = 2.5) -> Non
resp_json = await resp.json()
self._tuner_status = resp_json

# noinspection PyUnresolvedReferences
async def _async_tuner_refresh_tcp(self) -> None:
"""Refresh the tuner information using the TCP control protocol
Expand All @@ -120,10 +120,16 @@ async def _async_tuner_refresh_tcp(self) -> None:
proto.get_tuner_status(tuner_idx=idx)
for idx in range(self.tuner_count)
]
tuner_status = await asyncio.gather(*tuners)
tuner_status_info = await asyncio.gather(*tuners)

# -- process all tuners --#
for tuner in tuner_status:
tuner_status: List[Dict[str, str]] = []
for tuner in tuner_status_info:
if tuner is None:
self._is_online = False
continue

self._is_online = True
key = tuner.get("data", {})[HDHOMERUN_TAG_GETSET_NAME].decode().rstrip("\0")
val = tuner.get("data", {})[HDHOMERUN_TAG_GETSET_VALUE].decode().rstrip("\0")
tuner_info: Dict[str, int | str] = {
Expand Down Expand Up @@ -169,9 +175,12 @@ async def _async_tuner_refresh_tcp(self) -> None:
urlparse(tuner_target.get("data", {})[HDHOMERUN_TAG_GETSET_VALUE]).hostname.decode()
)

if not self._tuner_status:
self._tuner_status = []
self._tuner_status.append(tuner_info)
if not tuner_status:
tuner_status = []
tuner_status.append(tuner_info)

if tuner_status:
self._tuner_status = tuner_status

async def async_tuner_refresh(self, timeout: Optional[float] = 2.5) -> None:
"""Genric function refreshing tuners
Expand All @@ -187,14 +196,57 @@ async def async_tuner_refresh(self, timeout: Optional[float] = 2.5) -> None:
else:
await self._async_tuner_refresh_tcp()

async def async_rediscover(self) -> None:
async def async_rediscover(self, timeout: Optional[float] = 2.5) -> HDHomeRunDevice:
"""Refresh the information for a device
:param timeout: timeout for the query
:return: None
"""

tcp_property_map: Dict[str, str] = {
"/sys/version": "_sys_version",
"/sys/model": "_sys_model",
"/sys/hwmodel": "_sys_hwmodel",
}

from .discover import Discover # late import for Discover to avoid a circular import
await Discover.rediscover(target=self)
device: HDHomeRunDevice = await Discover.rediscover(target=self)
if getattr(device, "_discover_url", None) is None and device.online:
proto: HDHomeRunProtocol = HDHomeRunProtocol(host=self.ip)
supplemental_info = [
proto.get_version(),
proto.get_model(),
proto.get_hwmodel(),
]
info = await asyncio.gather(*supplemental_info)
prop: Dict[str, Dict[int | str, bytes]]
for prop in info:
tcp_prop_name = prop.get("data", {})[HDHOMERUN_TAG_GETSET_NAME].decode().rstrip("\0")
prop_value = prop.get("data", {})[HDHOMERUN_TAG_GETSET_VALUE].decode().rstrip("\0")
if (prop_name := tcp_property_map.get(tcp_prop_name, None)) is not None:
setattr(device, prop_name, prop_value)

# region #-- get the channels from the lineup_url --#
if device.lineup_url and device.online:
if self._session is None:
self._created_session: bool = True
self._session = aiohttp.ClientSession()

try:
response = await self._session.get(url=device.lineup_url, timeout=timeout, raise_for_status=True)
except asyncio.TimeoutError:
setattr(device, "_is_online", False)
_LOGGER.error("Timeout experienced reaching %s", device.lineup_url)
except aiohttp.ClientConnectorError:
setattr(device, "_is_online", False)
except Exception as err:
raise err from None
else:
resp_json = await response.json()
setattr(device, "_channels", resp_json)
# endregion

return device

# region #-- properties --#
@property
Expand Down
115 changes: 37 additions & 78 deletions custom_components/hdhomerun/pyhdhr/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@
HDHOMERUN_DISCOVER_UDP_PORT,
)
from .exceptions import (
HDHomeRunConnectionError,
HDHomeRunError,
HDHomeRunHTTPDiscoveryNotAvailableError,
HDHomeRunUDPDiscoveryDeviceNotFoundError,
HDHomeRunTimeoutError,
)
from .protocol import HDHomeRunProtocol

Expand Down Expand Up @@ -71,9 +67,7 @@ async def discover(self, broadcast_address: Optional[str] = "255.255.255.255") -
N.B. when the mode is set to AUTO HTTP is discovery is attempted first and then UDP.
The device lists are merged with the settings from UDP winning if they are not None.
A request is then made to the DiscoverURL to attempt to get more friendly information.
This request is made irrespective of whetherthe device has a registered URL or not
(one is built if there isn't one - UDP discovery does not tell us about the discover_url).
A request is then made to the discover_url to attempt to get more friendly information.
:param broadcast_address: the address to broadcast to when using the UDP protocol
:return: a list of device objects for those found
Expand All @@ -83,11 +77,16 @@ async def discover(self, broadcast_address: Optional[str] = "255.255.255.255") -

# region #-- get the devices from HTTP --#
if self._mode in (DiscoverMode.AUTO, DiscoverMode.HTTP):
for dev in (await DiscoverHTTP.discover()):
devices[dev.device_id] = dev
try:
discovered_devices: List[HDHomeRunDevice] = await DiscoverHTTP.discover()
except HDHomeRunHTTPDiscoveryNotAvailableError:
pass
else:
for dev in discovered_devices:
devices[dev.device_id] = dev
# endregion

# region #-- get the devices from UDP --#
# # region #-- get the devices from UDP --#
if self._mode in (DiscoverMode.AUTO, DiscoverMode.UDP):
discovered_devices: List[HDHomeRunDevice] = await DiscoverUDP.discover(target=broadcast_address)
for dev in discovered_devices:
Expand All @@ -98,38 +97,35 @@ async def discover(self, broadcast_address: Optional[str] = "255.255.255.255") -
# noinspection PyRedundantParentheses
if (property_value := getattr(dev, property_name, None)):
setattr(devices.get(dev.device_id), property_name, property_value)
# endregion
# # endregion

# region #-- try and get the details from HTTP - just in case --#
# region #-- try and rediscover via HTTP to get further details (UDP won't offer any more) --#
for device_id, dev in devices.items():
try:
await DiscoverHTTP.rediscover(target=dev)
except HDHomeRunHTTPDiscoveryNotAvailableError:
pass
await DiscoverHTTP.rediscover(target=dev)
# endregion

return list(devices.values())

@staticmethod
async def rediscover(target: HDHomeRunDevice) -> None:
async def rediscover(target: HDHomeRunDevice) -> HDHomeRunDevice:
"""Get updated information for the given target
`target` is update in place with the latest information
:param target: the device to refresh information for
:return: None
:return: the updated device
"""

try:
await DiscoverHTTP.rediscover(target=target)
except (HDHomeRunHTTPDiscoveryNotAvailableError, HDHomeRunTimeoutError):
result = await DiscoverUDP.rediscover(target=target.ip)
if len(result) == 0:
raise HDHomeRunUDPDiscoveryDeviceNotFoundError(device=target.ip)
# noinspection PyUnusedLocal
target = result[0] # refresh the target
except Exception as err:
raise err from None
if getattr(target, "_discover_url", None) is not None:
device: HDHomeRunDevice = await DiscoverHTTP.rediscover(target=target)
else:
device: List[HDHomeRunDevice] = await DiscoverUDP.rediscover(target=target.ip)

if not device:
setattr(target, "_is_online", False)
return target
else:
ret = device[0] if isinstance(device, List) else device
setattr(ret, "_is_online", True)
return ret


class DiscoverHTTP:
Expand Down Expand Up @@ -175,14 +171,9 @@ async def discover(

try:
response = await session.get(url=discover_url, timeout=timeout, raise_for_status=True)
except asyncio.TimeoutError:
raise HDHomeRunTimeoutError(device=urlparse(discover_url).hostname) from None
except aiohttp.ClientConnectorError:
raise HDHomeRunConnectionError(device=urlparse(discover_url).hostname) from None
except aiohttp.ClientResponseError:
raise HDHomeRunHTTPDiscoveryNotAvailableError(device=urlparse(discover_url).hostname) from None
except Exception as err:
raise HDHomeRunError(device=urlparse(discover_url).hostname, message=str(err)) from None
_LOGGER.debug("Error in HTTP discovery, %s: %s", type(err), err)
raise HDHomeRunHTTPDiscoveryNotAvailableError(device=urlparse(discover_url).hostname) from None
else:
resp_json = await response.json()
if resp_json: # we didn't just get an empty list or dictionary
Expand All @@ -207,71 +198,39 @@ async def rediscover(
target: HDHomeRunDevice,
session: Optional[aiohttp.ClientSession] = None,
timeout: float = 2.5
) -> None:
) -> HDHomeRunDevice:
"""Gather updated information about a device
N.B. the discover_url will be used if available. If not, one is built (UDP discovered devices
won't have one). If the discovery attempt fails it is marked in the object so that we don't
try again during the object's lifetime (no point keep trying if it isn't there).
won't have one).
:param target: the device to refresh information for
:param session: existing session
:param timeout: timeout for the query
:return:
:return: the updated device
"""

_LOGGER.debug("entered, args: %s", locals())

discover_url: str = getattr(target, "_discover_url", None)
if discover_url is None and not getattr(target, "_http_discovery_attempted", False):
_LOGGER.debug("discover_url, %s", discover_url)
if discover_url is None:
# noinspection HttpUrlsUsage
discover_url = f"http://{target.ip}/discover.json"

if not discover_url:
raise HDHomeRunHTTPDiscoveryNotAvailableError(device=target.ip)

try:
updated_device = await DiscoverHTTP.discover(discover_url=discover_url, session=session, timeout=timeout)
except HDHomeRunTimeoutError:
setattr(target, "_is_online", False)
except HDHomeRunHTTPDiscoveryNotAvailableError as err: # flag to not try again
setattr(target, "_http_discovery_attempted", True)
raise err from None
except HDHomeRunHTTPDiscoveryNotAvailableError:
pass
else:
setattr(target, "_discover_url", discover_url)
setattr(target, "_http_discovery_attempted", False)
setattr(target, "_is_online", True)
updated_device = updated_device[0]
for json_property_name, property_name in DiscoverHTTP.JSON_PROPERTIES_MAP.items(): # set the properties
# noinspection PyRedundantParentheses
if (property_value := getattr(updated_device, property_name, None)):
if (property_value := getattr(updated_device, property_name, None)) is not None:
setattr(target, property_name, property_value)

# region #-- get the channels from the lineup_url --#
if target.lineup_url is not None and target.online:
created_session: bool = False
if session is None:
created_session = True
session = aiohttp.ClientSession()

try:
response = await session.get(url=target.lineup_url, timeout=timeout, raise_for_status=True)
except asyncio.TimeoutError:
setattr(target, "_is_online", False)
_LOGGER.error("Timeout experienced reaching %s", target.lineup_url)
except aiohttp.ClientConnectorError as err:
setattr(target, "_is_online", False)
except Exception as err:
raise err from None
else:
resp_json = await response.json()
setattr(target, "_channels", resp_json)
finally:
if created_session:
await session.close()
# endregion

_LOGGER.debug("exited")
return target


class DiscoverUDP:
Expand Down
Loading

0 comments on commit c6c57ec

Please sign in to comment.