Simplify switchbot config flow (#76272)
This commit is contained in:
parent
54fc17e10d
commit
b1497b0857
13 changed files with 412 additions and 246 deletions
|
@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ADDRESS,
|
CONF_ADDRESS,
|
||||||
CONF_MAC,
|
CONF_MAC,
|
||||||
|
CONF_NAME,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_SENSOR_TYPE,
|
CONF_SENSOR_TYPE,
|
||||||
Platform,
|
Platform,
|
||||||
|
@ -17,31 +18,26 @@ from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from .const import (
|
from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SupportedModels
|
||||||
ATTR_BOT,
|
|
||||||
ATTR_CONTACT,
|
|
||||||
ATTR_CURTAIN,
|
|
||||||
ATTR_HYGROMETER,
|
|
||||||
ATTR_MOTION,
|
|
||||||
ATTR_PLUG,
|
|
||||||
CONF_RETRY_COUNT,
|
|
||||||
DEFAULT_RETRY_COUNT,
|
|
||||||
DOMAIN,
|
|
||||||
)
|
|
||||||
from .coordinator import SwitchbotDataUpdateCoordinator
|
from .coordinator import SwitchbotDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS_BY_TYPE = {
|
PLATFORMS_BY_TYPE = {
|
||||||
ATTR_BOT: [Platform.SWITCH, Platform.SENSOR],
|
SupportedModels.BULB.value: [Platform.SENSOR],
|
||||||
ATTR_PLUG: [Platform.SWITCH, Platform.SENSOR],
|
SupportedModels.BOT.value: [Platform.SWITCH, Platform.SENSOR],
|
||||||
ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR],
|
SupportedModels.PLUG.value: [Platform.SWITCH, Platform.SENSOR],
|
||||||
ATTR_HYGROMETER: [Platform.SENSOR],
|
SupportedModels.CURTAIN.value: [
|
||||||
ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR],
|
Platform.COVER,
|
||||||
ATTR_MOTION: [Platform.BINARY_SENSOR, Platform.SENSOR],
|
Platform.BINARY_SENSOR,
|
||||||
|
Platform.SENSOR,
|
||||||
|
],
|
||||||
|
SupportedModels.HYGROMETER.value: [Platform.SENSOR],
|
||||||
|
SupportedModels.CONTACT.value: [Platform.BINARY_SENSOR, Platform.SENSOR],
|
||||||
|
SupportedModels.MOTION.value: [Platform.BINARY_SENSOR, Platform.SENSOR],
|
||||||
}
|
}
|
||||||
CLASS_BY_DEVICE = {
|
CLASS_BY_DEVICE = {
|
||||||
ATTR_CURTAIN: switchbot.SwitchbotCurtain,
|
SupportedModels.CURTAIN.value: switchbot.SwitchbotCurtain,
|
||||||
ATTR_BOT: switchbot.Switchbot,
|
SupportedModels.BOT.value: switchbot.Switchbot,
|
||||||
ATTR_PLUG: switchbot.SwitchbotPlugMini,
|
SupportedModels.PLUG.value: switchbot.SwitchbotPlugMini,
|
||||||
}
|
}
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -49,6 +45,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Switchbot from a config entry."""
|
"""Set up Switchbot from a config entry."""
|
||||||
|
assert entry.unique_id is not None
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
if CONF_ADDRESS not in entry.data and CONF_MAC in entry.data:
|
if CONF_ADDRESS not in entry.data and CONF_MAC in entry.data:
|
||||||
# Bleak uses addresses not mac addresses which are are actually
|
# Bleak uses addresses not mac addresses which are are actually
|
||||||
|
@ -81,7 +78,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
retry_count=entry.options[CONF_RETRY_COUNT],
|
retry_count=entry.options[CONF_RETRY_COUNT],
|
||||||
)
|
)
|
||||||
coordinator = hass.data[DOMAIN][entry.entry_id] = SwitchbotDataUpdateCoordinator(
|
coordinator = hass.data[DOMAIN][entry.entry_id] = SwitchbotDataUpdateCoordinator(
|
||||||
hass, _LOGGER, ble_device, device
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
ble_device,
|
||||||
|
device,
|
||||||
|
entry.unique_id,
|
||||||
|
entry.data.get(CONF_NAME, entry.title),
|
||||||
)
|
)
|
||||||
entry.async_on_unload(coordinator.async_start())
|
entry.async_on_unload(coordinator.async_start())
|
||||||
if not await coordinator.async_wait_ready():
|
if not await coordinator.async_wait_ready():
|
||||||
|
|
|
@ -7,7 +7,6 @@ from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity import EntityCategory
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
@ -53,20 +52,10 @@ async def async_setup_entry(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Switchbot curtain based on a config entry."""
|
"""Set up Switchbot curtain based on a config entry."""
|
||||||
coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
unique_id = entry.unique_id
|
|
||||||
assert unique_id is not None
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
SwitchBotBinarySensor(coordinator, binary_sensor)
|
||||||
SwitchBotBinarySensor(
|
for binary_sensor in coordinator.data["data"]
|
||||||
coordinator,
|
if binary_sensor in BINARY_SENSOR_TYPES
|
||||||
unique_id,
|
|
||||||
binary_sensor,
|
|
||||||
entry.data[CONF_ADDRESS],
|
|
||||||
entry.data[CONF_NAME],
|
|
||||||
)
|
|
||||||
for binary_sensor in coordinator.data["data"]
|
|
||||||
if binary_sensor in BINARY_SENSOR_TYPES
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,15 +67,12 @@ class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: SwitchbotDataUpdateCoordinator,
|
coordinator: SwitchbotDataUpdateCoordinator,
|
||||||
unique_id: str,
|
|
||||||
binary_sensor: str,
|
binary_sensor: str,
|
||||||
mac: str,
|
|
||||||
switchbot_name: str,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Switchbot sensor."""
|
"""Initialize the Switchbot sensor."""
|
||||||
super().__init__(coordinator, unique_id, mac, name=switchbot_name)
|
super().__init__(coordinator)
|
||||||
self._sensor = binary_sensor
|
self._sensor = binary_sensor
|
||||||
self._attr_unique_id = f"{unique_id}-{binary_sensor}"
|
self._attr_unique_id = f"{coordinator.base_unique_id}-{binary_sensor}"
|
||||||
self.entity_description = BINARY_SENSOR_TYPES[binary_sensor]
|
self.entity_description = BINARY_SENSOR_TYPES[binary_sensor]
|
||||||
self._attr_name = self.entity_description.name
|
self._attr_name = self.entity_description.name
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,9 @@ from homeassistant.components.bluetooth import (
|
||||||
async_discovered_service_info,
|
async_discovered_service_info,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
|
||||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE
|
from homeassistant.const import CONF_ADDRESS, CONF_PASSWORD, CONF_SENSOR_TYPE
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import AbortFlow, FlowResult
|
||||||
|
|
||||||
from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SUPPORTED_MODEL_TYPES
|
from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SUPPORTED_MODEL_TYPES
|
||||||
|
|
||||||
|
@ -26,6 +26,17 @@ def format_unique_id(address: str) -> str:
|
||||||
return address.replace(":", "").lower()
|
return address.replace(":", "").lower()
|
||||||
|
|
||||||
|
|
||||||
|
def short_address(address: str) -> str:
|
||||||
|
"""Convert a Bluetooth address to a short address."""
|
||||||
|
results = address.replace("-", ":").split(":")
|
||||||
|
return f"{results[-2].upper()}{results[-1].upper()}"[-4:]
|
||||||
|
|
||||||
|
|
||||||
|
def name_from_discovery(discovery: SwitchBotAdvertisement) -> str:
|
||||||
|
"""Get the name from a discovery."""
|
||||||
|
return f'{discovery.data["modelFriendlyName"]} {short_address(discovery.address)}'
|
||||||
|
|
||||||
|
|
||||||
class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Switchbot."""
|
"""Handle a config flow for Switchbot."""
|
||||||
|
|
||||||
|
@ -59,62 +70,128 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
self._discovered_adv = parsed
|
self._discovered_adv = parsed
|
||||||
data = parsed.data
|
data = parsed.data
|
||||||
self.context["title_placeholders"] = {
|
self.context["title_placeholders"] = {
|
||||||
"name": data["modelName"],
|
"name": data["modelFriendlyName"],
|
||||||
"address": discovery_info.address,
|
"address": short_address(discovery_info.address),
|
||||||
}
|
}
|
||||||
return await self.async_step_user()
|
if self._discovered_adv.data["isEncrypted"]:
|
||||||
|
return await self.async_step_password()
|
||||||
|
return await self.async_step_confirm()
|
||||||
|
|
||||||
|
async def _async_create_entry_from_discovery(
|
||||||
|
self, user_input: dict[str, Any]
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Create an entry from a discovery."""
|
||||||
|
assert self._discovered_adv is not None
|
||||||
|
discovery = self._discovered_adv
|
||||||
|
name = name_from_discovery(discovery)
|
||||||
|
model_name = discovery.data["modelName"]
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=name,
|
||||||
|
data={
|
||||||
|
**user_input,
|
||||||
|
CONF_ADDRESS: discovery.address,
|
||||||
|
CONF_SENSOR_TYPE: str(SUPPORTED_MODEL_TYPES[model_name]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_confirm(self, user_input: dict[str, Any] = None) -> FlowResult:
|
||||||
|
"""Confirm a single device."""
|
||||||
|
assert self._discovered_adv is not None
|
||||||
|
if user_input is not None:
|
||||||
|
return await self._async_create_entry_from_discovery(user_input)
|
||||||
|
|
||||||
|
self._set_confirm_only()
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="confirm",
|
||||||
|
data_schema=vol.Schema({}),
|
||||||
|
description_placeholders={
|
||||||
|
"name": name_from_discovery(self._discovered_adv)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_password(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle the password step."""
|
||||||
|
assert self._discovered_adv is not None
|
||||||
|
if user_input is not None:
|
||||||
|
# There is currently no api to validate the password
|
||||||
|
# that does not operate the device so we have
|
||||||
|
# to accept it as-is
|
||||||
|
return await self._async_create_entry_from_discovery(user_input)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="password",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
|
||||||
|
description_placeholders={
|
||||||
|
"name": name_from_discovery(self._discovered_adv)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_discover_devices(self) -> None:
|
||||||
|
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:
|
||||||
|
raise AbortFlow("no_unconfigured_devices")
|
||||||
|
|
||||||
|
async def _async_set_device(self, discovery: SwitchBotAdvertisement) -> None:
|
||||||
|
"""Set the device to work with."""
|
||||||
|
self._discovered_adv = discovery
|
||||||
|
address = discovery.address
|
||||||
|
await self.async_set_unique_id(
|
||||||
|
format_unique_id(address), raise_on_progress=False
|
||||||
|
)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
"""Handle the user step to pick discovered device."""
|
"""Handle the user step to pick discovered device."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
|
device_adv: SwitchBotAdvertisement | None = None
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
address = user_input[CONF_ADDRESS]
|
device_adv = self._discovered_advs[user_input[CONF_ADDRESS]]
|
||||||
await self.async_set_unique_id(
|
await self._async_set_device(device_adv)
|
||||||
format_unique_id(address), raise_on_progress=False
|
if device_adv.data["isEncrypted"]:
|
||||||
)
|
return await self.async_step_password()
|
||||||
self._abort_if_unique_id_configured()
|
return await self._async_create_entry_from_discovery(user_input)
|
||||||
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._async_discover_devices()
|
||||||
self._discovered_advs[discovery.address] = discovery
|
if len(self._discovered_advs) == 1:
|
||||||
else:
|
# If there is only one device we can ask for a password
|
||||||
current_addresses = self._async_current_ids()
|
# or simply confirm it
|
||||||
for discovery_info in async_discovered_service_info(self.hass):
|
device_adv = list(self._discovered_advs.values())[0]
|
||||||
address = discovery_info.address
|
await self._async_set_device(device_adv)
|
||||||
if (
|
if device_adv.data["isEncrypted"]:
|
||||||
format_unique_id(address) in current_addresses
|
return await self.async_step_password()
|
||||||
or address in self._discovered_advs
|
return await self.async_step_confirm()
|
||||||
):
|
|
||||||
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(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=data_schema, errors=errors
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_ADDRESS): vol.In(
|
||||||
|
{
|
||||||
|
address: name_from_discovery(parsed)
|
||||||
|
for address, parsed in self._discovered_advs.items()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,39 @@
|
||||||
"""Constants for the switchbot integration."""
|
"""Constants for the switchbot integration."""
|
||||||
|
from switchbot import SwitchbotModel
|
||||||
|
|
||||||
|
from homeassistant.backports.enum import StrEnum
|
||||||
|
|
||||||
DOMAIN = "switchbot"
|
DOMAIN = "switchbot"
|
||||||
MANUFACTURER = "switchbot"
|
MANUFACTURER = "switchbot"
|
||||||
|
|
||||||
# Config Attributes
|
# Config Attributes
|
||||||
ATTR_BOT = "bot"
|
|
||||||
ATTR_CURTAIN = "curtain"
|
|
||||||
ATTR_HYGROMETER = "hygrometer"
|
|
||||||
ATTR_CONTACT = "contact"
|
|
||||||
ATTR_PLUG = "plug"
|
|
||||||
ATTR_MOTION = "motion"
|
|
||||||
DEFAULT_NAME = "Switchbot"
|
DEFAULT_NAME = "Switchbot"
|
||||||
|
|
||||||
|
|
||||||
|
class SupportedModels(StrEnum):
|
||||||
|
"""Supported Switchbot models."""
|
||||||
|
|
||||||
|
BOT = "bot"
|
||||||
|
BULB = "bulb"
|
||||||
|
CURTAIN = "curtain"
|
||||||
|
HYGROMETER = "hygrometer"
|
||||||
|
CONTACT = "contact"
|
||||||
|
PLUG = "plug"
|
||||||
|
MOTION = "motion"
|
||||||
|
|
||||||
|
|
||||||
SUPPORTED_MODEL_TYPES = {
|
SUPPORTED_MODEL_TYPES = {
|
||||||
"WoHand": ATTR_BOT,
|
SwitchbotModel.BOT: SupportedModels.BOT,
|
||||||
"WoCurtain": ATTR_CURTAIN,
|
SwitchbotModel.CURTAIN: SupportedModels.CURTAIN,
|
||||||
"WoSensorTH": ATTR_HYGROMETER,
|
SwitchbotModel.METER: SupportedModels.HYGROMETER,
|
||||||
"WoContact": ATTR_CONTACT,
|
SwitchbotModel.CONTACT_SENSOR: SupportedModels.CONTACT,
|
||||||
"WoPlug": ATTR_PLUG,
|
SwitchbotModel.PLUG_MINI: SupportedModels.PLUG,
|
||||||
"WoPresence": ATTR_MOTION,
|
SwitchbotModel.MOTION_SENSOR: SupportedModels.MOTION,
|
||||||
|
SwitchbotModel.COLOR_BULB: SupportedModels.BULB,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Config Defaults
|
# Config Defaults
|
||||||
DEFAULT_RETRY_COUNT = 3
|
DEFAULT_RETRY_COUNT = 3
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,8 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||||
logger: logging.Logger,
|
logger: logging.Logger,
|
||||||
ble_device: BLEDevice,
|
ble_device: BLEDevice,
|
||||||
device: switchbot.SwitchbotDevice,
|
device: switchbot.SwitchbotDevice,
|
||||||
|
base_unique_id: str,
|
||||||
|
device_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize global switchbot data updater."""
|
"""Initialize global switchbot data updater."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
@ -45,6 +47,8 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||||
self.ble_device = ble_device
|
self.ble_device = ble_device
|
||||||
self.device = device
|
self.device = device
|
||||||
self.data: dict[str, Any] = {}
|
self.data: dict[str, Any] = {}
|
||||||
|
self.device_name = device_name
|
||||||
|
self.base_unique_id = base_unique_id
|
||||||
self._ready_event = asyncio.Event()
|
self._ready_event = asyncio.Event()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
@ -4,8 +4,6 @@ from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from switchbot import SwitchbotCurtain
|
|
||||||
|
|
||||||
from homeassistant.components.cover import (
|
from homeassistant.components.cover import (
|
||||||
ATTR_CURRENT_POSITION,
|
ATTR_CURRENT_POSITION,
|
||||||
ATTR_POSITION,
|
ATTR_POSITION,
|
||||||
|
@ -14,7 +12,6 @@ from homeassistant.components.cover import (
|
||||||
CoverEntityFeature,
|
CoverEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
@ -33,19 +30,7 @@ async def async_setup_entry(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Switchbot curtain based on a config entry."""
|
"""Set up Switchbot curtain based on a config entry."""
|
||||||
coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
unique_id = entry.unique_id
|
async_add_entities([SwitchBotCurtainEntity(coordinator)])
|
||||||
assert unique_id is not None
|
|
||||||
async_add_entities(
|
|
||||||
[
|
|
||||||
SwitchBotCurtainEntity(
|
|
||||||
coordinator,
|
|
||||||
unique_id,
|
|
||||||
entry.data[CONF_ADDRESS],
|
|
||||||
entry.data[CONF_NAME],
|
|
||||||
coordinator.device,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity):
|
class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity):
|
||||||
|
@ -59,19 +44,10 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity):
|
||||||
| CoverEntityFeature.SET_POSITION
|
| CoverEntityFeature.SET_POSITION
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None:
|
||||||
self,
|
|
||||||
coordinator: SwitchbotDataUpdateCoordinator,
|
|
||||||
unique_id: str,
|
|
||||||
address: str,
|
|
||||||
name: str,
|
|
||||||
device: SwitchbotCurtain,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the Switchbot."""
|
"""Initialize the Switchbot."""
|
||||||
super().__init__(coordinator, unique_id, address, name)
|
super().__init__(coordinator)
|
||||||
self._attr_unique_id = unique_id
|
|
||||||
self._attr_is_closed = None
|
self._attr_is_closed = None
|
||||||
self._device = device
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Run when entity about to be added."""
|
"""Run when entity about to be added."""
|
||||||
|
|
|
@ -20,24 +20,19 @@ class SwitchbotEntity(PassiveBluetoothCoordinatorEntity):
|
||||||
|
|
||||||
coordinator: SwitchbotDataUpdateCoordinator
|
coordinator: SwitchbotDataUpdateCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None:
|
||||||
self,
|
|
||||||
coordinator: SwitchbotDataUpdateCoordinator,
|
|
||||||
unique_id: str,
|
|
||||||
address: str,
|
|
||||||
name: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
self._device = coordinator.device
|
||||||
self._last_run_success: bool | None = None
|
self._last_run_success: bool | None = None
|
||||||
self._unique_id = unique_id
|
self._address = coordinator.ble_device.address
|
||||||
self._address = address
|
self._attr_unique_id = coordinator.base_unique_id
|
||||||
self._attr_name = name
|
self._attr_name = coordinator.device_name
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
connections={(dr.CONNECTION_BLUETOOTH, self._address)},
|
connections={(dr.CONNECTION_BLUETOOTH, self._address)},
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
model=self.data["modelName"],
|
model=self.data["modelName"],
|
||||||
name=name,
|
name=coordinator.device_name,
|
||||||
)
|
)
|
||||||
if ":" not in self._address:
|
if ":" not in self._address:
|
||||||
# MacOS Bluetooth addresses are not mac addresses
|
# MacOS Bluetooth addresses are not mac addresses
|
||||||
|
|
|
@ -9,8 +9,6 @@ from homeassistant.components.sensor import (
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ADDRESS,
|
|
||||||
CONF_NAME,
|
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
|
@ -73,20 +71,13 @@ async def async_setup_entry(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Switchbot sensor based on a config entry."""
|
"""Set up Switchbot sensor based on a config entry."""
|
||||||
coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
unique_id = entry.unique_id
|
|
||||||
assert unique_id is not None
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
SwitchBotSensor(
|
||||||
SwitchBotSensor(
|
coordinator,
|
||||||
coordinator,
|
sensor,
|
||||||
unique_id,
|
)
|
||||||
sensor,
|
for sensor in coordinator.data["data"]
|
||||||
entry.data[CONF_ADDRESS],
|
if sensor in SENSOR_TYPES
|
||||||
entry.data[CONF_NAME],
|
|
||||||
)
|
|
||||||
for sensor in coordinator.data["data"]
|
|
||||||
if sensor in SENSOR_TYPES
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,16 +87,14 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: SwitchbotDataUpdateCoordinator,
|
coordinator: SwitchbotDataUpdateCoordinator,
|
||||||
unique_id: str,
|
|
||||||
sensor: str,
|
sensor: str,
|
||||||
address: str,
|
|
||||||
switchbot_name: str,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Switchbot sensor."""
|
"""Initialize the Switchbot sensor."""
|
||||||
super().__init__(coordinator, unique_id, address, name=switchbot_name)
|
super().__init__(coordinator)
|
||||||
self._sensor = sensor
|
self._sensor = sensor
|
||||||
self._attr_unique_id = f"{unique_id}-{sensor}"
|
self._attr_unique_id = f"{coordinator.base_unique_id}-{sensor}"
|
||||||
self._attr_name = f"{switchbot_name} {sensor.replace('_', ' ').title()}"
|
name = coordinator.device_name
|
||||||
|
self._attr_name = f"{name} {sensor.replace('_', ' ').title()}"
|
||||||
self.entity_description = SENSOR_TYPES[sensor]
|
self.entity_description = SENSOR_TYPES[sensor]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -3,10 +3,16 @@
|
||||||
"flow_title": "{name} ({address})",
|
"flow_title": "{name} ({address})",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Setup Switchbot device",
|
|
||||||
"data": {
|
"data": {
|
||||||
"address": "Device address",
|
"address": "Device address"
|
||||||
"name": "[%key:common::config_flow::data::name%]",
|
}
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"description": "Do you want to setup {name}?"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"description": "The {name} device requires a password",
|
||||||
|
"data": {
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,9 @@ from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from switchbot import Switchbot
|
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME, STATE_ON
|
from homeassistant.const import STATE_ON
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_platform
|
from homeassistant.helpers import entity_platform
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
@ -29,19 +27,7 @@ async def async_setup_entry(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Switchbot based on a config entry."""
|
"""Set up Switchbot based on a config entry."""
|
||||||
coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
unique_id = entry.unique_id
|
async_add_entities([SwitchBotSwitch(coordinator)])
|
||||||
assert unique_id is not None
|
|
||||||
async_add_entities(
|
|
||||||
[
|
|
||||||
SwitchBotSwitch(
|
|
||||||
coordinator,
|
|
||||||
unique_id,
|
|
||||||
entry.data[CONF_ADDRESS],
|
|
||||||
entry.data[CONF_NAME],
|
|
||||||
coordinator.device,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity):
|
class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity):
|
||||||
|
@ -49,18 +35,9 @@ class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity):
|
||||||
|
|
||||||
_attr_device_class = SwitchDeviceClass.SWITCH
|
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None:
|
||||||
self,
|
|
||||||
coordinator: SwitchbotDataUpdateCoordinator,
|
|
||||||
unique_id: str,
|
|
||||||
address: str,
|
|
||||||
name: str,
|
|
||||||
device: Switchbot,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the Switchbot."""
|
"""Initialize the Switchbot."""
|
||||||
super().__init__(coordinator, unique_id, address, name)
|
super().__init__(coordinator)
|
||||||
self._attr_unique_id = unique_id
|
|
||||||
self._device = device
|
|
||||||
self._attr_is_on = False
|
self._attr_is_on = False
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
|
|
|
@ -7,16 +7,22 @@
|
||||||
"switchbot_unsupported_type": "Unsupported Switchbot Type.",
|
"switchbot_unsupported_type": "Unsupported Switchbot Type.",
|
||||||
"unknown": "Unexpected error"
|
"unknown": "Unexpected error"
|
||||||
},
|
},
|
||||||
|
"error": {},
|
||||||
"flow_title": "{name} ({address})",
|
"flow_title": "{name} ({address})",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"confirm": {
|
||||||
|
"description": "Do you want to setup {name}?"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
"data": {
|
"data": {
|
||||||
"address": "Device address",
|
|
||||||
"mac": "Device MAC address",
|
|
||||||
"name": "Name",
|
|
||||||
"password": "Password"
|
"password": "Password"
|
||||||
},
|
},
|
||||||
"title": "Setup Switchbot device"
|
"description": "The {name} device requires a password"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"address": "Device address"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -24,10 +30,7 @@
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
"retry_count": "Retry count",
|
"retry_count": "Retry count"
|
||||||
"retry_timeout": "Timeout between retries",
|
|
||||||
"scan_timeout": "How long to scan for advertisement data",
|
|
||||||
"update_time": "Time between updates (seconds)"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ from bleak.backends.device import BLEDevice
|
||||||
from bleak.backends.scanner import AdvertisementData
|
from bleak.backends.scanner import AdvertisementData
|
||||||
|
|
||||||
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
|
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
|
||||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD
|
from homeassistant.const import CONF_ADDRESS
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
@ -13,38 +13,18 @@ from tests.common import MockConfigEntry
|
||||||
DOMAIN = "switchbot"
|
DOMAIN = "switchbot"
|
||||||
|
|
||||||
ENTRY_CONFIG = {
|
ENTRY_CONFIG = {
|
||||||
CONF_NAME: "test-name",
|
|
||||||
CONF_PASSWORD: "test-password",
|
|
||||||
CONF_ADDRESS: "e7:89:43:99:99:99",
|
CONF_ADDRESS: "e7:89:43:99:99:99",
|
||||||
}
|
}
|
||||||
|
|
||||||
USER_INPUT = {
|
USER_INPUT = {
|
||||||
CONF_NAME: "test-name",
|
|
||||||
CONF_PASSWORD: "test-password",
|
|
||||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
|
||||||
}
|
|
||||||
|
|
||||||
USER_INPUT_CURTAIN = {
|
|
||||||
CONF_NAME: "test-name",
|
|
||||||
CONF_PASSWORD: "test-password",
|
|
||||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
|
||||||
}
|
|
||||||
|
|
||||||
USER_INPUT_SENSOR = {
|
|
||||||
CONF_NAME: "test-name",
|
|
||||||
CONF_PASSWORD: "test-password",
|
|
||||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
}
|
}
|
||||||
|
|
||||||
USER_INPUT_UNSUPPORTED_DEVICE = {
|
USER_INPUT_UNSUPPORTED_DEVICE = {
|
||||||
CONF_NAME: "test-name",
|
|
||||||
CONF_PASSWORD: "test-password",
|
|
||||||
CONF_ADDRESS: "test",
|
CONF_ADDRESS: "test",
|
||||||
}
|
}
|
||||||
|
|
||||||
USER_INPUT_INVALID = {
|
USER_INPUT_INVALID = {
|
||||||
CONF_NAME: "test-name",
|
|
||||||
CONF_PASSWORD: "test-password",
|
|
||||||
CONF_ADDRESS: "invalid-mac",
|
CONF_ADDRESS: "invalid-mac",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +70,42 @@ WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||||
),
|
),
|
||||||
device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"),
|
device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
WOHAND_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||||
|
name="WoHand",
|
||||||
|
manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
|
||||||
|
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"\xc8\x10\xcf"},
|
||||||
|
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
||||||
|
address="798A8547-2A3D-C609-55FF-73FA824B923B",
|
||||||
|
rssi=-60,
|
||||||
|
source="local",
|
||||||
|
advertisement=AdvertisementData(
|
||||||
|
local_name="WoHand",
|
||||||
|
manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
|
||||||
|
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"\xc8\x10\xcf"},
|
||||||
|
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
||||||
|
),
|
||||||
|
device=BLEDevice("798A8547-2A3D-C609-55FF-73FA824B923B", "WoHand"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
WOHAND_SERVICE_ALT_ADDRESS_INFO = BluetoothServiceInfoBleak(
|
||||||
|
name="WoHand",
|
||||||
|
manufacturer_data={89: b"\xfd`0U\x92W"},
|
||||||
|
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"},
|
||||||
|
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
||||||
|
address="cc:cc:cc:cc:cc:cc",
|
||||||
|
rssi=-60,
|
||||||
|
source="local",
|
||||||
|
advertisement=AdvertisementData(
|
||||||
|
local_name="WoHand",
|
||||||
|
manufacturer_data={89: b"\xfd`0U\x92W"},
|
||||||
|
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"},
|
||||||
|
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
||||||
|
),
|
||||||
|
device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"),
|
||||||
|
)
|
||||||
WOCURTAIN_SERVICE_INFO = BluetoothServiceInfoBleak(
|
WOCURTAIN_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||||
name="WoCurtain",
|
name="WoCurtain",
|
||||||
address="aa:bb:cc:dd:ee:ff",
|
address="aa:bb:cc:dd:ee:ff",
|
||||||
|
|
|
@ -10,9 +10,9 @@ from homeassistant.data_entry_flow import FlowResultType
|
||||||
from . import (
|
from . import (
|
||||||
NOT_SWITCHBOT_INFO,
|
NOT_SWITCHBOT_INFO,
|
||||||
USER_INPUT,
|
USER_INPUT,
|
||||||
USER_INPUT_CURTAIN,
|
|
||||||
USER_INPUT_SENSOR,
|
|
||||||
WOCURTAIN_SERVICE_INFO,
|
WOCURTAIN_SERVICE_INFO,
|
||||||
|
WOHAND_ENCRYPTED_SERVICE_INFO,
|
||||||
|
WOHAND_SERVICE_ALT_ADDRESS_INFO,
|
||||||
WOHAND_SERVICE_INFO,
|
WOHAND_SERVICE_INFO,
|
||||||
WOSENSORTH_SERVICE_INFO,
|
WOSENSORTH_SERVICE_INFO,
|
||||||
init_integration,
|
init_integration,
|
||||||
|
@ -32,27 +32,53 @@ async def test_bluetooth_discovery(hass):
|
||||||
data=WOHAND_SERVICE_INFO,
|
data=WOHAND_SERVICE_INFO,
|
||||||
)
|
)
|
||||||
assert result["type"] == FlowResultType.FORM
|
assert result["type"] == FlowResultType.FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "confirm"
|
||||||
|
|
||||||
with patch_async_setup_entry() as mock_setup_entry:
|
with patch_async_setup_entry() as mock_setup_entry:
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
USER_INPUT,
|
{},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result["title"] == "test-name"
|
assert result["title"] == "Bot EEFF"
|
||||||
assert result["data"] == {
|
assert result["data"] == {
|
||||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
CONF_NAME: "test-name",
|
|
||||||
CONF_PASSWORD: "test-password",
|
|
||||||
CONF_SENSOR_TYPE: "bot",
|
CONF_SENSOR_TYPE: "bot",
|
||||||
}
|
}
|
||||||
|
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_bluetooth_discovery_requires_password(hass):
|
||||||
|
"""Test discovery via bluetooth with a valid device that needs a password."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_BLUETOOTH},
|
||||||
|
data=WOHAND_ENCRYPTED_SERVICE_INFO,
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "password"
|
||||||
|
|
||||||
|
with patch_async_setup_entry() as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_PASSWORD: "abc123"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "Bot 923B"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B",
|
||||||
|
CONF_SENSOR_TYPE: "bot",
|
||||||
|
CONF_PASSWORD: "abc123",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_bluetooth_discovery_already_setup(hass):
|
async def test_bluetooth_discovery_already_setup(hass):
|
||||||
"""Test discovery via bluetooth with a valid device when already setup."""
|
"""Test discovery via bluetooth with a valid device when already setup."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
|
@ -97,22 +123,20 @@ async def test_user_setup_wohand(hass):
|
||||||
DOMAIN, context={"source": SOURCE_USER}
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
)
|
)
|
||||||
assert result["type"] == FlowResultType.FORM
|
assert result["type"] == FlowResultType.FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "confirm"
|
||||||
assert result["errors"] == {}
|
assert result["errors"] is None
|
||||||
|
|
||||||
with patch_async_setup_entry() as mock_setup_entry:
|
with patch_async_setup_entry() as mock_setup_entry:
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
USER_INPUT,
|
{},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result["title"] == "test-name"
|
assert result["title"] == "Bot EEFF"
|
||||||
assert result["data"] == {
|
assert result["data"] == {
|
||||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
CONF_NAME: "test-name",
|
|
||||||
CONF_PASSWORD: "test-password",
|
|
||||||
CONF_SENSOR_TYPE: "bot",
|
CONF_SENSOR_TYPE: "bot",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,28 +178,129 @@ async def test_user_setup_wocurtain(hass):
|
||||||
DOMAIN, context={"source": SOURCE_USER}
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
)
|
)
|
||||||
assert result["type"] == FlowResultType.FORM
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "confirm"
|
||||||
|
assert result["errors"] is None
|
||||||
|
|
||||||
|
with patch_async_setup_entry() as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "Curtain EEFF"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
|
CONF_SENSOR_TYPE: "curtain",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_setup_wocurtain_or_bot(hass):
|
||||||
|
"""Test the user initiated form with valid address."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||||
|
return_value=[WOCURTAIN_SERVICE_INFO, WOHAND_SERVICE_ALT_ADDRESS_INFO],
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "user"
|
||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
|
||||||
with patch_async_setup_entry() as mock_setup_entry:
|
with patch_async_setup_entry() as mock_setup_entry:
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
USER_INPUT_CURTAIN,
|
USER_INPUT,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result["title"] == "test-name"
|
assert result["title"] == "Curtain EEFF"
|
||||||
assert result["data"] == {
|
assert result["data"] == {
|
||||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
CONF_NAME: "test-name",
|
|
||||||
CONF_PASSWORD: "test-password",
|
|
||||||
CONF_SENSOR_TYPE: "curtain",
|
CONF_SENSOR_TYPE: "curtain",
|
||||||
}
|
}
|
||||||
|
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_setup_wocurtain_or_bot_with_password(hass):
|
||||||
|
"""Test the user initiated form and valid address and a bot with a password."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||||
|
return_value=[WOCURTAIN_SERVICE_INFO, WOHAND_ENCRYPTED_SERVICE_INFO],
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B"},
|
||||||
|
)
|
||||||
|
assert result2["type"] == FlowResultType.FORM
|
||||||
|
assert result2["step_id"] == "password"
|
||||||
|
assert result2["errors"] is None
|
||||||
|
|
||||||
|
with patch_async_setup_entry() as mock_setup_entry:
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
|
{CONF_PASSWORD: "abc123"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result3["title"] == "Bot 923B"
|
||||||
|
assert result3["data"] == {
|
||||||
|
CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B",
|
||||||
|
CONF_PASSWORD: "abc123",
|
||||||
|
CONF_SENSOR_TYPE: "bot",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_setup_single_bot_with_password(hass):
|
||||||
|
"""Test the user initiated form for a bot with a password."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||||
|
return_value=[WOHAND_ENCRYPTED_SERVICE_INFO],
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "password"
|
||||||
|
assert result["errors"] is None
|
||||||
|
|
||||||
|
with patch_async_setup_entry() as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_PASSWORD: "abc123"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result2["title"] == "Bot 923B"
|
||||||
|
assert result2["data"] == {
|
||||||
|
CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B",
|
||||||
|
CONF_PASSWORD: "abc123",
|
||||||
|
CONF_SENSOR_TYPE: "bot",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_user_setup_wosensor(hass):
|
async def test_user_setup_wosensor(hass):
|
||||||
"""Test the user initiated form with password and valid mac."""
|
"""Test the user initiated form with password and valid mac."""
|
||||||
with patch(
|
with patch(
|
||||||
|
@ -186,22 +311,20 @@ async def test_user_setup_wosensor(hass):
|
||||||
DOMAIN, context={"source": SOURCE_USER}
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
)
|
)
|
||||||
assert result["type"] == FlowResultType.FORM
|
assert result["type"] == FlowResultType.FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "confirm"
|
||||||
assert result["errors"] == {}
|
assert result["errors"] is None
|
||||||
|
|
||||||
with patch_async_setup_entry() as mock_setup_entry:
|
with patch_async_setup_entry() as mock_setup_entry:
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
USER_INPUT_SENSOR,
|
{},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result["title"] == "test-name"
|
assert result["title"] == "Meter EEFF"
|
||||||
assert result["data"] == {
|
assert result["data"] == {
|
||||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
CONF_NAME: "test-name",
|
|
||||||
CONF_PASSWORD: "test-password",
|
|
||||||
CONF_SENSOR_TYPE: "hygrometer",
|
CONF_SENSOR_TYPE: "hygrometer",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,7 +352,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass):
|
||||||
data=WOCURTAIN_SERVICE_INFO,
|
data=WOCURTAIN_SERVICE_INFO,
|
||||||
)
|
)
|
||||||
assert result["type"] == FlowResultType.FORM
|
assert result["type"] == FlowResultType.FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "confirm"
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||||
|
@ -244,15 +367,13 @@ async def test_async_step_user_takes_precedence_over_discovery(hass):
|
||||||
with patch_async_setup_entry() as mock_setup_entry:
|
with patch_async_setup_entry() as mock_setup_entry:
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
user_input=USER_INPUT,
|
user_input={},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result2["title"] == "test-name"
|
assert result2["title"] == "Curtain EEFF"
|
||||||
assert result2["data"] == {
|
assert result2["data"] == {
|
||||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
CONF_NAME: "test-name",
|
|
||||||
CONF_PASSWORD: "test-password",
|
|
||||||
CONF_SENSOR_TYPE: "curtain",
|
CONF_SENSOR_TYPE: "curtain",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue