Add Xiaomi Aqara Config Flow (#35595)

* Xiaomi Aqara Config Flow

* Xiaomi Aqara Config Flow

* Xiaomi Aqara Config Flow

* Xiaomi Aqara Config Flow

* Xiaomi Aqara Config Flow

First tested and working version

* Remove depricated discovery

* Add Xiaomi Aqara Config Flow

* Add Xiaomi Aqara tests

* Update .coveragerc

* Update requirements_test_all.txt

* fix spelling mistake

* fix select scheme

* fix wrong conflict resolve

* add IP to zeroconf discovery title

* black styling

* add getmac requirement

Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>

* add getmac

* add getmac

* Clean up

* Update homeassistant/components/xiaomi_aqara/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/xiaomi_aqara/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/xiaomi_aqara/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/xiaomi_aqara/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/xiaomi_aqara/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* resolve data storage

* move format_mac down

* Remove discovery_retry from config flow

* remove unused strings

* fix styling

* fix black styling

* fix tests

* remove mac connection

This is needed to prevent a conflict with the Xiaomi Miio integration that I discovered during testing.

* fix flake8

* remove getmac depandance

* check for inavlid_interface + test

* Validate gateway key

* add invalid key tests

* Fix spelling

* Only set up sensors if no key

Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
starkillerOG 2020-06-22 11:54:17 +02:00 committed by GitHub
parent 6aba87f3a6
commit 1f9721bad3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1128 additions and 341 deletions

View file

@ -909,7 +909,14 @@ omit =
homeassistant/components/xeoma/camera.py homeassistant/components/xeoma/camera.py
homeassistant/components/xfinity/device_tracker.py homeassistant/components/xfinity/device_tracker.py
homeassistant/components/xiaomi/camera.py homeassistant/components/xiaomi/camera.py
homeassistant/components/xiaomi_aqara/* homeassistant/components/xiaomi_aqara/__init__.py
homeassistant/components/xiaomi_aqara/binary_sensor.py
homeassistant/components/xiaomi_aqara/const.py
homeassistant/components/xiaomi_aqara/cover.py
homeassistant/components/xiaomi_aqara/light.py
homeassistant/components/xiaomi_aqara/lock.py
homeassistant/components/xiaomi_aqara/sensor.py
homeassistant/components/xiaomi_aqara/switch.py
homeassistant/components/xiaomi_miio/__init__.py homeassistant/components/xiaomi_miio/__init__.py
homeassistant/components/xiaomi_miio/air_quality.py homeassistant/components/xiaomi_miio/air_quality.py
homeassistant/components/xiaomi_miio/alarm_control_panel.py homeassistant/components/xiaomi_miio/alarm_control_panel.py

View file

@ -57,7 +57,6 @@ SERVICE_HANDLERS = {
SERVICE_APPLE_TV: ("apple_tv", None), SERVICE_APPLE_TV: ("apple_tv", None),
SERVICE_ENIGMA2: ("media_player", "enigma2"), SERVICE_ENIGMA2: ("media_player", "enigma2"),
SERVICE_WINK: ("wink", None), SERVICE_WINK: ("wink", None),
SERVICE_XIAOMI_GW: ("xiaomi_aqara", None),
SERVICE_SABNZBD: ("sabnzbd", None), SERVICE_SABNZBD: ("sabnzbd", None),
SERVICE_SAMSUNG_PRINTER: ("sensor", "syncthru"), SERVICE_SAMSUNG_PRINTER: ("sensor", "syncthru"),
SERVICE_KONNECTED: ("konnected", None), SERVICE_KONNECTED: ("konnected", None),
@ -92,6 +91,7 @@ MIGRATED_SERVICE_HANDLERS = [
"sonos", "sonos",
"songpal", "songpal",
SERVICE_WEMO, SERVICE_WEMO,
SERVICE_XIAOMI_GW,
] ]
DEFAULT_ENABLED = ( DEFAULT_ENABLED = (

View file

@ -1,11 +1,12 @@
"""Support for Xiaomi Gateways.""" """Support for Xiaomi Gateways."""
import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
import voluptuous as vol import voluptuous as vol
from xiaomi_gateway import XiaomiGatewayDiscovery from xiaomi_gateway import XiaomiGateway, XiaomiGatewayDiscovery
from homeassistant.components.discovery import SERVICE_XIAOMI_GW from homeassistant import config_entries, core
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_BATTERY_LEVEL,
ATTR_VOLTAGE, ATTR_VOLTAGE,
@ -15,29 +16,34 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import discovery from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .const import (
CONF_INTERFACE,
CONF_KEY,
CONF_PROTOCOL,
CONF_SID,
DEFAULT_DISCOVERY_RETRY,
DOMAIN,
GATEWAYS_KEY,
LISTENER_KEY,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
GATEWAY_PLATFORMS = ["binary_sensor", "sensor", "switch", "light", "cover", "lock"]
GATEWAY_PLATFORMS_NO_KEY = ["binary_sensor", "sensor"]
ATTR_GW_MAC = "gw_mac" ATTR_GW_MAC = "gw_mac"
ATTR_RINGTONE_ID = "ringtone_id" ATTR_RINGTONE_ID = "ringtone_id"
ATTR_RINGTONE_VOL = "ringtone_vol" ATTR_RINGTONE_VOL = "ringtone_vol"
ATTR_DEVICE_ID = "device_id" ATTR_DEVICE_ID = "device_id"
CONF_DISCOVERY_RETRY = "discovery_retry"
CONF_GATEWAYS = "gateways"
CONF_INTERFACE = "interface"
CONF_KEY = "key"
CONF_DISABLE = "disable"
DOMAIN = "xiaomi_aqara"
PY_XIAOMI_GATEWAY = "xiaomi_gw"
TIME_TILL_UNAVAILABLE = timedelta(minutes=150) TIME_TILL_UNAVAILABLE = timedelta(minutes=150)
SERVICE_PLAY_RINGTONE = "play_ringtone" SERVICE_PLAY_RINGTONE = "play_ringtone"
@ -45,10 +51,6 @@ SERVICE_STOP_RINGTONE = "stop_ringtone"
SERVICE_ADD_DEVICE = "add_device" SERVICE_ADD_DEVICE = "add_device"
SERVICE_REMOVE_DEVICE = "remove_device" SERVICE_REMOVE_DEVICE = "remove_device"
GW_MAC = vol.All(
cv.string, lambda value: value.replace(":", "").lower(), vol.Length(min=12, max=12)
)
SERVICE_SCHEMA_PLAY_RINGTONE = vol.Schema( SERVICE_SCHEMA_PLAY_RINGTONE = vol.Schema(
{ {
vol.Required(ATTR_RINGTONE_ID): vol.All( vol.Required(ATTR_RINGTONE_ID): vol.All(
@ -65,102 +67,8 @@ SERVICE_SCHEMA_REMOVE_DEVICE = vol.Schema(
) )
GATEWAY_CONFIG = vol.Schema(
{
vol.Optional(CONF_KEY): vol.All(cv.string, vol.Length(min=16, max=16)),
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=9898): cv.port,
vol.Optional(CONF_DISABLE, default=False): cv.boolean,
}
)
GATEWAY_CONFIG_MAC_OPTIONAL = GATEWAY_CONFIG.extend({vol.Optional(CONF_MAC): GW_MAC})
GATEWAY_CONFIG_MAC_REQUIRED = GATEWAY_CONFIG.extend({vol.Required(CONF_MAC): GW_MAC})
def _fix_conf_defaults(config):
"""Update some configuration defaults."""
config["sid"] = config.pop(CONF_MAC, None)
if config.get(CONF_KEY) is None:
_LOGGER.warning(
"Key is not provided for gateway %s. Controlling the gateway "
"will not be possible",
config["sid"],
)
if config.get(CONF_HOST) is None:
config.pop(CONF_PORT)
return config
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(CONF_GATEWAYS, default={}): vol.All(
cv.ensure_list,
vol.Any(
vol.All([GATEWAY_CONFIG_MAC_OPTIONAL], vol.Length(max=1)),
vol.All([GATEWAY_CONFIG_MAC_REQUIRED], vol.Length(min=2)),
),
[_fix_conf_defaults],
),
vol.Optional(CONF_INTERFACE, default="any"): cv.string,
vol.Optional(CONF_DISCOVERY_RETRY, default=3): cv.positive_int,
}
)
},
extra=vol.ALLOW_EXTRA,
)
def setup(hass, config): def setup(hass, config):
"""Set up the Xiaomi component.""" """Set up the Xiaomi component."""
gateways = []
interface = "any"
discovery_retry = 3
if DOMAIN in config:
gateways = config[DOMAIN][CONF_GATEWAYS]
interface = config[DOMAIN][CONF_INTERFACE]
discovery_retry = config[DOMAIN][CONF_DISCOVERY_RETRY]
async def xiaomi_gw_discovered(service, discovery_info):
"""Perform action when Xiaomi Gateway device(s) has been found."""
# We don't need to do anything here, the purpose of Home Assistant's
# discovery service is to just trigger loading of this
# component, and then its own discovery process kicks in.
discovery.listen(hass, SERVICE_XIAOMI_GW, xiaomi_gw_discovered)
xiaomi = hass.data[PY_XIAOMI_GATEWAY] = XiaomiGatewayDiscovery(
hass.add_job, gateways, interface
)
_LOGGER.debug("Expecting %s gateways", len(gateways))
for k in range(discovery_retry):
_LOGGER.info("Discovering Xiaomi Gateways (Try %s)", k + 1)
xiaomi.discover_gateways()
if len(xiaomi.gateways) >= len(gateways):
break
if not xiaomi.gateways:
_LOGGER.error("No gateway discovered")
return False
xiaomi.listen()
_LOGGER.debug("Gateways discovered. Listening for broadcasts")
for component in ["binary_sensor", "sensor", "switch", "light", "cover", "lock"]:
discovery.load_platform(hass, component, DOMAIN, {}, config)
def stop_xiaomi(event):
"""Stop Xiaomi Socket."""
_LOGGER.info("Shutting down Xiaomi Hub")
xiaomi.stop_listen()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_xiaomi)
def play_ringtone_service(call): def play_ringtone_service(call):
"""Service to play ringtone through Gateway.""" """Service to play ringtone through Gateway."""
@ -196,13 +104,13 @@ def setup(hass, config):
gateway = call.data.get(ATTR_GW_MAC) gateway = call.data.get(ATTR_GW_MAC)
gateway.write_to_hub(gateway.sid, remove_device=device_id) gateway.write_to_hub(gateway.sid, remove_device=device_id)
gateway_only_schema = _add_gateway_to_schema(xiaomi, vol.Schema({})) gateway_only_schema = _add_gateway_to_schema(hass, vol.Schema({}))
hass.services.register( hass.services.register(
DOMAIN, DOMAIN,
SERVICE_PLAY_RINGTONE, SERVICE_PLAY_RINGTONE,
play_ringtone_service, play_ringtone_service,
schema=_add_gateway_to_schema(xiaomi, SERVICE_SCHEMA_PLAY_RINGTONE), schema=_add_gateway_to_schema(hass, SERVICE_SCHEMA_PLAY_RINGTONE),
) )
hass.services.register( hass.services.register(
@ -217,21 +125,119 @@ def setup(hass, config):
DOMAIN, DOMAIN,
SERVICE_REMOVE_DEVICE, SERVICE_REMOVE_DEVICE,
remove_device_service, remove_device_service,
schema=_add_gateway_to_schema(xiaomi, SERVICE_SCHEMA_REMOVE_DEVICE), schema=_add_gateway_to_schema(hass, SERVICE_SCHEMA_REMOVE_DEVICE),
) )
return True return True
async def async_setup_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
"""Set up the xiaomi aqara components from a config entry."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(GATEWAYS_KEY, {})
# Connect to Xiaomi Aqara Gateway
xiaomi_gateway = await hass.async_add_executor_job(
XiaomiGateway,
entry.data[CONF_HOST],
entry.data[CONF_PORT],
entry.data[CONF_SID],
entry.data[CONF_KEY],
DEFAULT_DISCOVERY_RETRY,
entry.data[CONF_INTERFACE],
entry.data[CONF_PROTOCOL],
)
hass.data[DOMAIN][GATEWAYS_KEY][entry.entry_id] = xiaomi_gateway
gateway_discovery = hass.data[DOMAIN].setdefault(
LISTENER_KEY,
XiaomiGatewayDiscovery(hass.add_job, [], entry.data[CONF_INTERFACE]),
)
if len(hass.data[DOMAIN][GATEWAYS_KEY]) == 1:
# start listining for local pushes (only once)
await hass.async_add_executor_job(gateway_discovery.listen)
# register stop callback to shutdown listining for local pushes
def stop_xiaomi(event):
"""Stop Xiaomi Socket."""
_LOGGER.debug("Shutting down Xiaomi Gateway Listener")
gateway_discovery.stop_listen()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_xiaomi)
gateway_discovery.gateways[entry.data[CONF_HOST]] = xiaomi_gateway
_LOGGER.debug(
"Gateway with host '%s' connected, listening for broadcasts",
entry.data[CONF_HOST],
)
device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.unique_id)},
manufacturer="Xiaomi Aqara",
name=entry.title,
sw_version=entry.data[CONF_PROTOCOL],
)
if entry.data[CONF_KEY] is not None:
platforms = GATEWAY_PLATFORMS
else:
platforms = GATEWAY_PLATFORMS_NO_KEY
for component in platforms:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
"""Unload a config entry."""
if entry.data[CONF_KEY] is not None:
platforms = GATEWAY_PLATFORMS
else:
platforms = GATEWAY_PLATFORMS_NO_KEY
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in platforms
]
)
)
if unload_ok:
hass.data[DOMAIN][GATEWAYS_KEY].pop(entry.entry_id)
if len(hass.data[DOMAIN][GATEWAYS_KEY]) == 0:
# No gateways left, stop Xiaomi socket
hass.data[DOMAIN].pop(GATEWAYS_KEY)
_LOGGER.debug("Shutting down Xiaomi Gateway Listener")
gateway_discovery = hass.data[DOMAIN].pop(LISTENER_KEY)
await hass.async_add_executor_job(gateway_discovery.stop_listen)
return unload_ok
class XiaomiDevice(Entity): class XiaomiDevice(Entity):
"""Representation a base Xiaomi device.""" """Representation a base Xiaomi device."""
def __init__(self, device, device_type, xiaomi_hub): def __init__(self, device, device_type, xiaomi_hub, config_entry):
"""Initialize the Xiaomi device.""" """Initialize the Xiaomi device."""
self._state = None self._state = None
self._is_available = True self._is_available = True
self._sid = device["sid"] self._sid = device["sid"]
self._model = device["model"]
self._protocol = device["proto"]
self._name = f"{device_type}_{self._sid}" self._name = f"{device_type}_{self._sid}"
self._device_name = f"{self._model}_{self._sid}"
self._type = device_type self._type = device_type
self._write_to_hub = xiaomi_hub.write_to_hub self._write_to_hub = xiaomi_hub.write_to_hub
self._get_from_hub = xiaomi_hub.get_from_hub self._get_from_hub = xiaomi_hub.get_from_hub
@ -248,6 +254,16 @@ class XiaomiDevice(Entity):
else: else:
self._unique_id = f"{self._type}{self._sid}" self._unique_id = f"{self._type}{self._sid}"
self._gateway_id = config_entry.unique_id
if config_entry.data[CONF_MAC] == format_mac(self._sid):
# this entity belongs to the gateway itself
self._is_gateway = True
self._device_id = config_entry.unique_id
else:
# this entity is connected through zigbee
self._is_gateway = False
self._device_id = self._sid
def _add_push_data_job(self, *args): def _add_push_data_job(self, *args):
self.hass.add_job(self.push_data, *args) self.hass.add_job(self.push_data, *args)
@ -266,6 +282,32 @@ class XiaomiDevice(Entity):
"""Return a unique ID.""" """Return a unique ID."""
return self._unique_id return self._unique_id
@property
def device_id(self):
"""Return the device id of the Xiaomi Aqara device."""
return self._device_id
@property
def device_info(self):
"""Return the device info of the Xiaomi Aqara device."""
if self._is_gateway:
device_info = {
"identifiers": {(DOMAIN, self._device_id)},
"model": self._model,
}
else:
device_info = {
"connections": {(dr.CONNECTION_ZIGBEE, self._device_id)},
"identifiers": {(DOMAIN, self._device_id)},
"manufacturer": "Xiaomi Aqara",
"model": self._model,
"name": self._device_name,
"sw_version": self._protocol,
"via_device": (DOMAIN, self._gateway_id),
}
return device_info
@property @property
def available(self): def available(self):
"""Return True if entity is available.""" """Return True if entity is available."""
@ -334,24 +376,26 @@ class XiaomiDevice(Entity):
raise NotImplementedError() raise NotImplementedError()
def _add_gateway_to_schema(xiaomi, schema): def _add_gateway_to_schema(hass, schema):
"""Extend a voluptuous schema with a gateway validator.""" """Extend a voluptuous schema with a gateway validator."""
def gateway(sid): def gateway(sid):
"""Convert sid to a gateway.""" """Convert sid to a gateway."""
sid = str(sid).replace(":", "").lower() sid = str(sid).replace(":", "").lower()
for gateway in xiaomi.gateways.values(): for gateway in hass.data[DOMAIN][GATEWAYS_KEY].values():
if gateway.sid == sid: if gateway.sid == sid:
return gateway return gateway
raise vol.Invalid(f"Unknown gateway sid {sid}") raise vol.Invalid(f"Unknown gateway sid {sid}")
gateways = list(xiaomi.gateways.values())
kwargs = {} kwargs = {}
xiaomi_data = hass.data.get(DOMAIN)
if xiaomi_data is not None:
gateways = list(xiaomi_data[GATEWAYS_KEY].values())
# If the user has only 1 gateway, make it the default for services. # If the user has only 1 gateway, make it the default for services.
if len(gateways) == 1: if len(gateways) == 1:
kwargs["default"] = gateways[0].sid kwargs["default"] = gateways[0].sid
return schema.extend({vol.Required(ATTR_GW_MAC, **kwargs): gateway}) return schema.extend({vol.Required(ATTR_GW_MAC, **kwargs): gateway})

View file

@ -5,7 +5,8 @@ from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from . import PY_XIAOMI_GATEWAY, XiaomiDevice from . import XiaomiDevice
from .const import DOMAIN, GATEWAYS_KEY
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -21,94 +22,115 @@ DENSITY = "density"
ATTR_DENSITY = "Density" ATTR_DENSITY = "Density"
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Perform the setup for Xiaomi devices.""" """Perform the setup for Xiaomi devices."""
devices = [] entities = []
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
for device in gateway.devices["binary_sensor"]: for entity in gateway.devices["binary_sensor"]:
model = device["model"] model = entity["model"]
if model in ["motion", "sensor_motion", "sensor_motion.aq2"]: if model in ["motion", "sensor_motion", "sensor_motion.aq2"]:
devices.append(XiaomiMotionSensor(device, hass, gateway)) entities.append(XiaomiMotionSensor(entity, hass, gateway, config_entry))
elif model in ["magnet", "sensor_magnet", "sensor_magnet.aq2"]: elif model in ["magnet", "sensor_magnet", "sensor_magnet.aq2"]:
devices.append(XiaomiDoorSensor(device, gateway)) entities.append(XiaomiDoorSensor(entity, gateway, config_entry))
elif model == "sensor_wleak.aq1": elif model == "sensor_wleak.aq1":
devices.append(XiaomiWaterLeakSensor(device, gateway)) entities.append(XiaomiWaterLeakSensor(entity, gateway, config_entry))
elif model in ["smoke", "sensor_smoke"]: elif model in ["smoke", "sensor_smoke"]:
devices.append(XiaomiSmokeSensor(device, gateway)) entities.append(XiaomiSmokeSensor(entity, gateway, config_entry))
elif model in ["natgas", "sensor_natgas"]: elif model in ["natgas", "sensor_natgas"]:
devices.append(XiaomiNatgasSensor(device, gateway)) entities.append(XiaomiNatgasSensor(entity, gateway, config_entry))
elif model in [ elif model in [
"switch", "switch",
"sensor_switch", "sensor_switch",
"sensor_switch.aq2", "sensor_switch.aq2",
"sensor_switch.aq3", "sensor_switch.aq3",
"remote.b1acn01", "remote.b1acn01",
]: ]:
if "proto" not in device or int(device["proto"][0:1]) == 1: if "proto" not in entity or int(entity["proto"][0:1]) == 1:
data_key = "status" data_key = "status"
else:
data_key = "button_0"
devices.append(XiaomiButton(device, "Switch", data_key, hass, gateway))
elif model in [
"86sw1",
"sensor_86sw1",
"sensor_86sw1.aq1",
"remote.b186acn01",
]:
if "proto" not in device or int(device["proto"][0:1]) == 1:
data_key = "channel_0"
else:
data_key = "button_0"
devices.append(
XiaomiButton(device, "Wall Switch", data_key, hass, gateway)
)
elif model in [
"86sw2",
"sensor_86sw2",
"sensor_86sw2.aq1",
"remote.b286acn01",
]:
if "proto" not in device or int(device["proto"][0:1]) == 1:
data_key_left = "channel_0"
data_key_right = "channel_1"
else:
data_key_left = "button_0"
data_key_right = "button_1"
devices.append(
XiaomiButton(
device, "Wall Switch (Left)", data_key_left, hass, gateway
)
)
devices.append(
XiaomiButton(
device, "Wall Switch (Right)", data_key_right, hass, gateway
)
)
devices.append(
XiaomiButton(
device, "Wall Switch (Both)", "dual_channel", hass, gateway
)
)
elif model in ["cube", "sensor_cube", "sensor_cube.aqgl01"]:
devices.append(XiaomiCube(device, hass, gateway))
elif model in ["vibration", "vibration.aq1"]:
devices.append(XiaomiVibration(device, "Vibration", "status", gateway))
else: else:
_LOGGER.warning("Unmapped Device Model %s", model) data_key = "button_0"
entities.append(
XiaomiButton(entity, "Switch", data_key, hass, gateway, config_entry)
)
elif model in [
"86sw1",
"sensor_86sw1",
"sensor_86sw1.aq1",
"remote.b186acn01",
]:
if "proto" not in entity or int(entity["proto"][0:1]) == 1:
data_key = "channel_0"
else:
data_key = "button_0"
entities.append(
XiaomiButton(
entity, "Wall Switch", data_key, hass, gateway, config_entry
)
)
elif model in [
"86sw2",
"sensor_86sw2",
"sensor_86sw2.aq1",
"remote.b286acn01",
]:
if "proto" not in entity or int(entity["proto"][0:1]) == 1:
data_key_left = "channel_0"
data_key_right = "channel_1"
else:
data_key_left = "button_0"
data_key_right = "button_1"
entities.append(
XiaomiButton(
entity,
"Wall Switch (Left)",
data_key_left,
hass,
gateway,
config_entry,
)
)
entities.append(
XiaomiButton(
entity,
"Wall Switch (Right)",
data_key_right,
hass,
gateway,
config_entry,
)
)
entities.append(
XiaomiButton(
entity,
"Wall Switch (Both)",
"dual_channel",
hass,
gateway,
config_entry,
)
)
elif model in ["cube", "sensor_cube", "sensor_cube.aqgl01"]:
entities.append(XiaomiCube(entity, hass, gateway, config_entry))
elif model in ["vibration", "vibration.aq1"]:
entities.append(
XiaomiVibration(entity, "Vibration", "status", gateway, config_entry)
)
else:
_LOGGER.warning("Unmapped Device Model %s", model)
add_entities(devices) async_add_entities(entities)
class XiaomiBinarySensor(XiaomiDevice, BinarySensorEntity): class XiaomiBinarySensor(XiaomiDevice, BinarySensorEntity):
"""Representation of a base XiaomiBinarySensor.""" """Representation of a base XiaomiBinarySensor."""
def __init__(self, device, name, xiaomi_hub, data_key, device_class): def __init__(self, device, name, xiaomi_hub, data_key, device_class, config_entry):
"""Initialize the XiaomiSmokeSensor.""" """Initialize the XiaomiSmokeSensor."""
self._data_key = data_key self._data_key = data_key
self._device_class = device_class self._device_class = device_class
self._should_poll = False self._should_poll = False
self._density = 0 self._density = 0
XiaomiDevice.__init__(self, device, name, xiaomi_hub) super().__init__(device, name, xiaomi_hub, config_entry)
@property @property
def should_poll(self): def should_poll(self):
@ -134,11 +156,11 @@ class XiaomiBinarySensor(XiaomiDevice, BinarySensorEntity):
class XiaomiNatgasSensor(XiaomiBinarySensor): class XiaomiNatgasSensor(XiaomiBinarySensor):
"""Representation of a XiaomiNatgasSensor.""" """Representation of a XiaomiNatgasSensor."""
def __init__(self, device, xiaomi_hub): def __init__(self, device, xiaomi_hub, config_entry):
"""Initialize the XiaomiSmokeSensor.""" """Initialize the XiaomiSmokeSensor."""
self._density = None self._density = None
XiaomiBinarySensor.__init__( super().__init__(
self, device, "Natgas Sensor", xiaomi_hub, "alarm", "gas" device, "Natgas Sensor", xiaomi_hub, "alarm", "gas", config_entry
) )
@property @property
@ -172,7 +194,7 @@ class XiaomiNatgasSensor(XiaomiBinarySensor):
class XiaomiMotionSensor(XiaomiBinarySensor): class XiaomiMotionSensor(XiaomiBinarySensor):
"""Representation of a XiaomiMotionSensor.""" """Representation of a XiaomiMotionSensor."""
def __init__(self, device, hass, xiaomi_hub): def __init__(self, device, hass, xiaomi_hub, config_entry):
"""Initialize the XiaomiMotionSensor.""" """Initialize the XiaomiMotionSensor."""
self._hass = hass self._hass = hass
self._no_motion_since = 0 self._no_motion_since = 0
@ -181,8 +203,8 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
data_key = "status" data_key = "status"
else: else:
data_key = "motion_status" data_key = "motion_status"
XiaomiBinarySensor.__init__( super().__init__(
self, device, "Motion Sensor", xiaomi_hub, data_key, "motion" device, "Motion Sensor", xiaomi_hub, data_key, "motion", config_entry
) )
@property @property
@ -263,15 +285,15 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
class XiaomiDoorSensor(XiaomiBinarySensor): class XiaomiDoorSensor(XiaomiBinarySensor):
"""Representation of a XiaomiDoorSensor.""" """Representation of a XiaomiDoorSensor."""
def __init__(self, device, xiaomi_hub): def __init__(self, device, xiaomi_hub, config_entry):
"""Initialize the XiaomiDoorSensor.""" """Initialize the XiaomiDoorSensor."""
self._open_since = 0 self._open_since = 0
if "proto" not in device or int(device["proto"][0:1]) == 1: if "proto" not in device or int(device["proto"][0:1]) == 1:
data_key = "status" data_key = "status"
else: else:
data_key = "window_status" data_key = "window_status"
XiaomiBinarySensor.__init__( super().__init__(
self, device, "Door Window Sensor", xiaomi_hub, data_key, "opening" device, "Door Window Sensor", xiaomi_hub, data_key, "opening", config_entry,
) )
@property @property
@ -309,14 +331,14 @@ class XiaomiDoorSensor(XiaomiBinarySensor):
class XiaomiWaterLeakSensor(XiaomiBinarySensor): class XiaomiWaterLeakSensor(XiaomiBinarySensor):
"""Representation of a XiaomiWaterLeakSensor.""" """Representation of a XiaomiWaterLeakSensor."""
def __init__(self, device, xiaomi_hub): def __init__(self, device, xiaomi_hub, config_entry):
"""Initialize the XiaomiWaterLeakSensor.""" """Initialize the XiaomiWaterLeakSensor."""
if "proto" not in device or int(device["proto"][0:1]) == 1: if "proto" not in device or int(device["proto"][0:1]) == 1:
data_key = "status" data_key = "status"
else: else:
data_key = "wleak_status" data_key = "wleak_status"
XiaomiBinarySensor.__init__( super().__init__(
self, device, "Water Leak Sensor", xiaomi_hub, data_key, "moisture" device, "Water Leak Sensor", xiaomi_hub, data_key, "moisture", config_entry,
) )
def parse_data(self, data, raw_data): def parse_data(self, data, raw_data):
@ -343,11 +365,11 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor):
class XiaomiSmokeSensor(XiaomiBinarySensor): class XiaomiSmokeSensor(XiaomiBinarySensor):
"""Representation of a XiaomiSmokeSensor.""" """Representation of a XiaomiSmokeSensor."""
def __init__(self, device, xiaomi_hub): def __init__(self, device, xiaomi_hub, config_entry):
"""Initialize the XiaomiSmokeSensor.""" """Initialize the XiaomiSmokeSensor."""
self._density = 0 self._density = 0
XiaomiBinarySensor.__init__( super().__init__(
self, device, "Smoke Sensor", xiaomi_hub, "alarm", "smoke" device, "Smoke Sensor", xiaomi_hub, "alarm", "smoke", config_entry
) )
@property @property
@ -380,10 +402,10 @@ class XiaomiSmokeSensor(XiaomiBinarySensor):
class XiaomiVibration(XiaomiBinarySensor): class XiaomiVibration(XiaomiBinarySensor):
"""Representation of a Xiaomi Vibration Sensor.""" """Representation of a Xiaomi Vibration Sensor."""
def __init__(self, device, name, data_key, xiaomi_hub): def __init__(self, device, name, data_key, xiaomi_hub, config_entry):
"""Initialize the XiaomiVibration.""" """Initialize the XiaomiVibration."""
self._last_action = None self._last_action = None
super().__init__(device, name, xiaomi_hub, data_key, None) super().__init__(device, name, xiaomi_hub, data_key, None, config_entry)
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -414,11 +436,11 @@ class XiaomiVibration(XiaomiBinarySensor):
class XiaomiButton(XiaomiBinarySensor): class XiaomiButton(XiaomiBinarySensor):
"""Representation of a Xiaomi Button.""" """Representation of a Xiaomi Button."""
def __init__(self, device, name, data_key, hass, xiaomi_hub): def __init__(self, device, name, data_key, hass, xiaomi_hub, config_entry):
"""Initialize the XiaomiButton.""" """Initialize the XiaomiButton."""
self._hass = hass self._hass = hass
self._last_action = None self._last_action = None
XiaomiBinarySensor.__init__(self, device, name, xiaomi_hub, data_key, None) super().__init__(device, name, xiaomi_hub, data_key, None, config_entry)
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -469,7 +491,7 @@ class XiaomiButton(XiaomiBinarySensor):
class XiaomiCube(XiaomiBinarySensor): class XiaomiCube(XiaomiBinarySensor):
"""Representation of a Xiaomi Cube.""" """Representation of a Xiaomi Cube."""
def __init__(self, device, hass, xiaomi_hub): def __init__(self, device, hass, xiaomi_hub, config_entry):
"""Initialize the Xiaomi Cube.""" """Initialize the Xiaomi Cube."""
self._hass = hass self._hass = hass
self._last_action = None self._last_action = None
@ -478,7 +500,7 @@ class XiaomiCube(XiaomiBinarySensor):
data_key = "status" data_key = "status"
else: else:
data_key = "cube_status" data_key = "cube_status"
XiaomiBinarySensor.__init__(self, device, "Cube", xiaomi_hub, data_key, None) super().__init__(device, "Cube", xiaomi_hub, data_key, None, config_entry)
@property @property
def device_state_attributes(self): def device_state_attributes(self):

View file

@ -0,0 +1,183 @@
"""Config flow to configure Xiaomi Aqara."""
import logging
from socket import gaierror
import voluptuous as vol
from xiaomi_gateway import XiaomiGatewayDiscovery
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT
from homeassistant.helpers.device_registry import format_mac
# pylint: disable=unused-import
from .const import (
CONF_INTERFACE,
CONF_KEY,
CONF_PROTOCOL,
CONF_SID,
DOMAIN,
ZEROCONF_GATEWAY,
)
_LOGGER = logging.getLogger(__name__)
DEFAULT_GATEWAY_NAME = "Xiaomi Aqara Gateway"
DEFAULT_INTERFACE = "any"
GATEWAY_CONFIG = vol.Schema(
{vol.Optional(CONF_INTERFACE, default=DEFAULT_INTERFACE): str}
)
GATEWAY_SETTINGS = vol.Schema(
{
vol.Optional(CONF_KEY): vol.All(str, vol.Length(min=16, max=16)),
vol.Optional(CONF_NAME, default=DEFAULT_GATEWAY_NAME): str,
}
)
class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Xiaomi Aqara config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
def __init__(self):
"""Initialize."""
self.host = None
self.interface = DEFAULT_INTERFACE
self.gateways = None
self.selected_gateway = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
errors = {}
if user_input is not None:
self.interface = user_input[CONF_INTERFACE]
# Discover Xiaomi Aqara Gateways in the netwerk to get required SIDs.
xiaomi = XiaomiGatewayDiscovery(self.hass.add_job, [], self.interface)
try:
await self.hass.async_add_executor_job(xiaomi.discover_gateways)
except gaierror:
errors[CONF_INTERFACE] = "invalid_interface"
if not errors:
self.gateways = xiaomi.gateways
# if host is already known by zeroconf discovery
if self.host is not None:
self.selected_gateway = self.gateways.get(self.host)
if self.selected_gateway is not None:
return await self.async_step_settings()
errors["base"] = "not_found_error"
else:
if len(self.gateways) == 1:
self.selected_gateway = list(self.gateways.values())[0]
return await self.async_step_settings()
if len(self.gateways) > 1:
return await self.async_step_select()
errors["base"] = "discovery_error"
return self.async_show_form(
step_id="user", data_schema=GATEWAY_CONFIG, errors=errors
)
async def async_step_select(self, user_input=None):
"""Handle multiple aqara gateways found."""
errors = {}
if user_input is not None:
ip_adress = user_input["select_ip"]
self.selected_gateway = self.gateways[ip_adress]
return await self.async_step_settings()
select_schema = vol.Schema(
{
vol.Required("select_ip"): vol.In(
[gateway.ip_adress for gateway in self.gateways.values()]
)
}
)
return self.async_show_form(
step_id="select", data_schema=select_schema, errors=errors
)
async def async_step_zeroconf(self, discovery_info):
"""Handle zeroconf discovery."""
name = discovery_info.get("name")
self.host = discovery_info.get("host")
mac_address = discovery_info.get("properties", {}).get("mac")
if not name or not self.host or not mac_address:
return self.async_abort(reason="not_xiaomi_aqara")
# Check if the discovered device is an xiaomi aqara gateway.
if not name.startswith(ZEROCONF_GATEWAY):
_LOGGER.debug(
"Xiaomi device '%s' discovered with host %s, not identified as xiaomi aqara gateway",
name,
self.host,
)
return self.async_abort(reason="not_xiaomi_aqara")
# format mac (include semicolns and make uppercase)
mac_address = format_mac(mac_address)
unique_id = mac_address
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured({CONF_HOST: self.host})
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": {"name": self.host}})
return await self.async_step_user()
async def async_step_settings(self, user_input=None):
"""Specify settings and connect aqara gateway."""
errors = {}
if user_input is not None:
# get all required data
name = user_input[CONF_NAME]
key = user_input.get(CONF_KEY)
ip_adress = self.selected_gateway.ip_adress
port = self.selected_gateway.port
sid = self.selected_gateway.sid
protocol = self.selected_gateway.proto
if key is not None:
# validate key by issuing stop ringtone playback command.
self.selected_gateway.key = key
valid_key = self.selected_gateway.write_to_hub(sid, mid=10000)
else:
valid_key = True
if valid_key:
# format_mac, for a gateway the sid equels the mac address
mac_address = format_mac(sid)
# set unique_id
unique_id = mac_address
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=name,
data={
CONF_HOST: ip_adress,
CONF_PORT: port,
CONF_MAC: mac_address,
CONF_INTERFACE: self.interface,
CONF_PROTOCOL: protocol,
CONF_KEY: key,
CONF_SID: sid,
},
)
errors[CONF_KEY] = "invalid_key"
return self.async_show_form(
step_id="settings", data_schema=GATEWAY_SETTINGS, errors=errors
)

View file

@ -0,0 +1,15 @@
"""Constants of the Xiaomi Aqara component."""
DOMAIN = "xiaomi_aqara"
GATEWAYS_KEY = "gateways"
LISTENER_KEY = "listener"
ZEROCONF_GATEWAY = "lumi-gateway"
CONF_INTERFACE = "interface"
CONF_PROTOCOL = "protocol"
CONF_KEY = "key"
CONF_SID = "sid"
DEFAULT_DISCOVERY_RETRY = 5

View file

@ -3,7 +3,8 @@ import logging
from homeassistant.components.cover import ATTR_POSITION, CoverEntity from homeassistant.components.cover import ATTR_POSITION, CoverEntity
from . import PY_XIAOMI_GATEWAY, XiaomiDevice from . import XiaomiDevice
from .const import DOMAIN, GATEWAYS_KEY
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -13,29 +14,31 @@ DATA_KEY_PROTO_V1 = "status"
DATA_KEY_PROTO_V2 = "curtain_status" DATA_KEY_PROTO_V2 = "curtain_status"
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Perform the setup for Xiaomi devices.""" """Perform the setup for Xiaomi devices."""
devices = [] entities = []
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
for device in gateway.devices["cover"]: for device in gateway.devices["cover"]:
model = device["model"] model = device["model"]
if model in ["curtain", "curtain.aq2", "curtain.hagl04"]: if model in ["curtain", "curtain.aq2", "curtain.hagl04"]:
if "proto" not in device or int(device["proto"][0:1]) == 1: if "proto" not in device or int(device["proto"][0:1]) == 1:
data_key = DATA_KEY_PROTO_V1 data_key = DATA_KEY_PROTO_V1
else: else:
data_key = DATA_KEY_PROTO_V2 data_key = DATA_KEY_PROTO_V2
devices.append(XiaomiGenericCover(device, "Curtain", data_key, gateway)) entities.append(
add_entities(devices) XiaomiGenericCover(device, "Curtain", data_key, gateway, config_entry)
)
async_add_entities(entities)
class XiaomiGenericCover(XiaomiDevice, CoverEntity): class XiaomiGenericCover(XiaomiDevice, CoverEntity):
"""Representation of a XiaomiGenericCover.""" """Representation of a XiaomiGenericCover."""
def __init__(self, device, name, data_key, xiaomi_hub): def __init__(self, device, name, data_key, xiaomi_hub, config_entry):
"""Initialize the XiaomiGenericCover.""" """Initialize the XiaomiGenericCover."""
self._data_key = data_key self._data_key = data_key
self._pos = 0 self._pos = 0
XiaomiDevice.__init__(self, device, name, xiaomi_hub) super().__init__(device, name, xiaomi_hub, config_entry)
@property @property
def current_cover_position(self): def current_cover_position(self):

View file

@ -12,32 +12,35 @@ from homeassistant.components.light import (
) )
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from . import PY_XIAOMI_GATEWAY, XiaomiDevice from . import XiaomiDevice
from .const import DOMAIN, GATEWAYS_KEY
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Perform the setup for Xiaomi devices.""" """Perform the setup for Xiaomi devices."""
devices = [] entities = []
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
for device in gateway.devices["light"]: for device in gateway.devices["light"]:
model = device["model"] model = device["model"]
if model in ["gateway", "gateway.v3"]: if model in ["gateway", "gateway.v3"]:
devices.append(XiaomiGatewayLight(device, "Gateway Light", gateway)) entities.append(
add_entities(devices) XiaomiGatewayLight(device, "Gateway Light", gateway, config_entry)
)
async_add_entities(entities)
class XiaomiGatewayLight(XiaomiDevice, LightEntity): class XiaomiGatewayLight(XiaomiDevice, LightEntity):
"""Representation of a XiaomiGatewayLight.""" """Representation of a XiaomiGatewayLight."""
def __init__(self, device, name, xiaomi_hub): def __init__(self, device, name, xiaomi_hub, config_entry):
"""Initialize the XiaomiGatewayLight.""" """Initialize the XiaomiGatewayLight."""
self._data_key = "rgb" self._data_key = "rgb"
self._hs = (0, 0) self._hs = (0, 0)
self._brightness = 100 self._brightness = 100
XiaomiDevice.__init__(self, device, name, xiaomi_hub) super().__init__(device, name, xiaomi_hub, config_entry)
@property @property
def is_on(self): def is_on(self):

View file

@ -6,7 +6,8 @@ from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from . import PY_XIAOMI_GATEWAY, XiaomiDevice from . import XiaomiDevice
from .const import DOMAIN, GATEWAYS_KEY
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -20,27 +21,26 @@ ATTR_VERIFIED_WRONG_TIMES = "verified_wrong_times"
UNLOCK_MAINTAIN_TIME = 5 UNLOCK_MAINTAIN_TIME = 5
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Perform the setup for Xiaomi devices.""" """Perform the setup for Xiaomi devices."""
devices = [] entities = []
gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
for gateway in hass.data[PY_XIAOMI_GATEWAY].gateways.values(): for device in gateway.devices["lock"]:
for device in gateway.devices["lock"]: model = device["model"]
model = device["model"] if model == "lock.aq1":
if model == "lock.aq1": entities.append(XiaomiAqaraLock(device, "Lock", gateway, config_entry))
devices.append(XiaomiAqaraLock(device, "Lock", gateway)) async_add_entities(entities)
async_add_entities(devices)
class XiaomiAqaraLock(LockEntity, XiaomiDevice): class XiaomiAqaraLock(LockEntity, XiaomiDevice):
"""Representation of a XiaomiAqaraLock.""" """Representation of a XiaomiAqaraLock."""
def __init__(self, device, name, xiaomi_hub): def __init__(self, device, name, xiaomi_hub, config_entry):
"""Initialize the XiaomiAqaraLock.""" """Initialize the XiaomiAqaraLock."""
self._changed_by = 0 self._changed_by = 0
self._verified_wrong_times = 0 self._verified_wrong_times = 0
super().__init__(device, name, xiaomi_hub) super().__init__(device, name, xiaomi_hub, config_entry)
@property @property
def is_locked(self) -> bool: def is_locked(self) -> bool:

View file

@ -1,8 +1,10 @@
{ {
"domain": "xiaomi_aqara", "domain": "xiaomi_aqara",
"name": "Xiaomi Gateway (Aqara)", "name": "Xiaomi Gateway (Aqara)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara", "documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara",
"requirements": ["PyXiaomiGateway==0.12.4"], "requirements": ["PyXiaomiGateway==0.12.4"],
"after_dependencies": ["discovery"], "after_dependencies": ["discovery"],
"codeowners": ["@danielhiversen", "@syssi"] "codeowners": ["@danielhiversen", "@syssi"],
"zeroconf": ["_miio._udp.local."]
} }

View file

@ -10,7 +10,8 @@ from homeassistant.const import (
UNIT_PERCENTAGE, UNIT_PERCENTAGE,
) )
from . import PY_XIAOMI_GATEWAY, XiaomiDevice from . import XiaomiDevice
from .const import DOMAIN, GATEWAYS_KEY
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -24,50 +25,70 @@ SENSOR_TYPES = {
} }
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Perform the setup for Xiaomi devices.""" """Perform the setup for Xiaomi devices."""
devices = [] entities = []
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
for device in gateway.devices["sensor"]: for device in gateway.devices["sensor"]:
if device["model"] == "sensor_ht": if device["model"] == "sensor_ht":
devices.append( entities.append(
XiaomiSensor(device, "Temperature", "temperature", gateway) XiaomiSensor(
device, "Temperature", "temperature", gateway, config_entry
) )
devices.append(XiaomiSensor(device, "Humidity", "humidity", gateway)) )
elif device["model"] in ["weather", "weather.v1"]: entities.append(
devices.append( XiaomiSensor(device, "Humidity", "humidity", gateway, config_entry)
XiaomiSensor(device, "Temperature", "temperature", gateway) )
elif device["model"] in ["weather", "weather.v1"]:
entities.append(
XiaomiSensor(
device, "Temperature", "temperature", gateway, config_entry
) )
devices.append(XiaomiSensor(device, "Humidity", "humidity", gateway)) )
devices.append(XiaomiSensor(device, "Pressure", "pressure", gateway)) entities.append(
elif device["model"] == "sensor_motion.aq2": XiaomiSensor(device, "Humidity", "humidity", gateway, config_entry)
devices.append(XiaomiSensor(device, "Illumination", "lux", gateway)) )
elif device["model"] in ["gateway", "gateway.v3", "acpartner.v3"]: entities.append(
devices.append( XiaomiSensor(device, "Pressure", "pressure", gateway, config_entry)
XiaomiSensor(device, "Illumination", "illumination", gateway) )
elif device["model"] == "sensor_motion.aq2":
entities.append(
XiaomiSensor(device, "Illumination", "lux", gateway, config_entry)
)
elif device["model"] in ["gateway", "gateway.v3", "acpartner.v3"]:
entities.append(
XiaomiSensor(
device, "Illumination", "illumination", gateway, config_entry
) )
elif device["model"] in ["vibration"]: )
devices.append( elif device["model"] in ["vibration"]:
XiaomiSensor(device, "Bed Activity", "bed_activity", gateway) entities.append(
XiaomiSensor(
device, "Bed Activity", "bed_activity", gateway, config_entry
) )
devices.append( )
XiaomiSensor(device, "Tilt Angle", "final_tilt_angle", gateway) entities.append(
XiaomiSensor(
device, "Tilt Angle", "final_tilt_angle", gateway, config_entry
) )
devices.append( )
XiaomiSensor(device, "Coordination", "coordination", gateway) entities.append(
XiaomiSensor(
device, "Coordination", "coordination", gateway, config_entry
) )
else: )
_LOGGER.warning("Unmapped Device Model ") else:
add_entities(devices) _LOGGER.warning("Unmapped Device Model")
async_add_entities(entities)
class XiaomiSensor(XiaomiDevice): class XiaomiSensor(XiaomiDevice):
"""Representation of a XiaomiSensor.""" """Representation of a XiaomiSensor."""
def __init__(self, device, name, data_key, xiaomi_hub): def __init__(self, device, name, data_key, xiaomi_hub, config_entry):
"""Initialize the XiaomiSensor.""" """Initialize the XiaomiSensor."""
self._data_key = data_key self._data_key = data_key
XiaomiDevice.__init__(self, device, name, xiaomi_hub) super().__init__(device, name, xiaomi_hub, config_entry)
@property @property
def icon(self): def icon(self):

View file

@ -0,0 +1,40 @@
{
"config": {
"flow_title": "Xiaomi Aqara Gateway: {name}",
"step": {
"user": {
"title": "Xiaomi Aqara Gateway",
"description": "Connect to your Xiaomi Aqara Gateway",
"data": {
"interface": "The network interface to use"
}
},
"settings": {
"title": "Xiaomi Aqara Gateway, optional settings",
"description": "The key (password) can be retrieved using this tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. If the key is not provided only sensors will be accessible",
"data": {
"key": "The key of your gateway",
"name": "Name of the Gateway"
}
},
"select": {
"title": "Select the Xiaomi Aqara Gateway that you wish to connect",
"description": "Run the setup again if you want to connect aditional gateways",
"data": {
"select_ip": "Gateway IP"
}
}
},
"error": {
"discovery_error": "Failed to discover a Xiaomi Aqara Gateway, try using the IP of the device running HomeAssistant as interface",
"not_found_error": "Zeroconf discovered Gateway could not be located to get the necessary information, try using the IP of the device running HomeAssistant as interface",
"invalid_interface": "Invalid network interface",
"invalid_key": "Invalid gateway key"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "Config flow for this gateway is already in progress",
"not_xiaomi_aqara": "Not a Xiaomi Aqara Gateway, discovered device did not match known gateways"
}
}
}

View file

@ -3,7 +3,8 @@ import logging
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from . import PY_XIAOMI_GATEWAY, XiaomiDevice from . import XiaomiDevice
from .const import DOMAIN, GATEWAYS_KEY
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -20,76 +21,108 @@ ENERGY_CONSUMED = "energy_consumed"
IN_USE = "inuse" IN_USE = "inuse"
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Perform the setup for Xiaomi devices.""" """Perform the setup for Xiaomi devices."""
devices = [] entities = []
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
for device in gateway.devices["switch"]: for device in gateway.devices["switch"]:
model = device["model"] model = device["model"]
if model == "plug": if model == "plug":
if "proto" not in device or int(device["proto"][0:1]) == 1: if "proto" not in device or int(device["proto"][0:1]) == 1:
data_key = "status" data_key = "status"
else: else:
data_key = "channel_0" data_key = "channel_0"
devices.append( entities.append(
XiaomiGenericSwitch(device, "Plug", data_key, True, gateway) XiaomiGenericSwitch(
device, "Plug", data_key, True, gateway, config_entry
) )
elif model in ["ctrl_neutral1", "ctrl_neutral1.aq1"]: )
devices.append( elif model in ["ctrl_neutral1", "ctrl_neutral1.aq1"]:
XiaomiGenericSwitch( entities.append(
device, "Wall Switch", "channel_0", False, gateway XiaomiGenericSwitch(
) device, "Wall Switch", "channel_0", False, gateway, config_entry
) )
elif model in ["ctrl_ln1", "ctrl_ln1.aq1"]: )
devices.append( elif model in ["ctrl_ln1", "ctrl_ln1.aq1"]:
XiaomiGenericSwitch( entities.append(
device, "Wall Switch LN", "channel_0", False, gateway XiaomiGenericSwitch(
) device, "Wall Switch LN", "channel_0", False, gateway, config_entry
) )
elif model in ["ctrl_neutral2", "ctrl_neutral2.aq1"]: )
devices.append( elif model in ["ctrl_neutral2", "ctrl_neutral2.aq1"]:
XiaomiGenericSwitch( entities.append(
device, "Wall Switch Left", "channel_0", False, gateway XiaomiGenericSwitch(
) device,
"Wall Switch Left",
"channel_0",
False,
gateway,
config_entry,
) )
devices.append( )
XiaomiGenericSwitch( entities.append(
device, "Wall Switch Right", "channel_1", False, gateway XiaomiGenericSwitch(
) device,
"Wall Switch Right",
"channel_1",
False,
gateway,
config_entry,
) )
elif model in ["ctrl_ln2", "ctrl_ln2.aq1"]: )
devices.append( elif model in ["ctrl_ln2", "ctrl_ln2.aq1"]:
XiaomiGenericSwitch( entities.append(
device, "Wall Switch LN Left", "channel_0", False, gateway XiaomiGenericSwitch(
) device,
"Wall Switch LN Left",
"channel_0",
False,
gateway,
config_entry,
) )
devices.append( )
XiaomiGenericSwitch( entities.append(
device, "Wall Switch LN Right", "channel_1", False, gateway XiaomiGenericSwitch(
) device,
"Wall Switch LN Right",
"channel_1",
False,
gateway,
config_entry,
) )
elif model in ["86plug", "ctrl_86plug", "ctrl_86plug.aq1"]: )
if "proto" not in device or int(device["proto"][0:1]) == 1: elif model in ["86plug", "ctrl_86plug", "ctrl_86plug.aq1"]:
data_key = "status" if "proto" not in device or int(device["proto"][0:1]) == 1:
else: data_key = "status"
data_key = "channel_0" else:
devices.append( data_key = "channel_0"
XiaomiGenericSwitch(device, "Wall Plug", data_key, True, gateway) entities.append(
XiaomiGenericSwitch(
device, "Wall Plug", data_key, True, gateway, config_entry
) )
add_entities(devices) )
async_add_entities(entities)
class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity):
"""Representation of a XiaomiPlug.""" """Representation of a XiaomiPlug."""
def __init__(self, device, name, data_key, supports_power_consumption, xiaomi_hub): def __init__(
self,
device,
name,
data_key,
supports_power_consumption,
xiaomi_hub,
config_entry,
):
"""Initialize the XiaomiPlug.""" """Initialize the XiaomiPlug."""
self._data_key = data_key self._data_key = data_key
self._in_use = None self._in_use = None
self._load_power = None self._load_power = None
self._power_consumed = None self._power_consumed = None
self._supports_power_consumption = supports_power_consumption self._supports_power_consumption = supports_power_consumption
XiaomiDevice.__init__(self, device, name, xiaomi_hub) super().__init__(device, name, xiaomi_hub, config_entry)
@property @property
def icon(self): def icon(self):

View file

@ -0,0 +1,40 @@
{
"config": {
"flow_title": "Xiaomi Aqara Gateway: {name}",
"step": {
"user": {
"title": "Xiaomi Aqara Gateway",
"description": "Connect to your Xiaomi Aqara Gateway",
"data": {
"interface": "The network interface to use"
}
},
"settings": {
"title": "Xiaomi Aqara Gateway, optional settings",
"description": "The key (password) can be retrieved using this tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. If the key is not provided only sensors will be accessible",
"data": {
"key": "The key of your gateway",
"name": "Name of the Gateway"
}
},
"select": {
"title": "Select the Xiaomi Aqara Gateway that you wish to connect",
"description": "Run the setup again if you want to connect aditional gateways",
"data": {
"select_ip": "Gateway IP"
}
}
},
"error": {
"discovery_error": "Failed to discover a Xiaomi Aqara Gateway, try using the IP of the device running HomeAssistant as interface",
"not_found_error": "Zeroconf discovered Gateway could not be located to get the necessary information, try using the IP of the device running HomeAssistant as interface",
"invalid_interface": "Invalid network interface",
"invalid_key": "Invalid gateway key"
},
"abort": {
"already_configured": "Device is already configured",
"already_in_progress": "Config flow for this gateway is already in progress",
"not_xiaomi_aqara": "Not a Xiaomi Aqara Gateway, discovered device did not match known gateways"
}
}
}

View file

@ -176,6 +176,7 @@ FLOWS = [
"wiffi", "wiffi",
"withings", "withings",
"wled", "wled",
"xiaomi_aqara",
"xiaomi_miio", "xiaomi_miio",
"zerproc", "zerproc",
"zha", "zha",

View file

@ -38,6 +38,7 @@ ZEROCONF = {
"ipp" "ipp"
], ],
"_miio._udp.local.": [ "_miio._udp.local.": [
"xiaomi_aqara",
"xiaomi_miio" "xiaomi_miio"
], ],
"_nut._tcp.local.": [ "_nut._tcp.local.": [

View file

@ -29,6 +29,9 @@ PyTransportNSW==0.1.1
# homeassistant.components.homekit # homeassistant.components.homekit
PyTurboJPEG==1.4.0 PyTurboJPEG==1.4.0
# homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.12.4
# homeassistant.components.remember_the_milk # homeassistant.components.remember_the_milk
RtmAPI==0.7.2 RtmAPI==0.7.2

View file

@ -0,0 +1 @@
"""Tests for the Xiaomi Aqara integration."""

View file

@ -0,0 +1,368 @@
"""Test the Xiaomi Aqara config flow."""
from socket import gaierror
import pytest
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.xiaomi_aqara import config_flow, const
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT
from tests.async_mock import Mock, patch
ZEROCONF_NAME = "name"
ZEROCONF_PROP = "properties"
ZEROCONF_MAC = "mac"
TEST_HOST = "1.2.3.4"
TEST_HOST_2 = "5.6.7.8"
TEST_KEY = "1234567890123456"
TEST_PORT = 1234
TEST_NAME = "Test_Aqara_Gateway"
TEST_SID = "abcdefghijkl"
TEST_PROTOCOL = "1.1.1"
TEST_MAC = "ab:cd:ef:gh:ij:kl"
TEST_GATEWAY_ID = TEST_MAC
TEST_ZEROCONF_NAME = "lumi-gateway-v3_miio12345678._miio._udp.local."
@pytest.fixture(name="xiaomi_aqara", autouse=True)
def xiaomi_aqara_fixture():
"""Mock xiaomi_aqara discovery and entry setup."""
mock_gateway_discovery = get_mock_discovery([TEST_HOST])
with patch(
"homeassistant.components.xiaomi_aqara.config_flow.XiaomiGatewayDiscovery",
return_value=mock_gateway_discovery,
), patch(
"homeassistant.components.xiaomi_aqara.async_setup_entry", return_value=True
):
yield
def get_mock_discovery(host_list, invalid_interface=False, invalid_key=False):
"""Return a mock gateway info instance."""
gateway_discovery = Mock()
gateway_dict = {}
for host in host_list:
gateway = Mock()
gateway.ip_adress = host
gateway.port = TEST_PORT
gateway.sid = TEST_SID
gateway.proto = TEST_PROTOCOL
if invalid_key:
gateway.write_to_hub = Mock(return_value=False)
gateway_dict[host] = gateway
gateway_discovery.gateways = gateway_dict
if invalid_interface:
gateway_discovery.discover_gateways = Mock(side_effect=gaierror)
return gateway_discovery
async def test_config_flow_user_success(hass):
"""Test a successful config flow initialized by the user."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE},
)
assert result["type"] == "form"
assert result["step_id"] == "settings"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME},
)
assert result["type"] == "create_entry"
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: TEST_HOST,
CONF_PORT: TEST_PORT,
CONF_MAC: TEST_MAC,
const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE,
const.CONF_PROTOCOL: TEST_PROTOCOL,
const.CONF_KEY: TEST_KEY,
const.CONF_SID: TEST_SID,
}
async def test_config_flow_user_multiple_success(hass):
"""Test a successful config flow initialized by the user with multiple gateways discoverd."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
mock_gateway_discovery = get_mock_discovery([TEST_HOST, TEST_HOST_2])
with patch(
"homeassistant.components.xiaomi_aqara.config_flow.XiaomiGatewayDiscovery",
return_value=mock_gateway_discovery,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE},
)
assert result["type"] == "form"
assert result["step_id"] == "select"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"select_ip": TEST_HOST_2},
)
assert result["type"] == "form"
assert result["step_id"] == "settings"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME},
)
assert result["type"] == "create_entry"
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: TEST_HOST_2,
CONF_PORT: TEST_PORT,
CONF_MAC: TEST_MAC,
const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE,
const.CONF_PROTOCOL: TEST_PROTOCOL,
const.CONF_KEY: TEST_KEY,
const.CONF_SID: TEST_SID,
}
async def test_config_flow_user_no_key_success(hass):
"""Test a successful config flow initialized by the user without a key."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE},
)
assert result["type"] == "form"
assert result["step_id"] == "settings"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_NAME: TEST_NAME},
)
assert result["type"] == "create_entry"
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: TEST_HOST,
CONF_PORT: TEST_PORT,
CONF_MAC: TEST_MAC,
const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE,
const.CONF_PROTOCOL: TEST_PROTOCOL,
const.CONF_KEY: None,
const.CONF_SID: TEST_SID,
}
async def test_config_flow_user_discovery_error(hass):
"""Test a failed config flow initialized by the user with no gateways discoverd."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
mock_gateway_discovery = get_mock_discovery([])
with patch(
"homeassistant.components.xiaomi_aqara.config_flow.XiaomiGatewayDiscovery",
return_value=mock_gateway_discovery,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE},
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {"base": "discovery_error"}
async def test_config_flow_user_invalid_interface(hass):
"""Test a failed config flow initialized by the user with an invalid interface."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
mock_gateway_discovery = get_mock_discovery([], invalid_interface=True)
with patch(
"homeassistant.components.xiaomi_aqara.config_flow.XiaomiGatewayDiscovery",
return_value=mock_gateway_discovery,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE},
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {const.CONF_INTERFACE: "invalid_interface"}
async def test_config_flow_user_invalid_key(hass):
"""Test a failed config flow initialized by the user with an invalid key."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
mock_gateway_discovery = get_mock_discovery([TEST_HOST], invalid_key=True)
with patch(
"homeassistant.components.xiaomi_aqara.config_flow.XiaomiGatewayDiscovery",
return_value=mock_gateway_discovery,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE},
)
assert result["type"] == "form"
assert result["step_id"] == "settings"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME},
)
assert result["type"] == "form"
assert result["step_id"] == "settings"
assert result["errors"] == {const.CONF_KEY: "invalid_key"}
async def test_zeroconf_success(hass):
"""Test a successful zeroconf discovery of a xiaomi aqara gateway."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
zeroconf.ATTR_HOST: TEST_HOST,
ZEROCONF_NAME: TEST_ZEROCONF_NAME,
ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC},
},
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE},
)
assert result["type"] == "form"
assert result["step_id"] == "settings"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME},
)
assert result["type"] == "create_entry"
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: TEST_HOST,
CONF_PORT: TEST_PORT,
CONF_MAC: TEST_MAC,
const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE,
const.CONF_PROTOCOL: TEST_PROTOCOL,
const.CONF_KEY: TEST_KEY,
const.CONF_SID: TEST_SID,
}
async def test_zeroconf_missing_data(hass):
"""Test a failed zeroconf discovery because of missing data."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={zeroconf.ATTR_HOST: TEST_HOST, ZEROCONF_NAME: TEST_ZEROCONF_NAME},
)
assert result["type"] == "abort"
assert result["reason"] == "not_xiaomi_aqara"
async def test_zeroconf_unknown_device(hass):
"""Test a failed zeroconf discovery because of a unknown device."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
zeroconf.ATTR_HOST: TEST_HOST,
ZEROCONF_NAME: "not-a-xiaomi-aqara-gateway",
ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC},
},
)
assert result["type"] == "abort"
assert result["reason"] == "not_xiaomi_aqara"
async def test_zeroconf_not_found_error(hass):
"""Test a failed zeroconf discovery because the correct gateway could not be found."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
zeroconf.ATTR_HOST: TEST_HOST,
ZEROCONF_NAME: TEST_ZEROCONF_NAME,
ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC},
},
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
mock_gateway_discovery = get_mock_discovery([TEST_HOST_2])
with patch(
"homeassistant.components.xiaomi_aqara.config_flow.XiaomiGatewayDiscovery",
return_value=mock_gateway_discovery,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE},
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {"base": "not_found_error"}