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:
J. Nick Koston 2022-07-24 11:38:45 -05:00 committed by GitHub
parent 79be87f9ce
commit 198167a2c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 542 additions and 462 deletions

View file

@ -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."""