Add Keymitt BLE integration (#76575)
Co-authored-by: J. Nick Koston <nick@koston.org>pull/78924/head
parent
bbb5d6772c
commit
0e0318dc53
|
@ -626,6 +626,11 @@ omit =
|
|||
homeassistant/components/kef/*
|
||||
homeassistant/components/keyboard/*
|
||||
homeassistant/components/keyboard_remote/*
|
||||
homeassistant/components/keymitt_ble/__init__.py
|
||||
homeassistant/components/keymitt_ble/const.py
|
||||
homeassistant/components/keymitt_ble/entity.py
|
||||
homeassistant/components/keymitt_ble/switch.py
|
||||
homeassistant/components/keymitt_ble/coordinator.py
|
||||
homeassistant/components/kira/*
|
||||
homeassistant/components/kiwi/lock.py
|
||||
homeassistant/components/kodi/__init__.py
|
||||
|
|
|
@ -580,6 +580,8 @@ build.json @home-assistant/supervisor
|
|||
/homeassistant/components/kegtron/ @Ernst79
|
||||
/tests/components/kegtron/ @Ernst79
|
||||
/homeassistant/components/keyboard_remote/ @bendavid @lanrat
|
||||
/homeassistant/components/keymitt_ble/ @spycle
|
||||
/tests/components/keymitt_ble/ @spycle
|
||||
/homeassistant/components/kmtronic/ @dgomes
|
||||
/tests/components/kmtronic/ @dgomes
|
||||
/homeassistant/components/knx/ @Julius2342 @farmio @marvin-w
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
"""Integration to integrate Keymitt BLE devices with Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from microbot import MicroBotApiClient
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MicroBotDataUpdateCoordinator
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
PLATFORMS: list[str] = [Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up this integration using UI."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
token: str = entry.data[CONF_ACCESS_TOKEN]
|
||||
bdaddr: str = entry.data[CONF_ADDRESS]
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, bdaddr)
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(f"Could not find MicroBot with address {bdaddr}")
|
||||
client = MicroBotApiClient(
|
||||
device=ble_device,
|
||||
token=token,
|
||||
)
|
||||
coordinator = MicroBotDataUpdateCoordinator(
|
||||
hass, client=client, ble_device=ble_device
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(coordinator.async_start())
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Handle removal of an entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
|
@ -0,0 +1,157 @@
|
|||
"""Adds config flow for MicroBot."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from bleak.backends.device import BLEDevice
|
||||
from microbot import (
|
||||
MicroBotAdvertisement,
|
||||
MicroBotApiClient,
|
||||
parse_advertisement_data,
|
||||
randomid,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfoBleak,
|
||||
async_discovered_service_info,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
|
||||
def short_address(address: str) -> str:
|
||||
"""Convert a Bluetooth address to a short address."""
|
||||
results = address.replace("-", ":").split(":")
|
||||
return f"{results[0].upper()}{results[1].upper()}"[0:4]
|
||||
|
||||
|
||||
def name_from_discovery(discovery: MicroBotAdvertisement) -> str:
|
||||
"""Get the name from a discovery."""
|
||||
return f'{discovery.data["local_name"]} {short_address(discovery.address)}'
|
||||
|
||||
|
||||
class MicroBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for MicroBot."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
self._errors = {}
|
||||
self._discovered_adv: MicroBotAdvertisement | None = None
|
||||
self._discovered_advs: dict[str, MicroBotAdvertisement] = {}
|
||||
self._client: MicroBotApiClient | None = None
|
||||
self._ble_device: BLEDevice | None = None
|
||||
self._name: str | None = None
|
||||
self._bdaddr: str | None = None
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
) -> FlowResult:
|
||||
"""Handle the bluetooth discovery step."""
|
||||
_LOGGER.debug("Discovered bluetooth device: %s", discovery_info)
|
||||
await self.async_set_unique_id(discovery_info.address)
|
||||
self._abort_if_unique_id_configured()
|
||||
self._ble_device = discovery_info.device
|
||||
parsed = parse_advertisement_data(
|
||||
discovery_info.device, discovery_info.advertisement
|
||||
)
|
||||
self._discovered_adv = parsed
|
||||
self.context["title_placeholders"] = {
|
||||
"name": name_from_discovery(self._discovered_adv),
|
||||
}
|
||||
return await self.async_step_init()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
# This is for backwards compatibility.
|
||||
return await self.async_step_init(user_input)
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Check if paired."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
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):
|
||||
self._ble_device = discovery_info.device
|
||||
address = discovery_info.address
|
||||
if address in current_addresses or address in self._discovered_advs:
|
||||
continue
|
||||
parsed = parse_advertisement_data(
|
||||
discovery_info.device, discovery_info.advertisement
|
||||
)
|
||||
if parsed:
|
||||
self._discovered_adv = parsed
|
||||
self._discovered_advs[address] = parsed
|
||||
|
||||
if not self._discovered_advs:
|
||||
return self.async_abort(reason="no_unconfigured_devices")
|
||||
|
||||
if user_input is not None:
|
||||
self._name = name_from_discovery(self._discovered_adv)
|
||||
self._bdaddr = user_input[CONF_ADDRESS]
|
||||
await self.async_set_unique_id(self._bdaddr, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
return await self.async_step_link()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ADDRESS): vol.In(
|
||||
{
|
||||
address: f"{parsed.data['local_name']} ({address})"
|
||||
for address, parsed in self._discovered_advs.items()
|
||||
}
|
||||
)
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_link(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Given a configured host, will ask the user to press the button to pair."""
|
||||
errors: dict[str, str] = {}
|
||||
token = randomid(32)
|
||||
self._client = MicroBotApiClient(
|
||||
device=self._ble_device,
|
||||
token=token,
|
||||
)
|
||||
assert self._client is not None
|
||||
if user_input is None:
|
||||
await self._client.connect(init=True)
|
||||
return self.async_show_form(step_id="link")
|
||||
|
||||
if not self._client.is_connected():
|
||||
errors["base"] = "linking"
|
||||
else:
|
||||
await self._client.disconnect()
|
||||
|
||||
if errors:
|
||||
return self.async_show_form(step_id="link", errors=errors)
|
||||
|
||||
assert self._name is not None
|
||||
return self.async_create_entry(
|
||||
title=self._name,
|
||||
data=user_input
|
||||
| {
|
||||
CONF_ADDRESS: self._bdaddr,
|
||||
CONF_ACCESS_TOKEN: token,
|
||||
},
|
||||
)
|
|
@ -0,0 +1,4 @@
|
|||
"""Constants for Keymitt BLE."""
|
||||
# Base component constants
|
||||
DOMAIN = "keymitt_ble"
|
||||
MANUFACTURER = "Naran/Keymitt"
|
|
@ -0,0 +1,56 @@
|
|||
"""Integration to integrate Keymitt BLE devices with Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from microbot import MicroBotApiClient, parse_advertisement_data
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
PassiveBluetoothDataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bleak.backends.device import BLEDevice
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
PLATFORMS: list[str] = [Platform.SWITCH]
|
||||
|
||||
|
||||
class MicroBotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||
"""Class to manage fetching data from the MicroBot."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
client: MicroBotApiClient,
|
||||
ble_device: BLEDevice,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self.api: MicroBotApiClient = client
|
||||
self.data: dict[str, Any] = {}
|
||||
self.ble_device = ble_device
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
ble_device.address,
|
||||
bluetooth.BluetoothScanningMode.ACTIVE,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_handle_bluetooth_event(
|
||||
self,
|
||||
service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||
change: bluetooth.BluetoothChange,
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
if adv := parse_advertisement_data(
|
||||
service_info.device, service_info.advertisement
|
||||
):
|
||||
self.data = adv.data
|
||||
_LOGGER.debug("%s: MicroBot data: %s", self.ble_device.address, self.data)
|
||||
self.api.update_from_advertisement(adv)
|
||||
super()._async_handle_bluetooth_event(service_info, change)
|
|
@ -0,0 +1,39 @@
|
|||
"""MicroBot class."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
PassiveBluetoothCoordinatorEntity,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
|
||||
from .const import MANUFACTURER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MicroBotDataUpdateCoordinator
|
||||
|
||||
|
||||
class MicroBotEntity(PassiveBluetoothCoordinatorEntity):
|
||||
"""Generic entity for all MicroBots."""
|
||||
|
||||
coordinator: MicroBotDataUpdateCoordinator
|
||||
|
||||
def __init__(self, coordinator, config_entry):
|
||||
"""Initialise the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._address = self.coordinator.ble_device.address
|
||||
self._attr_name = "Push"
|
||||
self._attr_unique_id = self._address
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_BLUETOOTH, self._address)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model="Push",
|
||||
name="MicroBot",
|
||||
)
|
||||
|
||||
@property
|
||||
def data(self) -> dict[str, Any]:
|
||||
"""Return coordinator data for this entity."""
|
||||
return self.coordinator.data
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"domain": "keymitt_ble",
|
||||
"name": "Keymitt MicroBot Push",
|
||||
"documentation": "https://www.home-assistant.io/integrations/keymitt_ble",
|
||||
"config_flow": true,
|
||||
"bluetooth": [
|
||||
{
|
||||
"service_uuid": "00001831-0000-1000-8000-00805f9b34fb"
|
||||
},
|
||||
{
|
||||
"service_data_uuid": "00001831-0000-1000-8000-00805f9b34fb"
|
||||
},
|
||||
{
|
||||
"local_name": "mib*"
|
||||
}
|
||||
],
|
||||
"codeowners": ["@spycle"],
|
||||
"requirements": ["PyMicroBot==0.0.6"],
|
||||
"iot_class": "assumed_state",
|
||||
"dependencies": ["bluetooth"],
|
||||
"loggers": ["keymitt_ble"]
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
calibrate:
|
||||
name: Calibrate
|
||||
description: Calibration - Set depth, press & hold duration, and operation mode. Warning - this will send a push command to the device
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity
|
||||
description: Name of entity to calibrate
|
||||
selector:
|
||||
entity:
|
||||
integration: keymitt_ble
|
||||
domain: switch
|
||||
depth:
|
||||
name: Depth
|
||||
description: Depth in percent
|
||||
example: 50
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
mode: slider
|
||||
step: 1
|
||||
min: 0
|
||||
max: 100
|
||||
unit_of_measurement: "%"
|
||||
duration:
|
||||
name: Duration
|
||||
description: Duration in seconds
|
||||
example: 1
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
step: 1
|
||||
min: 0
|
||||
max: 60
|
||||
unit_of_measurement: seconds
|
||||
mode:
|
||||
name: Mode
|
||||
description: normal | invert | toggle
|
||||
example: "normal"
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "normal"
|
||||
- "invert"
|
||||
- "toggle"
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Setup MicroBot device",
|
||||
"data": {
|
||||
"address": "Device address",
|
||||
"name": "Name"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Pairing",
|
||||
"description": "Press the button on the MicroBot Push when the LED is solid pink or green to register with Home Assistant."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"linking": "Failed to pair, please try again. Is the MicroBot in pairing mode?"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"no_unconfigured_devices": "No unconfigured devices found.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
"""Switch platform for MicroBot."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import MicroBotEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MicroBotDataUpdateCoordinator
|
||||
|
||||
CALIBRATE = "calibrate"
|
||||
CALIBRATE_SCHEMA = {
|
||||
vol.Required("depth"): cv.positive_int,
|
||||
vol.Required("duration"): cv.positive_int,
|
||||
vol.Required("mode"): vol.In(["normal", "invert", "toggle"]),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: entity_platform.AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MicroBot based on a config entry."""
|
||||
coordinator: MicroBotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities([MicroBotBinarySwitch(coordinator, entry)])
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
CALIBRATE,
|
||||
CALIBRATE_SCHEMA,
|
||||
"async_calibrate",
|
||||
)
|
||||
|
||||
|
||||
class MicroBotBinarySwitch(MicroBotEntity, SwitchEntity):
|
||||
"""MicroBot switch class."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the switch."""
|
||||
await self.coordinator.api.push_on()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the switch."""
|
||||
await self.coordinator.api.push_off()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the switch is on."""
|
||||
return self.coordinator.api.is_on
|
||||
|
||||
async def async_calibrate(
|
||||
self,
|
||||
depth: int,
|
||||
duration: int,
|
||||
mode: str,
|
||||
) -> None:
|
||||
"""Send calibration commands to the switch."""
|
||||
await self.coordinator.api.calibrate(depth, duration, mode)
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured_device": "Device is already configured",
|
||||
"cannot_connect": "Failed to connect",
|
||||
"no_unconfigured_devices": "No unconfigured devices found.",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"error": {
|
||||
"linking": "Failed to pair, please try again. Is the MicroBot in pairing mode?"
|
||||
},
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"address": "Device address",
|
||||
"name": "Name"
|
||||
},
|
||||
"title": "Setup MicroBot device"
|
||||
},
|
||||
"link": {
|
||||
"description": "Press the button on the MicroBot Push when the LED is solid pink or green to register with Home Assistant.",
|
||||
"title": "Pairing"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -153,6 +153,18 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [
|
|||
"connectable": False,
|
||||
"manufacturer_id": 65535,
|
||||
},
|
||||
{
|
||||
"domain": "keymitt_ble",
|
||||
"service_uuid": "00001831-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
{
|
||||
"domain": "keymitt_ble",
|
||||
"service_data_uuid": "00001831-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
{
|
||||
"domain": "keymitt_ble",
|
||||
"local_name": "mib*",
|
||||
},
|
||||
{
|
||||
"domain": "led_ble",
|
||||
"local_name": "LEDnet*",
|
||||
|
|
|
@ -190,6 +190,7 @@ FLOWS = {
|
|||
"kaleidescape",
|
||||
"keenetic_ndms2",
|
||||
"kegtron",
|
||||
"keymitt_ble",
|
||||
"kmtronic",
|
||||
"knx",
|
||||
"kodi",
|
||||
|
|
|
@ -22,6 +22,9 @@ PyFlick==0.0.2
|
|||
# homeassistant.components.mvglive
|
||||
PyMVGLive==1.1.4
|
||||
|
||||
# homeassistant.components.keymitt_ble
|
||||
PyMicroBot==0.0.6
|
||||
|
||||
# homeassistant.components.mobile_app
|
||||
# homeassistant.components.owntracks
|
||||
PyNaCl==1.5.0
|
||||
|
|
|
@ -18,6 +18,9 @@ HAP-python==4.5.0
|
|||
# homeassistant.components.flick_electric
|
||||
PyFlick==0.0.2
|
||||
|
||||
# homeassistant.components.keymitt_ble
|
||||
PyMicroBot==0.0.6
|
||||
|
||||
# homeassistant.components.mobile_app
|
||||
# homeassistant.components.owntracks
|
||||
PyNaCl==1.5.0
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
"""Tests for the MicroBot integration."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
|
||||
from homeassistant.const import CONF_ADDRESS
|
||||
|
||||
DOMAIN = "keymitt_ble"
|
||||
|
||||
ENTRY_CONFIG = {
|
||||
CONF_ADDRESS: "e7:89:43:99:99:99",
|
||||
}
|
||||
|
||||
USER_INPUT = {
|
||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||
}
|
||||
|
||||
USER_INPUT_INVALID = {
|
||||
CONF_ADDRESS: "invalid-mac",
|
||||
}
|
||||
|
||||
|
||||
def patch_async_setup_entry(return_value=True):
|
||||
"""Patch async setup entry to return True."""
|
||||
return patch(
|
||||
"homeassistant.components.keymitt_ble.async_setup_entry",
|
||||
return_value=return_value,
|
||||
)
|
||||
|
||||
|
||||
SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||
name="mibp",
|
||||
service_uuids=["00001831-0000-1000-8000-00805f9b34fb"],
|
||||
address="aa:bb:cc:dd:ee:ff",
|
||||
manufacturer_data={},
|
||||
service_data={},
|
||||
rssi=-60,
|
||||
source="local",
|
||||
advertisement=AdvertisementData(
|
||||
local_name="mibp",
|
||||
manufacturer_data={},
|
||||
service_uuids=["00001831-0000-1000-8000-00805f9b34fb"],
|
||||
),
|
||||
device=BLEDevice("aa:bb:cc:dd:ee:ff", "mibp"),
|
||||
time=0,
|
||||
connectable=True,
|
||||
)
|
||||
|
||||
|
||||
class MockMicroBotApiClient:
|
||||
"""Mock MicroBotApiClient."""
|
||||
|
||||
def __init__(self, device, token):
|
||||
"""Mock init."""
|
||||
|
||||
async def connect(self, init):
|
||||
"""Mock connect."""
|
||||
|
||||
async def disconnect(self):
|
||||
"""Mock disconnect."""
|
||||
|
||||
def is_connected(self):
|
||||
"""Mock connected."""
|
||||
return True
|
||||
|
||||
|
||||
class MockMicroBotApiClientFail:
|
||||
"""Mock MicroBotApiClient."""
|
||||
|
||||
def __init__(self, device, token):
|
||||
"""Mock init."""
|
||||
|
||||
async def connect(self, init):
|
||||
"""Mock connect."""
|
||||
|
||||
async def disconnect(self):
|
||||
"""Mock disconnect."""
|
||||
|
||||
def is_connected(self):
|
||||
"""Mock disconnected."""
|
||||
return False
|
|
@ -0,0 +1,8 @@
|
|||
"""Define fixtures available for all tests."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_bluetooth(enable_bluetooth):
|
||||
"""Auto mock bluetooth."""
|
|
@ -0,0 +1,173 @@
|
|||
"""Test the MicroBot config flow."""
|
||||
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import (
|
||||
SERVICE_INFO,
|
||||
USER_INPUT,
|
||||
MockMicroBotApiClient,
|
||||
MockMicroBotApiClientFail,
|
||||
patch_async_setup_entry,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
DOMAIN = "keymitt_ble"
|
||||
|
||||
|
||||
async def test_bluetooth_discovery(hass):
|
||||
"""Test discovery via bluetooth with a valid device."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=SERVICE_INFO,
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
with patch_async_setup_entry() as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_bluetooth_discovery_already_setup(hass):
|
||||
"""Test discovery via bluetooth with a valid device when already setup."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||
},
|
||||
unique_id="aa:bb:cc:dd:ee:ff",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=SERVICE_INFO,
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_user_setup(hass):
|
||||
"""Test the user initiated form with valid mac."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info",
|
||||
return_value=[SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["step_id"] == "link"
|
||||
assert result2["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.keymitt_ble.config_flow.MicroBotApiClient",
|
||||
MockMicroBotApiClient,
|
||||
), patch_async_setup_entry() as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result3["result"].data == {
|
||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||
CONF_ACCESS_TOKEN: ANY,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_user_setup_already_configured(hass):
|
||||
"""Test the user initiated form with valid mac."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||
},
|
||||
unique_id="aa:bb:cc:dd:ee:ff",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
with patch(
|
||||
"homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info",
|
||||
return_value=[SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "no_unconfigured_devices"
|
||||
|
||||
|
||||
async def test_user_no_devices(hass):
|
||||
"""Test the user initiated form with valid mac."""
|
||||
with patch(
|
||||
"homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info",
|
||||
return_value=[],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "no_unconfigured_devices"
|
||||
|
||||
|
||||
async def test_no_link(hass):
|
||||
"""Test the user initiated form with invalid response."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info",
|
||||
return_value=[SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["step_id"] == "link"
|
||||
with patch(
|
||||
"homeassistant.components.keymitt_ble.config_flow.MicroBotApiClient",
|
||||
MockMicroBotApiClientFail,
|
||||
), patch_async_setup_entry() as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == FlowResultType.FORM
|
||||
assert result3["step_id"] == "link"
|
||||
assert result3["errors"] == {"base": "linking"}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
Loading…
Reference in New Issue