From e6b86b662ad8d3c7cc7a4ccd213e490060e5b82b Mon Sep 17 00:00:00 2001 From: RJPoelstra <36924801+RJPoelstra@users.noreply.github.com> Date: Mon, 16 Sep 2024 11:56:13 +0200 Subject: [PATCH] Add reconnect logic and proper reporting to MotionMount integration (#125670) * Add reconnect logic and proper reporting * Use snake_case * Log on warning, not on info * Reduce line length * Refactor non-raising code out of try blocks * Remove `_ensure_connected()` from action functions --- .../components/motionmount/entity.py | 24 ++++++++++++++ .../components/motionmount/number.py | 13 ++++++-- .../components/motionmount/select.py | 31 ++++++++++++++----- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/motionmount/entity.py b/homeassistant/components/motionmount/entity.py index d2da2481f1a..ba81c9d10bd 100644 --- a/homeassistant/components/motionmount/entity.py +++ b/homeassistant/components/motionmount/entity.py @@ -1,5 +1,7 @@ """Support for MotionMount sensors.""" +import logging +import socket from typing import TYPE_CHECKING import motionmount @@ -12,6 +14,8 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN, EMPTY_MAC +_LOGGER = logging.getLogger(__name__) + class MotionMountEntity(Entity): """Representation of a MotionMount entity.""" @@ -70,3 +74,23 @@ class MotionMountEntity(Entity): self.mm.remove_listener(self.async_write_ha_state) self.mm.remove_listener(self.update_name) await super().async_will_remove_from_hass() + + async def _ensure_connected(self) -> bool: + """Make sure there is a connection with the MotionMount. + + Returns false if the connection failed to be ensured. + """ + + if self.mm.is_connected: + return True + try: + await self.mm.connect() + except (ConnectionError, TimeoutError, socket.gaierror): + # We're not interested in exceptions here. In case of a failed connection + # the try/except from the caller will report it. + # The purpose of `_ensure_connected()` is only to make sure we try to + # reconnect, where failures should not be logged each time + return False + else: + _LOGGER.warning("Successfully reconnected to MotionMount") + return True diff --git a/homeassistant/components/motionmount/number.py b/homeassistant/components/motionmount/number.py index 3217a4558e1..25370ec51d8 100644 --- a/homeassistant/components/motionmount/number.py +++ b/homeassistant/components/motionmount/number.py @@ -1,11 +1,14 @@ """Support for MotionMount numeric control.""" +import socket + import motionmount from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -46,7 +49,10 @@ class MotionMountExtension(MotionMountEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set the new value for extension.""" - await self.mm.set_extension(int(value)) + try: + await self.mm.set_extension(int(value)) + except (TimeoutError, socket.gaierror) as ex: + raise HomeAssistantError("Failed to communicate with MotionMount") from ex class MotionMountTurn(MotionMountEntity, NumberEntity): @@ -69,4 +75,7 @@ class MotionMountTurn(MotionMountEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set the new value for turn.""" - await self.mm.set_turn(int(value * -1)) + try: + await self.mm.set_turn(int(value * -1)) + except (TimeoutError, socket.gaierror) as ex: + raise HomeAssistantError("Failed to communicate with MotionMount") from ex diff --git a/homeassistant/components/motionmount/select.py b/homeassistant/components/motionmount/select.py index d15bbb7326b..9bca6578bcc 100644 --- a/homeassistant/components/motionmount/select.py +++ b/homeassistant/components/motionmount/select.py @@ -1,15 +1,23 @@ """Support for MotionMount numeric control.""" +from datetime import timedelta +import logging +import socket + import motionmount from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, WALL_PRESET_NAME from .entity import MotionMountEntity +_LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=60) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -23,6 +31,7 @@ async def async_setup_entry( class MotionMountPresets(MotionMountEntity, SelectEntity): """The presets of a MotionMount.""" + _attr_should_poll = True _attr_translation_key = "motionmount_preset" def __init__( @@ -44,8 +53,15 @@ class MotionMountPresets(MotionMountEntity, SelectEntity): async def async_update(self) -> None: """Get latest state from MotionMount.""" - self._presets = await self.mm.get_presets() - self._update_options(self._presets) + if not await self._ensure_connected(): + return + + try: + self._presets = await self.mm.get_presets() + except (TimeoutError, socket.gaierror) as ex: + _LOGGER.warning("Failed to communicate with MotionMount: %s", ex) + else: + self._update_options(self._presets) @property def current_option(self) -> str | None: @@ -72,8 +88,9 @@ class MotionMountPresets(MotionMountEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Set the new option.""" index = int(option[:1]) - await self.mm.go_to_preset(index) - self._attr_current_option = option - - # Perform an update so we detect changes to the presets (changes are not pushed) - self.async_schedule_update_ha_state(True) + try: + await self.mm.go_to_preset(index) + except (TimeoutError, socket.gaierror) as ex: + raise HomeAssistantError("Failed to communicate with MotionMount") from ex + else: + self._attr_current_option = option