diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index b60de49044d..508851e6dad 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -1,4 +1,5 @@ """The motion_blinds component.""" +import asyncio from datetime import timedelta import logging from socket import timeout @@ -20,6 +21,7 @@ from .const import ( DEFAULT_INTERFACE, DEFAULT_WAIT_FOR_PUSH, DOMAIN, + KEY_API_LOCK, KEY_COORDINATOR, KEY_GATEWAY, KEY_MULTICAST_LISTENER, @@ -56,6 +58,7 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): update_interval=update_interval, ) + self.api_lock = coordinator_info[KEY_API_LOCK] self._gateway = coordinator_info[KEY_GATEWAY] self._wait_for_push = coordinator_info[CONF_WAIT_FOR_PUSH] @@ -88,7 +91,8 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): async def _async_update_data(self): """Fetch the latest data from the gateway and blinds.""" - data = await self.hass.async_add_executor_job(self.update_gateway) + async with self.api_lock: + data = await self.hass.async_add_executor_job(self.update_gateway) all_available = all(device[ATTR_AVAILABLE] for device in data.values()) if all_available: @@ -130,8 +134,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not await connect_gateway_class.async_connect_gateway(host, key): raise ConfigEntryNotReady motion_gateway = connect_gateway_class.gateway_device + api_lock = asyncio.Lock() coordinator_info = { KEY_GATEWAY: motion_gateway, + KEY_API_LOCK: api_lock, CONF_WAIT_FOR_PUSH: wait_for_push, } diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index c9346a3b86e..a35aeb6cd89 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -13,6 +13,7 @@ DEFAULT_WAIT_FOR_PUSH = False DEFAULT_INTERFACE = "any" KEY_GATEWAY = "gateway" +KEY_API_LOCK = "api_lock" KEY_COORDINATOR = "coordinator" KEY_MULTICAST_LISTENER = "multicast_listener" KEY_VERSION = "version" diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 07024a9d1fc..80f5b4d60c4 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -1,5 +1,4 @@ """Support for Motion Blinds using their WLAN API.""" -from datetime import timedelta import logging from motionblinds import DEVICE_TYPES_WIFI, BlindType @@ -20,9 +19,8 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_point_in_time, track_point_in_time +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util import dt as dt_util from .const import ( ATTR_ABSOLUTE_POSITION, @@ -94,7 +92,6 @@ async def async_setup_entry( coordinator, blind, POSITION_DEVICE_MAP[blind.type], - config_entry, sw_version, ) ) @@ -105,7 +102,6 @@ async def async_setup_entry( coordinator, blind, TILT_DEVICE_MAP[blind.type], - config_entry, sw_version, ) ) @@ -116,7 +112,6 @@ async def async_setup_entry( coordinator, blind, TDBU_DEVICE_MAP[blind.type], - config_entry, sw_version, "Top", ) @@ -126,7 +121,6 @@ async def async_setup_entry( coordinator, blind, TDBU_DEVICE_MAP[blind.type], - config_entry, sw_version, "Bottom", ) @@ -136,7 +130,6 @@ async def async_setup_entry( coordinator, blind, TDBU_DEVICE_MAP[blind.type], - config_entry, sw_version, "Combined", ) @@ -152,7 +145,6 @@ async def async_setup_entry( coordinator, blind, POSITION_DEVICE_MAP[BlindType.RollerBlind], - config_entry, sw_version, ) ) @@ -170,12 +162,12 @@ async def async_setup_entry( class MotionPositionDevice(CoordinatorEntity, CoverEntity): """Representation of a Motion Blind Device.""" - def __init__(self, coordinator, blind, device_class, config_entry, sw_version): + def __init__(self, coordinator, blind, device_class, sw_version): """Initialize the blind.""" super().__init__(coordinator) self._blind = blind - self._config_entry = config_entry + self._api_lock = coordinator.api_lock self._requesting_position = False self._previous_positions = [] @@ -249,7 +241,9 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): if len(self._previous_positions) > 2: del self._previous_positions[: len(self._previous_positions) - 2] - await self.hass.async_add_executor_job(self._blind.Update_trigger) + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Update_trigger) + self.async_write_ha_state() if len(self._previous_positions) < 2 or not all( @@ -257,53 +251,58 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): for prev_position in self._previous_positions ): # keep updating the position @UPDATE_INTERVAL_MOVING until the position does not change. - async_track_point_in_time( - self.hass, - self.async_scheduled_update_request, - dt_util.utcnow() + timedelta(seconds=UPDATE_INTERVAL_MOVING), + async_call_later( + self.hass, UPDATE_INTERVAL_MOVING, self.async_scheduled_update_request ) else: self._previous_positions = [] self._requesting_position = False - def request_position_till_stop(self): + async def async_request_position_till_stop(self): """Request the position of the blind every UPDATE_INTERVAL_MOVING seconds until it stops moving.""" self._previous_positions = [] if self._requesting_position or self.current_cover_position is None: return self._requesting_position = True - track_point_in_time( - self.hass, - self.async_scheduled_update_request, - dt_util.utcnow() + timedelta(seconds=UPDATE_INTERVAL_MOVING), + async_call_later( + self.hass, UPDATE_INTERVAL_MOVING, self.async_scheduled_update_request ) - def open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Open the cover.""" - self._blind.Open() - self.request_position_till_stop() + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Open) + await self.async_request_position_till_stop() - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Close cover.""" - self._blind.Close() - self.request_position_till_stop() + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Close) + await self.async_request_position_till_stop() - def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] - self._blind.Set_position(100 - position) - self.request_position_till_stop() + async with self._api_lock: + await self.hass.async_add_executor_job( + self._blind.Set_position, 100 - position + ) + await self.async_request_position_till_stop() - def set_absolute_position(self, **kwargs): + async def async_set_absolute_position(self, **kwargs): """Move the cover to a specific absolute position (see TDBU).""" position = kwargs[ATTR_ABSOLUTE_POSITION] - self._blind.Set_position(100 - position) - self.request_position_till_stop() + async with self._api_lock: + await self.hass.async_add_executor_job( + self._blind.Set_position, 100 - position + ) + await self.async_request_position_till_stop() - def stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Stop the cover.""" - self._blind.Stop() + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Stop) class MotionTiltDevice(MotionPositionDevice): @@ -320,32 +319,34 @@ class MotionTiltDevice(MotionPositionDevice): return None return self._blind.angle * 100 / 180 - def open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs): """Open the cover tilt.""" - self._blind.Set_angle(180) + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Set_angle, 180) - def close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs): """Close the cover tilt.""" - self._blind.Set_angle(0) + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Set_angle, 0) - def set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" angle = kwargs[ATTR_TILT_POSITION] * 180 / 100 - self._blind.Set_angle(angle) + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Set_angle, angle) - def stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs): """Stop the cover.""" - self._blind.Stop() + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Stop) class MotionTDBUDevice(MotionPositionDevice): """Representation of a Motion Top Down Bottom Up blind Device.""" - def __init__( - self, coordinator, blind, device_class, config_entry, sw_version, motor - ): + def __init__(self, coordinator, blind, device_class, sw_version, motor): """Initialize the blind.""" - super().__init__(coordinator, blind, device_class, config_entry, sw_version) + super().__init__(coordinator, blind, device_class, sw_version) self._motor = motor self._motor_key = motor[0] self._attr_name = f"{blind.blind_type}-{motor}-{blind.mac[12:]}" @@ -389,33 +390,40 @@ class MotionTDBUDevice(MotionPositionDevice): attributes[ATTR_WIDTH] = self._blind.width return attributes - def open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Open the cover.""" - self._blind.Open(motor=self._motor_key) - self.request_position_till_stop() + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Open, self._motor_key) + await self.async_request_position_till_stop() - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Close cover.""" - self._blind.Close(motor=self._motor_key) - self.request_position_till_stop() + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Close, self._motor_key) + await self.async_request_position_till_stop() - def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific scaled position.""" position = kwargs[ATTR_POSITION] - self._blind.Set_scaled_position(100 - position, motor=self._motor_key) - self.request_position_till_stop() + async with self._api_lock: + await self.hass.async_add_executor_job( + self._blind.Set_scaled_position, 100 - position, self._motor_key + ) + await self.async_request_position_till_stop() - def set_absolute_position(self, **kwargs): + async def async_set_absolute_position(self, **kwargs): """Move the cover to a specific absolute position.""" position = kwargs[ATTR_ABSOLUTE_POSITION] target_width = kwargs.get(ATTR_WIDTH, None) - self._blind.Set_position( - 100 - position, motor=self._motor_key, width=target_width - ) + async with self._api_lock: + await self.hass.async_add_executor_job( + self._blind.Set_position, 100 - position, self._motor_key, target_width + ) - self.request_position_till_stop() + await self.async_request_position_till_stop() - def stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Stop the cover.""" - self._blind.Stop(motor=self._motor_key) + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Stop, self._motor_key)