J. Nick Koston 198167a2c8
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>
2022-07-24 11:38:45 -05:00

166 lines
5.9 KiB
Python

"""Config flow for Switchbot."""
from __future__ import annotations
import logging
from typing import Any, cast
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_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,
DEFAULT_RETRY_COUNT,
DEFAULT_RETRY_TIMEOUT,
DOMAIN,
SUPPORTED_MODEL_TYPES,
)
_LOGGER = logging.getLogger(__name__)
def format_unique_id(address: str) -> str:
"""Format the unique ID for a switchbot."""
return address.replace(":", "").lower()
class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Switchbot."""
VERSION = 1
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> SwitchbotOptionsFlowHandler:
"""Get the options flow for this handler."""
return SwitchbotOptionsFlowHandler(config_entry)
def __init__(self):
"""Initialize the config flow."""
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 the user step to pick discovered device."""
errors: dict[str, str] = {}
if user_input is not None:
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_advs[address].data["modelName"]
]
return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
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
if not self._discovered_advs:
return self.async_abort(reason="no_unconfigured_devices")
data_schema = vol.Schema(
{
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
)
class SwitchbotOptionsFlowHandler(OptionsFlow):
"""Handle Switchbot options."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage Switchbot options."""
if user_input is not None:
# Update common entity options for all other entities.
for entry in self.hass.config_entries.async_entries(DOMAIN):
if entry.unique_id != self.config_entry.unique_id:
self.hass.config_entries.async_update_entry(
entry, options=user_input
)
return self.async_create_entry(title="", data=user_input)
options = {
vol.Optional(
CONF_RETRY_COUNT,
default=self.config_entry.options.get(
CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT
),
): int,
vol.Optional(
CONF_RETRY_TIMEOUT,
default=self.config_entry.options.get(
CONF_RETRY_TIMEOUT, DEFAULT_RETRY_TIMEOUT
),
): int,
}
return self.async_show_form(step_id="init", data_schema=vol.Schema(options))