Update switchbot to be local push (#75645)
* Update switchbot to be local push * fixes * fixes * fixes * fixes * adjust * cover is not assumed anymore * cleanups * adjust * adjust * add missing cover * import compat * fixes * uses lower * uses lower * bleak users upper case addresses * fixes * bump * keep conf_mac and deprecated options for rollback * reuse coordinator * adjust * move around * move around * move around * move around * refactor fixes * compat with DataUpdateCoordinator * fix available * Update homeassistant/components/bluetooth/passive_update_processor.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/bluetooth/passive_update_coordinator.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/bluetooth/update_coordinator.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Split bluetooth coordinator into PassiveBluetoothDataUpdateCoordinator and PassiveBluetoothProcessorCoordinator The PassiveBluetoothDataUpdateCoordinator is now used to replace instances of DataUpdateCoordinator where the data is coming from bluetooth advertisements, and the integration may also mix in active updates The PassiveBluetoothProcessorCoordinator is used for integrations that want to process each bluetooth advertisement with multiple processors which can be dispatched to individual platforms or areas or the integration as it chooes * change connections * reduce code churn to reduce review overhead * reduce code churn to reduce review overhead * Update homeassistant/components/bluetooth/passive_update_coordinator.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * add basic test * add basic test * complete coverage * Update homeassistant/components/switchbot/coordinator.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/switchbot/coordinator.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/switchbot/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/switchbot/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * lint Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
79be87f9ce
commit
198167a2c8
17 changed files with 542 additions and 462 deletions
|
@ -2,25 +2,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from switchbot import GetSwitchbotDevices
|
||||
from switchbot import SwitchBotAdvertisement, parse_advertisement_data
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfoBleak,
|
||||
async_discovered_service_info,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
|
||||
from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
||||
|
||||
from .const import (
|
||||
CONF_RETRY_COUNT,
|
||||
CONF_RETRY_TIMEOUT,
|
||||
CONF_SCAN_TIMEOUT,
|
||||
CONF_TIME_BETWEEN_UPDATE_COMMAND,
|
||||
DEFAULT_RETRY_COUNT,
|
||||
DEFAULT_RETRY_TIMEOUT,
|
||||
DEFAULT_SCAN_TIMEOUT,
|
||||
DEFAULT_TIME_BETWEEN_UPDATE_COMMAND,
|
||||
DOMAIN,
|
||||
SUPPORTED_MODEL_TYPES,
|
||||
)
|
||||
|
@ -28,15 +29,9 @@ from .const import (
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _btle_connect() -> dict:
|
||||
"""Scan for BTLE advertisement data."""
|
||||
|
||||
switchbot_devices = await GetSwitchbotDevices().discover()
|
||||
|
||||
if not switchbot_devices:
|
||||
raise NotConnectedError("Failed to discover switchbot")
|
||||
|
||||
return switchbot_devices
|
||||
def format_unique_id(address: str) -> str:
|
||||
"""Format the unique ID for a switchbot."""
|
||||
return address.replace(":", "").lower()
|
||||
|
||||
|
||||
class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
@ -44,18 +39,6 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
VERSION = 1
|
||||
|
||||
async def _get_switchbots(self) -> dict:
|
||||
"""Try to discover nearby Switchbot devices."""
|
||||
# asyncio.lock prevents btle adapter exceptions if there are multiple calls to this method.
|
||||
# store asyncio.lock in hass data if not present.
|
||||
if DOMAIN not in self.hass.data:
|
||||
self.hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
# Discover switchbots nearby.
|
||||
_btle_adv_data = await _btle_connect()
|
||||
|
||||
return _btle_adv_data
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
|
@ -66,62 +49,79 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
def __init__(self):
|
||||
"""Initialize the config flow."""
|
||||
self._discovered_devices = {}
|
||||
self._discovered_adv: SwitchBotAdvertisement | None = None
|
||||
self._discovered_advs: dict[str, SwitchBotAdvertisement] = {}
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfo
|
||||
) -> FlowResult:
|
||||
"""Handle the bluetooth discovery step."""
|
||||
_LOGGER.debug("Discovered bluetooth device: %s", discovery_info)
|
||||
await self.async_set_unique_id(format_unique_id(discovery_info.address))
|
||||
self._abort_if_unique_id_configured()
|
||||
discovery_info_bleak = cast(BluetoothServiceInfoBleak, discovery_info)
|
||||
parsed = parse_advertisement_data(
|
||||
discovery_info_bleak.device, discovery_info_bleak.advertisement
|
||||
)
|
||||
if not parsed or parsed.data.get("modelName") not in SUPPORTED_MODEL_TYPES:
|
||||
return self.async_abort(reason="not_supported")
|
||||
self._discovered_adv = parsed
|
||||
data = parsed.data
|
||||
self.context["title_placeholders"] = {
|
||||
"name": data["modelName"],
|
||||
"address": discovery_info.address,
|
||||
}
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initiated by the user."""
|
||||
|
||||
"""Handle the user step to pick discovered device."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
await self.async_set_unique_id(user_input[CONF_MAC].replace(":", ""))
|
||||
address = user_input[CONF_ADDRESS]
|
||||
await self.async_set_unique_id(
|
||||
format_unique_id(address), raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
user_input[CONF_SENSOR_TYPE] = SUPPORTED_MODEL_TYPES[
|
||||
self._discovered_devices[self.unique_id]["modelName"]
|
||||
self._discovered_advs[address].data["modelName"]
|
||||
]
|
||||
|
||||
return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
|
||||
|
||||
try:
|
||||
self._discovered_devices = await self._get_switchbots()
|
||||
for device in self._discovered_devices.values():
|
||||
_LOGGER.debug("Found %s", device)
|
||||
if discovery := self._discovered_adv:
|
||||
self._discovered_advs[discovery.address] = discovery
|
||||
else:
|
||||
current_addresses = self._async_current_ids()
|
||||
for discovery_info in async_discovered_service_info(self.hass):
|
||||
address = discovery_info.address
|
||||
if (
|
||||
format_unique_id(address) in current_addresses
|
||||
or address in self._discovered_advs
|
||||
):
|
||||
continue
|
||||
parsed = parse_advertisement_data(
|
||||
discovery_info.device, discovery_info.advertisement
|
||||
)
|
||||
if parsed and parsed.data.get("modelName") in SUPPORTED_MODEL_TYPES:
|
||||
self._discovered_advs[address] = parsed
|
||||
|
||||
except NotConnectedError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
# Get devices already configured.
|
||||
configured_devices = {
|
||||
item.data[CONF_MAC]
|
||||
for item in self._async_current_entries(include_ignore=False)
|
||||
}
|
||||
|
||||
# Get supported devices not yet configured.
|
||||
unconfigured_devices = {
|
||||
device["mac_address"]: f"{device['mac_address']} {device['modelName']}"
|
||||
for device in self._discovered_devices.values()
|
||||
if device.get("modelName") in SUPPORTED_MODEL_TYPES
|
||||
and device["mac_address"] not in configured_devices
|
||||
}
|
||||
|
||||
if not unconfigured_devices:
|
||||
if not self._discovered_advs:
|
||||
return self.async_abort(reason="no_unconfigured_devices")
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_MAC): vol.In(unconfigured_devices),
|
||||
vol.Required(CONF_ADDRESS): vol.In(
|
||||
{
|
||||
address: f"{parsed.data['modelName']} ({address})"
|
||||
for address, parsed in self._discovered_advs.items()
|
||||
}
|
||||
),
|
||||
vol.Required(CONF_NAME): str,
|
||||
vol.Optional(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=data_schema, errors=errors
|
||||
)
|
||||
|
@ -148,13 +148,6 @@ class SwitchbotOptionsFlowHandler(OptionsFlow):
|
|||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
options = {
|
||||
vol.Optional(
|
||||
CONF_TIME_BETWEEN_UPDATE_COMMAND,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_TIME_BETWEEN_UPDATE_COMMAND,
|
||||
DEFAULT_TIME_BETWEEN_UPDATE_COMMAND,
|
||||
),
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_RETRY_COUNT,
|
||||
default=self.config_entry.options.get(
|
||||
|
@ -167,16 +160,6 @@ class SwitchbotOptionsFlowHandler(OptionsFlow):
|
|||
CONF_RETRY_TIMEOUT, DEFAULT_RETRY_TIMEOUT
|
||||
),
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_SCAN_TIMEOUT,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_SCAN_TIMEOUT, DEFAULT_SCAN_TIMEOUT
|
||||
),
|
||||
): int,
|
||||
}
|
||||
|
||||
return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
|
||||
|
||||
|
||||
class NotConnectedError(Exception):
|
||||
"""Exception for unable to find device."""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue