diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 0059d655767..68fbd4bd584 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -1,17 +1,14 @@ """Support for Switchbot devices.""" -from asyncio import Lock -import switchbot # pylint: disable=import-error +import switchbot from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SENSOR_TYPE, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from .const import ( ATTR_BOT, ATTR_CURTAIN, - BTLE_LOCK, COMMON_OPTIONS, CONF_RETRY_COUNT, CONF_RETRY_TIMEOUT, @@ -50,12 +47,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Uses BTLE advertisement data, all Switchbot devices in range is stored here. if DATA_COORDINATOR not in hass.data[DOMAIN]: - # Check if asyncio.lock is stored in hass data. - # BTLE has issues with multiple connections, - # so we use a lock to ensure that only one API request is reaching it at a time: - if BTLE_LOCK not in hass.data[DOMAIN]: - hass.data[DOMAIN][BTLE_LOCK] = Lock() - if COMMON_OPTIONS not in hass.data[DOMAIN]: hass.data[DOMAIN][COMMON_OPTIONS] = {**entry.options} @@ -72,7 +63,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api=switchbot, retry_count=hass.data[DOMAIN][COMMON_OPTIONS][CONF_RETRY_COUNT], scan_timeout=hass.data[DOMAIN][COMMON_OPTIONS][CONF_SCAN_TIMEOUT], - api_lock=hass.data[DOMAIN][BTLE_LOCK], ) hass.data[DOMAIN][DATA_COORDINATOR] = coordinator @@ -82,9 +72,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady - entry.async_on_unload(entry.add_update_listener(_async_update_listener)) hass.data[DOMAIN][entry.entry_id] = {DATA_COORDINATOR: coordinator} diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index c3f88e924ea..e2a5a951d1d 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -33,8 +34,8 @@ async def async_setup_entry( DATA_COORDINATOR ] - if not coordinator.data[entry.unique_id].get("data"): - return + if not coordinator.data.get(entry.unique_id): + raise PlatformNotReady async_add_entities( [ diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 70e032414a7..362f3b01ae7 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -1,11 +1,10 @@ """Config flow for Switchbot.""" from __future__ import annotations -from asyncio import Lock import logging from typing import Any -from switchbot import GetSwitchbotDevices # pylint: disable=import-error +from switchbot import GetSwitchbotDevices import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow @@ -14,7 +13,6 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from .const import ( - BTLE_LOCK, CONF_RETRY_COUNT, CONF_RETRY_TIMEOUT, CONF_SCAN_TIMEOUT, @@ -30,10 +28,10 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def _btle_connect() -> dict: +async def _btle_connect() -> dict: """Scan for BTLE advertisement data.""" - switchbot_devices = GetSwitchbotDevices().discover() + switchbot_devices = await GetSwitchbotDevices().discover() if not switchbot_devices: raise NotConnectedError("Failed to discover switchbot") @@ -52,14 +50,9 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): # store asyncio.lock in hass data if not present. if DOMAIN not in self.hass.data: self.hass.data.setdefault(DOMAIN, {}) - if BTLE_LOCK not in self.hass.data[DOMAIN]: - self.hass.data[DOMAIN][BTLE_LOCK] = Lock() - - connect_lock = self.hass.data[DOMAIN][BTLE_LOCK] # Discover switchbots nearby. - async with connect_lock: - _btle_adv_data = await self.hass.async_add_executor_job(_btle_connect) + _btle_adv_data = await _btle_connect() return _btle_adv_data diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 8ca7fadf41c..b1587e97c10 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -22,5 +22,4 @@ CONF_SCAN_TIMEOUT = "scan_timeout" # Data DATA_COORDINATOR = "coordinator" -BTLE_LOCK = "btle_lock" COMMON_OPTIONS = "common_options" diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index e901cc539ea..e8e2e240dc6 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -1,11 +1,10 @@ """Provides the switchbot DataUpdateCoordinator.""" from __future__ import annotations -from asyncio import Lock from datetime import timedelta import logging -import switchbot # pylint: disable=import-error +import switchbot from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -26,7 +25,6 @@ class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): api: switchbot, retry_count: int, scan_timeout: int, - api_lock: Lock, ) -> None: """Initialize global switchbot data updater.""" self.switchbot_api = api @@ -39,20 +37,12 @@ class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, name=DOMAIN, update_interval=self.update_interval ) - self.api_lock = api_lock - - def _update_data(self) -> dict | None: - """Fetch device states from switchbot api.""" - - return self.switchbot_data.discover( - retry=self.retry_count, scan_timeout=self.scan_timeout - ) - async def _async_update_data(self) -> dict | None: """Fetch data from switchbot.""" - async with self.api_lock: - switchbot_data = await self.hass.async_add_executor_job(self._update_data) + switchbot_data = await self.switchbot_data.discover( + retry=self.retry_count, scan_timeout=self.scan_timeout + ) if not switchbot_data: raise UpdateFailed("Unable to fetch switchbot services data") diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 9f265d696ad..9223217c173 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from switchbot import SwitchbotCurtain # pylint: disable=import-error +from switchbot import SwitchbotCurtain from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, @@ -16,6 +16,7 @@ from homeassistant.components.cover import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -36,6 +37,9 @@ async def async_setup_entry( DATA_COORDINATOR ] + if not coordinator.data.get(entry.unique_id): + raise PlatformNotReady + async_add_entities( [ SwitchBotCurtainEntity( @@ -94,44 +98,30 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Open the curtain.""" _LOGGER.debug("Switchbot to open curtain %s", self._mac) - - async with self.coordinator.api_lock: - self._last_run_success = bool( - await self.hass.async_add_executor_job(self._device.open) - ) + self._last_run_success = bool(await self._device.open()) + self.async_write_ha_state() async def async_close_cover(self, **kwargs: Any) -> None: """Close the curtain.""" _LOGGER.debug("Switchbot to close the curtain %s", self._mac) - - async with self.coordinator.api_lock: - self._last_run_success = bool( - await self.hass.async_add_executor_job(self._device.close) - ) + self._last_run_success = bool(await self._device.close()) + self.async_write_ha_state() async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the moving of this device.""" _LOGGER.debug("Switchbot to stop %s", self._mac) - - async with self.coordinator.api_lock: - self._last_run_success = bool( - await self.hass.async_add_executor_job(self._device.stop) - ) + self._last_run_success = bool(await self._device.stop()) + self.async_write_ha_state() async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover shutter to a specific position.""" position = kwargs.get(ATTR_POSITION) _LOGGER.debug("Switchbot to move at %d %s", position, self._mac) - - async with self.coordinator.api_lock: - self._last_run_success = bool( - await self.hass.async_add_executor_job( - self._device.set_position, position - ) - ) + self._last_run_success = bool(await self._device.set_position(position)) + self.async_write_ha_state() @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 7a16225dcbb..cb485ffd8a5 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.13.3"], + "requirements": ["PySwitchbot==0.14.0"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], "iot_class": "local_polling", diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 1ee0276b7ee..759a504d19a 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -53,8 +54,8 @@ async def async_setup_entry( DATA_COORDINATOR ] - if not coordinator.data[entry.unique_id].get("data"): - return + if not coordinator.data.get(entry.unique_id): + raise PlatformNotReady async_add_entities( [ diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index b5507594521..404a92eda82 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -4,12 +4,13 @@ from __future__ import annotations import logging from typing import Any -from switchbot import Switchbot # pylint: disable=import-error +from switchbot import Switchbot from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform from homeassistant.helpers.restore_state import RestoreEntity @@ -32,6 +33,9 @@ async def async_setup_entry( DATA_COORDINATOR ] + if not coordinator.data.get(entry.unique_id): + raise PlatformNotReady + async_add_entities( [ SwitchBotBotEntity( @@ -80,25 +84,19 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): """Turn device on.""" _LOGGER.info("Turn Switchbot bot on %s", self._mac) - async with self.coordinator.api_lock: - self._last_run_success = bool( - await self.hass.async_add_executor_job(self._device.turn_on) - ) - if self._last_run_success: - self._attr_is_on = True - self.async_write_ha_state() + self._last_run_success = bool(await self._device.turn_on()) + if self._last_run_success: + self._attr_is_on = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" _LOGGER.info("Turn Switchbot bot off %s", self._mac) - async with self.coordinator.api_lock: - self._last_run_success = bool( - await self.hass.async_add_executor_job(self._device.turn_off) - ) - if self._last_run_success: - self._attr_is_on = False - self.async_write_ha_state() + self._last_run_success = bool(await self._device.turn_off()) + if self._last_run_success: + self._attr_is_on = False + self.async_write_ha_state() @property def assumed_state(self) -> bool: diff --git a/requirements_all.txt b/requirements_all.txt index 95cbdd5b0bf..3173a74468b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -34,7 +34,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -# PySwitchbot==0.13.3 +PySwitchbot==0.14.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9f095161ea..ffab3df0314 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -30,7 +30,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -# PySwitchbot==0.13.3 +PySwitchbot==0.14.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 5db46ffbeba..a2a0eab897a 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -30,7 +30,6 @@ COMMENT_REQUIREMENTS = ( "opencv-python-headless", "pybluez", "pycups", - "PySwitchbot", "pySwitchmate", "python-eq3bt", "python-gammu", diff --git a/tests/components/switchbot/conftest.py b/tests/components/switchbot/conftest.py index 2fdea69de16..550aeb08082 100644 --- a/tests/components/switchbot/conftest.py +++ b/tests/components/switchbot/conftest.py @@ -45,6 +45,19 @@ class MocGetSwitchbotDevices: "model": "m", "rawAdvData": "000d6d00", }, + "c0ceb0d426be": { + "mac_address": "c0:ce:b0:d4:26:be", + "isEncrypted": False, + "data": { + "temp": {"c": 21.6, "f": 70.88}, + "fahrenheit": False, + "humidity": 73, + "battery": 100, + "rssi": -58, + }, + "model": "T", + "modelName": "WoSensorTH", + }, } self._curtain_all_services_data = { "mac_address": "e7:89:43:90:90:90", @@ -72,11 +85,11 @@ class MocGetSwitchbotDevices: "modelName": "WoOther", } - def discover(self, retry=0, scan_timeout=0): + async def discover(self, retry=0, scan_timeout=0): """Mock discover.""" return self._all_services_data - def get_device_data(self, mac=None): + async def get_device_data(self, mac=None): """Return data for specific device.""" if mac == "e7:89:43:99:99:99": return self._all_services_data