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:
parent
6aba87f3a6
commit
1f9721bad3
19 changed files with 1128 additions and 341 deletions
|
@ -909,7 +909,14 @@ omit =
|
|||
homeassistant/components/xeoma/camera.py
|
||||
homeassistant/components/xfinity/device_tracker.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/air_quality.py
|
||||
homeassistant/components/xiaomi_miio/alarm_control_panel.py
|
||||
|
|
|
@ -57,7 +57,6 @@ SERVICE_HANDLERS = {
|
|||
SERVICE_APPLE_TV: ("apple_tv", None),
|
||||
SERVICE_ENIGMA2: ("media_player", "enigma2"),
|
||||
SERVICE_WINK: ("wink", None),
|
||||
SERVICE_XIAOMI_GW: ("xiaomi_aqara", None),
|
||||
SERVICE_SABNZBD: ("sabnzbd", None),
|
||||
SERVICE_SAMSUNG_PRINTER: ("sensor", "syncthru"),
|
||||
SERVICE_KONNECTED: ("konnected", None),
|
||||
|
@ -92,6 +91,7 @@ MIGRATED_SERVICE_HANDLERS = [
|
|||
"sonos",
|
||||
"songpal",
|
||||
SERVICE_WEMO,
|
||||
SERVICE_XIAOMI_GW,
|
||||
]
|
||||
|
||||
DEFAULT_ENABLED = (
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
"""Support for Xiaomi Gateways."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
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 (
|
||||
ATTR_BATTERY_LEVEL,
|
||||
ATTR_VOLTAGE,
|
||||
|
@ -15,29 +16,34 @@ from homeassistant.const import (
|
|||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
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
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
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__)
|
||||
|
||||
GATEWAY_PLATFORMS = ["binary_sensor", "sensor", "switch", "light", "cover", "lock"]
|
||||
GATEWAY_PLATFORMS_NO_KEY = ["binary_sensor", "sensor"]
|
||||
|
||||
ATTR_GW_MAC = "gw_mac"
|
||||
ATTR_RINGTONE_ID = "ringtone_id"
|
||||
ATTR_RINGTONE_VOL = "ringtone_vol"
|
||||
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)
|
||||
|
||||
SERVICE_PLAY_RINGTONE = "play_ringtone"
|
||||
|
@ -45,10 +51,6 @@ SERVICE_STOP_RINGTONE = "stop_ringtone"
|
|||
SERVICE_ADD_DEVICE = "add_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(
|
||||
{
|
||||
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):
|
||||
"""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):
|
||||
"""Service to play ringtone through Gateway."""
|
||||
|
@ -196,13 +104,13 @@ def setup(hass, config):
|
|||
gateway = call.data.get(ATTR_GW_MAC)
|
||||
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(
|
||||
DOMAIN,
|
||||
SERVICE_PLAY_RINGTONE,
|
||||
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(
|
||||
|
@ -217,21 +125,119 @@ def setup(hass, config):
|
|||
DOMAIN,
|
||||
SERVICE_REMOVE_DEVICE,
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""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."""
|
||||
self._state = None
|
||||
self._is_available = True
|
||||
self._sid = device["sid"]
|
||||
self._model = device["model"]
|
||||
self._protocol = device["proto"]
|
||||
self._name = f"{device_type}_{self._sid}"
|
||||
self._device_name = f"{self._model}_{self._sid}"
|
||||
self._type = device_type
|
||||
self._write_to_hub = xiaomi_hub.write_to_hub
|
||||
self._get_from_hub = xiaomi_hub.get_from_hub
|
||||
|
@ -248,6 +254,16 @@ class XiaomiDevice(Entity):
|
|||
else:
|
||||
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):
|
||||
self.hass.add_job(self.push_data, *args)
|
||||
|
||||
|
@ -266,6 +282,32 @@ class XiaomiDevice(Entity):
|
|||
"""Return a 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
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
|
@ -334,24 +376,26 @@ class XiaomiDevice(Entity):
|
|||
raise NotImplementedError()
|
||||
|
||||
|
||||
def _add_gateway_to_schema(xiaomi, schema):
|
||||
def _add_gateway_to_schema(hass, schema):
|
||||
"""Extend a voluptuous schema with a gateway validator."""
|
||||
|
||||
def gateway(sid):
|
||||
"""Convert sid to a gateway."""
|
||||
sid = str(sid).replace(":", "").lower()
|
||||
|
||||
for gateway in xiaomi.gateways.values():
|
||||
for gateway in hass.data[DOMAIN][GATEWAYS_KEY].values():
|
||||
if gateway.sid == sid:
|
||||
return gateway
|
||||
|
||||
raise vol.Invalid(f"Unknown gateway sid {sid}")
|
||||
|
||||
gateways = list(xiaomi.gateways.values())
|
||||
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 len(gateways) == 1:
|
||||
kwargs["default"] = gateways[0].sid
|
||||
# If the user has only 1 gateway, make it the default for services.
|
||||
if len(gateways) == 1:
|
||||
kwargs["default"] = gateways[0].sid
|
||||
|
||||
return schema.extend({vol.Required(ATTR_GW_MAC, **kwargs): gateway})
|
||||
|
|
|
@ -5,7 +5,8 @@ from homeassistant.components.binary_sensor import BinarySensorEntity
|
|||
from homeassistant.core import callback
|
||||
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__)
|
||||
|
||||
|
@ -21,94 +22,115 @@ 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."""
|
||||
devices = []
|
||||
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
||||
for device in gateway.devices["binary_sensor"]:
|
||||
model = device["model"]
|
||||
if model in ["motion", "sensor_motion", "sensor_motion.aq2"]:
|
||||
devices.append(XiaomiMotionSensor(device, hass, gateway))
|
||||
elif model in ["magnet", "sensor_magnet", "sensor_magnet.aq2"]:
|
||||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
elif model == "sensor_wleak.aq1":
|
||||
devices.append(XiaomiWaterLeakSensor(device, gateway))
|
||||
elif model in ["smoke", "sensor_smoke"]:
|
||||
devices.append(XiaomiSmokeSensor(device, gateway))
|
||||
elif model in ["natgas", "sensor_natgas"]:
|
||||
devices.append(XiaomiNatgasSensor(device, gateway))
|
||||
elif model in [
|
||||
"switch",
|
||||
"sensor_switch",
|
||||
"sensor_switch.aq2",
|
||||
"sensor_switch.aq3",
|
||||
"remote.b1acn01",
|
||||
]:
|
||||
if "proto" not in device or int(device["proto"][0:1]) == 1:
|
||||
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))
|
||||
entities = []
|
||||
gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
|
||||
for entity in gateway.devices["binary_sensor"]:
|
||||
model = entity["model"]
|
||||
if model in ["motion", "sensor_motion", "sensor_motion.aq2"]:
|
||||
entities.append(XiaomiMotionSensor(entity, hass, gateway, config_entry))
|
||||
elif model in ["magnet", "sensor_magnet", "sensor_magnet.aq2"]:
|
||||
entities.append(XiaomiDoorSensor(entity, gateway, config_entry))
|
||||
elif model == "sensor_wleak.aq1":
|
||||
entities.append(XiaomiWaterLeakSensor(entity, gateway, config_entry))
|
||||
elif model in ["smoke", "sensor_smoke"]:
|
||||
entities.append(XiaomiSmokeSensor(entity, gateway, config_entry))
|
||||
elif model in ["natgas", "sensor_natgas"]:
|
||||
entities.append(XiaomiNatgasSensor(entity, gateway, config_entry))
|
||||
elif model in [
|
||||
"switch",
|
||||
"sensor_switch",
|
||||
"sensor_switch.aq2",
|
||||
"sensor_switch.aq3",
|
||||
"remote.b1acn01",
|
||||
]:
|
||||
if "proto" not in entity or int(entity["proto"][0:1]) == 1:
|
||||
data_key = "status"
|
||||
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):
|
||||
"""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."""
|
||||
self._data_key = data_key
|
||||
self._device_class = device_class
|
||||
self._should_poll = False
|
||||
self._density = 0
|
||||
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
|
||||
super().__init__(device, name, xiaomi_hub, config_entry)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -134,11 +156,11 @@ class XiaomiBinarySensor(XiaomiDevice, BinarySensorEntity):
|
|||
class XiaomiNatgasSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiNatgasSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
def __init__(self, device, xiaomi_hub, config_entry):
|
||||
"""Initialize the XiaomiSmokeSensor."""
|
||||
self._density = None
|
||||
XiaomiBinarySensor.__init__(
|
||||
self, device, "Natgas Sensor", xiaomi_hub, "alarm", "gas"
|
||||
super().__init__(
|
||||
device, "Natgas Sensor", xiaomi_hub, "alarm", "gas", config_entry
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -172,7 +194,7 @@ class XiaomiNatgasSensor(XiaomiBinarySensor):
|
|||
class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiMotionSensor."""
|
||||
|
||||
def __init__(self, device, hass, xiaomi_hub):
|
||||
def __init__(self, device, hass, xiaomi_hub, config_entry):
|
||||
"""Initialize the XiaomiMotionSensor."""
|
||||
self._hass = hass
|
||||
self._no_motion_since = 0
|
||||
|
@ -181,8 +203,8 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
|||
data_key = "status"
|
||||
else:
|
||||
data_key = "motion_status"
|
||||
XiaomiBinarySensor.__init__(
|
||||
self, device, "Motion Sensor", xiaomi_hub, data_key, "motion"
|
||||
super().__init__(
|
||||
device, "Motion Sensor", xiaomi_hub, data_key, "motion", config_entry
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -263,15 +285,15 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
|||
class XiaomiDoorSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiDoorSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
def __init__(self, device, xiaomi_hub, config_entry):
|
||||
"""Initialize the XiaomiDoorSensor."""
|
||||
self._open_since = 0
|
||||
if "proto" not in device or int(device["proto"][0:1]) == 1:
|
||||
data_key = "status"
|
||||
else:
|
||||
data_key = "window_status"
|
||||
XiaomiBinarySensor.__init__(
|
||||
self, device, "Door Window Sensor", xiaomi_hub, data_key, "opening"
|
||||
super().__init__(
|
||||
device, "Door Window Sensor", xiaomi_hub, data_key, "opening", config_entry,
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -309,14 +331,14 @@ class XiaomiDoorSensor(XiaomiBinarySensor):
|
|||
class XiaomiWaterLeakSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiWaterLeakSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
def __init__(self, device, xiaomi_hub, config_entry):
|
||||
"""Initialize the XiaomiWaterLeakSensor."""
|
||||
if "proto" not in device or int(device["proto"][0:1]) == 1:
|
||||
data_key = "status"
|
||||
else:
|
||||
data_key = "wleak_status"
|
||||
XiaomiBinarySensor.__init__(
|
||||
self, device, "Water Leak Sensor", xiaomi_hub, data_key, "moisture"
|
||||
super().__init__(
|
||||
device, "Water Leak Sensor", xiaomi_hub, data_key, "moisture", config_entry,
|
||||
)
|
||||
|
||||
def parse_data(self, data, raw_data):
|
||||
|
@ -343,11 +365,11 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor):
|
|||
class XiaomiSmokeSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiSmokeSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
def __init__(self, device, xiaomi_hub, config_entry):
|
||||
"""Initialize the XiaomiSmokeSensor."""
|
||||
self._density = 0
|
||||
XiaomiBinarySensor.__init__(
|
||||
self, device, "Smoke Sensor", xiaomi_hub, "alarm", "smoke"
|
||||
super().__init__(
|
||||
device, "Smoke Sensor", xiaomi_hub, "alarm", "smoke", config_entry
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -380,10 +402,10 @@ class XiaomiSmokeSensor(XiaomiBinarySensor):
|
|||
class XiaomiVibration(XiaomiBinarySensor):
|
||||
"""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."""
|
||||
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
|
||||
def device_state_attributes(self):
|
||||
|
@ -414,11 +436,11 @@ class XiaomiVibration(XiaomiBinarySensor):
|
|||
class XiaomiButton(XiaomiBinarySensor):
|
||||
"""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."""
|
||||
self._hass = hass
|
||||
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
|
||||
def device_state_attributes(self):
|
||||
|
@ -469,7 +491,7 @@ class XiaomiButton(XiaomiBinarySensor):
|
|||
class XiaomiCube(XiaomiBinarySensor):
|
||||
"""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."""
|
||||
self._hass = hass
|
||||
self._last_action = None
|
||||
|
@ -478,7 +500,7 @@ class XiaomiCube(XiaomiBinarySensor):
|
|||
data_key = "status"
|
||||
else:
|
||||
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
|
||||
def device_state_attributes(self):
|
||||
|
|
183
homeassistant/components/xiaomi_aqara/config_flow.py
Normal file
183
homeassistant/components/xiaomi_aqara/config_flow.py
Normal 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
|
||||
)
|
15
homeassistant/components/xiaomi_aqara/const.py
Normal file
15
homeassistant/components/xiaomi_aqara/const.py
Normal 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
|
|
@ -3,7 +3,8 @@ import logging
|
|||
|
||||
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__)
|
||||
|
||||
|
@ -13,29 +14,31 @@ DATA_KEY_PROTO_V1 = "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."""
|
||||
devices = []
|
||||
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
||||
for device in gateway.devices["cover"]:
|
||||
model = device["model"]
|
||||
if model in ["curtain", "curtain.aq2", "curtain.hagl04"]:
|
||||
if "proto" not in device or int(device["proto"][0:1]) == 1:
|
||||
data_key = DATA_KEY_PROTO_V1
|
||||
else:
|
||||
data_key = DATA_KEY_PROTO_V2
|
||||
devices.append(XiaomiGenericCover(device, "Curtain", data_key, gateway))
|
||||
add_entities(devices)
|
||||
entities = []
|
||||
gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
|
||||
for device in gateway.devices["cover"]:
|
||||
model = device["model"]
|
||||
if model in ["curtain", "curtain.aq2", "curtain.hagl04"]:
|
||||
if "proto" not in device or int(device["proto"][0:1]) == 1:
|
||||
data_key = DATA_KEY_PROTO_V1
|
||||
else:
|
||||
data_key = DATA_KEY_PROTO_V2
|
||||
entities.append(
|
||||
XiaomiGenericCover(device, "Curtain", data_key, gateway, config_entry)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class XiaomiGenericCover(XiaomiDevice, CoverEntity):
|
||||
"""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."""
|
||||
self._data_key = data_key
|
||||
self._pos = 0
|
||||
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
|
||||
super().__init__(device, name, xiaomi_hub, config_entry)
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
|
|
@ -12,32 +12,35 @@ from homeassistant.components.light import (
|
|||
)
|
||||
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__)
|
||||
|
||||
|
||||
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."""
|
||||
devices = []
|
||||
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
||||
for device in gateway.devices["light"]:
|
||||
model = device["model"]
|
||||
if model in ["gateway", "gateway.v3"]:
|
||||
devices.append(XiaomiGatewayLight(device, "Gateway Light", gateway))
|
||||
add_entities(devices)
|
||||
entities = []
|
||||
gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
|
||||
for device in gateway.devices["light"]:
|
||||
model = device["model"]
|
||||
if model in ["gateway", "gateway.v3"]:
|
||||
entities.append(
|
||||
XiaomiGatewayLight(device, "Gateway Light", gateway, config_entry)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class XiaomiGatewayLight(XiaomiDevice, LightEntity):
|
||||
"""Representation of a XiaomiGatewayLight."""
|
||||
|
||||
def __init__(self, device, name, xiaomi_hub):
|
||||
def __init__(self, device, name, xiaomi_hub, config_entry):
|
||||
"""Initialize the XiaomiGatewayLight."""
|
||||
self._data_key = "rgb"
|
||||
self._hs = (0, 0)
|
||||
self._brightness = 100
|
||||
|
||||
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
|
||||
super().__init__(device, name, xiaomi_hub, config_entry)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
|
|
@ -6,7 +6,8 @@ from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
|
|||
from homeassistant.core import callback
|
||||
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__)
|
||||
|
||||
|
@ -20,27 +21,26 @@ ATTR_VERIFIED_WRONG_TIMES = "verified_wrong_times"
|
|||
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."""
|
||||
devices = []
|
||||
|
||||
for gateway in hass.data[PY_XIAOMI_GATEWAY].gateways.values():
|
||||
for device in gateway.devices["lock"]:
|
||||
model = device["model"]
|
||||
if model == "lock.aq1":
|
||||
devices.append(XiaomiAqaraLock(device, "Lock", gateway))
|
||||
async_add_entities(devices)
|
||||
entities = []
|
||||
gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
|
||||
for device in gateway.devices["lock"]:
|
||||
model = device["model"]
|
||||
if model == "lock.aq1":
|
||||
entities.append(XiaomiAqaraLock(device, "Lock", gateway, config_entry))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class XiaomiAqaraLock(LockEntity, XiaomiDevice):
|
||||
"""Representation of a XiaomiAqaraLock."""
|
||||
|
||||
def __init__(self, device, name, xiaomi_hub):
|
||||
def __init__(self, device, name, xiaomi_hub, config_entry):
|
||||
"""Initialize the XiaomiAqaraLock."""
|
||||
self._changed_by = 0
|
||||
self._verified_wrong_times = 0
|
||||
|
||||
super().__init__(device, name, xiaomi_hub)
|
||||
super().__init__(device, name, xiaomi_hub, config_entry)
|
||||
|
||||
@property
|
||||
def is_locked(self) -> bool:
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
{
|
||||
"domain": "xiaomi_aqara",
|
||||
"name": "Xiaomi Gateway (Aqara)",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara",
|
||||
"requirements": ["PyXiaomiGateway==0.12.4"],
|
||||
"after_dependencies": ["discovery"],
|
||||
"codeowners": ["@danielhiversen", "@syssi"]
|
||||
"codeowners": ["@danielhiversen", "@syssi"],
|
||||
"zeroconf": ["_miio._udp.local."]
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ from homeassistant.const import (
|
|||
UNIT_PERCENTAGE,
|
||||
)
|
||||
|
||||
from . import PY_XIAOMI_GATEWAY, XiaomiDevice
|
||||
from . import XiaomiDevice
|
||||
from .const import DOMAIN, GATEWAYS_KEY
|
||||
|
||||
_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."""
|
||||
devices = []
|
||||
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
||||
for device in gateway.devices["sensor"]:
|
||||
if device["model"] == "sensor_ht":
|
||||
devices.append(
|
||||
XiaomiSensor(device, "Temperature", "temperature", gateway)
|
||||
entities = []
|
||||
gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
|
||||
for device in gateway.devices["sensor"]:
|
||||
if device["model"] == "sensor_ht":
|
||||
entities.append(
|
||||
XiaomiSensor(
|
||||
device, "Temperature", "temperature", gateway, config_entry
|
||||
)
|
||||
devices.append(XiaomiSensor(device, "Humidity", "humidity", gateway))
|
||||
elif device["model"] in ["weather", "weather.v1"]:
|
||||
devices.append(
|
||||
XiaomiSensor(device, "Temperature", "temperature", gateway)
|
||||
)
|
||||
entities.append(
|
||||
XiaomiSensor(device, "Humidity", "humidity", gateway, config_entry)
|
||||
)
|
||||
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))
|
||||
elif device["model"] == "sensor_motion.aq2":
|
||||
devices.append(XiaomiSensor(device, "Illumination", "lux", gateway))
|
||||
elif device["model"] in ["gateway", "gateway.v3", "acpartner.v3"]:
|
||||
devices.append(
|
||||
XiaomiSensor(device, "Illumination", "illumination", gateway)
|
||||
)
|
||||
entities.append(
|
||||
XiaomiSensor(device, "Humidity", "humidity", gateway, config_entry)
|
||||
)
|
||||
entities.append(
|
||||
XiaomiSensor(device, "Pressure", "pressure", gateway, config_entry)
|
||||
)
|
||||
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(
|
||||
XiaomiSensor(device, "Bed Activity", "bed_activity", gateway)
|
||||
)
|
||||
elif device["model"] in ["vibration"]:
|
||||
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 ")
|
||||
add_entities(devices)
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning("Unmapped Device Model")
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class XiaomiSensor(XiaomiDevice):
|
||||
"""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."""
|
||||
self._data_key = data_key
|
||||
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
|
||||
super().__init__(device, name, xiaomi_hub, config_entry)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
|
|
40
homeassistant/components/xiaomi_aqara/strings.json
Normal file
40
homeassistant/components/xiaomi_aqara/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,8 @@ import logging
|
|||
|
||||
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__)
|
||||
|
||||
|
@ -20,76 +21,108 @@ ENERGY_CONSUMED = "energy_consumed"
|
|||
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."""
|
||||
devices = []
|
||||
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
||||
for device in gateway.devices["switch"]:
|
||||
model = device["model"]
|
||||
if model == "plug":
|
||||
if "proto" not in device or int(device["proto"][0:1]) == 1:
|
||||
data_key = "status"
|
||||
else:
|
||||
data_key = "channel_0"
|
||||
devices.append(
|
||||
XiaomiGenericSwitch(device, "Plug", data_key, True, gateway)
|
||||
entities = []
|
||||
gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
|
||||
for device in gateway.devices["switch"]:
|
||||
model = device["model"]
|
||||
if model == "plug":
|
||||
if "proto" not in device or int(device["proto"][0:1]) == 1:
|
||||
data_key = "status"
|
||||
else:
|
||||
data_key = "channel_0"
|
||||
entities.append(
|
||||
XiaomiGenericSwitch(
|
||||
device, "Plug", data_key, True, gateway, config_entry
|
||||
)
|
||||
elif model in ["ctrl_neutral1", "ctrl_neutral1.aq1"]:
|
||||
devices.append(
|
||||
XiaomiGenericSwitch(
|
||||
device, "Wall Switch", "channel_0", False, gateway
|
||||
)
|
||||
)
|
||||
elif model in ["ctrl_neutral1", "ctrl_neutral1.aq1"]:
|
||||
entities.append(
|
||||
XiaomiGenericSwitch(
|
||||
device, "Wall Switch", "channel_0", False, gateway, config_entry
|
||||
)
|
||||
elif model in ["ctrl_ln1", "ctrl_ln1.aq1"]:
|
||||
devices.append(
|
||||
XiaomiGenericSwitch(
|
||||
device, "Wall Switch LN", "channel_0", False, gateway
|
||||
)
|
||||
)
|
||||
elif model in ["ctrl_ln1", "ctrl_ln1.aq1"]:
|
||||
entities.append(
|
||||
XiaomiGenericSwitch(
|
||||
device, "Wall Switch LN", "channel_0", False, gateway, config_entry
|
||||
)
|
||||
elif model in ["ctrl_neutral2", "ctrl_neutral2.aq1"]:
|
||||
devices.append(
|
||||
XiaomiGenericSwitch(
|
||||
device, "Wall Switch Left", "channel_0", False, gateway
|
||||
)
|
||||
)
|
||||
elif model in ["ctrl_neutral2", "ctrl_neutral2.aq1"]:
|
||||
entities.append(
|
||||
XiaomiGenericSwitch(
|
||||
device,
|
||||
"Wall Switch Left",
|
||||
"channel_0",
|
||||
False,
|
||||
gateway,
|
||||
config_entry,
|
||||
)
|
||||
devices.append(
|
||||
XiaomiGenericSwitch(
|
||||
device, "Wall Switch Right", "channel_1", False, gateway
|
||||
)
|
||||
)
|
||||
entities.append(
|
||||
XiaomiGenericSwitch(
|
||||
device,
|
||||
"Wall Switch Right",
|
||||
"channel_1",
|
||||
False,
|
||||
gateway,
|
||||
config_entry,
|
||||
)
|
||||
elif model in ["ctrl_ln2", "ctrl_ln2.aq1"]:
|
||||
devices.append(
|
||||
XiaomiGenericSwitch(
|
||||
device, "Wall Switch LN Left", "channel_0", False, gateway
|
||||
)
|
||||
)
|
||||
elif model in ["ctrl_ln2", "ctrl_ln2.aq1"]:
|
||||
entities.append(
|
||||
XiaomiGenericSwitch(
|
||||
device,
|
||||
"Wall Switch LN Left",
|
||||
"channel_0",
|
||||
False,
|
||||
gateway,
|
||||
config_entry,
|
||||
)
|
||||
devices.append(
|
||||
XiaomiGenericSwitch(
|
||||
device, "Wall Switch LN Right", "channel_1", False, gateway
|
||||
)
|
||||
)
|
||||
entities.append(
|
||||
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:
|
||||
data_key = "status"
|
||||
else:
|
||||
data_key = "channel_0"
|
||||
devices.append(
|
||||
XiaomiGenericSwitch(device, "Wall Plug", data_key, True, gateway)
|
||||
)
|
||||
elif model in ["86plug", "ctrl_86plug", "ctrl_86plug.aq1"]:
|
||||
if "proto" not in device or int(device["proto"][0:1]) == 1:
|
||||
data_key = "status"
|
||||
else:
|
||||
data_key = "channel_0"
|
||||
entities.append(
|
||||
XiaomiGenericSwitch(
|
||||
device, "Wall Plug", data_key, True, gateway, config_entry
|
||||
)
|
||||
add_entities(devices)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity):
|
||||
"""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."""
|
||||
self._data_key = data_key
|
||||
self._in_use = None
|
||||
self._load_power = None
|
||||
self._power_consumed = None
|
||||
self._supports_power_consumption = supports_power_consumption
|
||||
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
|
||||
super().__init__(device, name, xiaomi_hub, config_entry)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
|
|
40
homeassistant/components/xiaomi_aqara/translations/en.json
Normal file
40
homeassistant/components/xiaomi_aqara/translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -176,6 +176,7 @@ FLOWS = [
|
|||
"wiffi",
|
||||
"withings",
|
||||
"wled",
|
||||
"xiaomi_aqara",
|
||||
"xiaomi_miio",
|
||||
"zerproc",
|
||||
"zha",
|
||||
|
|
|
@ -38,6 +38,7 @@ ZEROCONF = {
|
|||
"ipp"
|
||||
],
|
||||
"_miio._udp.local.": [
|
||||
"xiaomi_aqara",
|
||||
"xiaomi_miio"
|
||||
],
|
||||
"_nut._tcp.local.": [
|
||||
|
|
|
@ -29,6 +29,9 @@ PyTransportNSW==0.1.1
|
|||
# homeassistant.components.homekit
|
||||
PyTurboJPEG==1.4.0
|
||||
|
||||
# homeassistant.components.xiaomi_aqara
|
||||
PyXiaomiGateway==0.12.4
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
RtmAPI==0.7.2
|
||||
|
||||
|
|
1
tests/components/xiaomi_aqara/__init__.py
Normal file
1
tests/components/xiaomi_aqara/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the Xiaomi Aqara integration."""
|
368
tests/components/xiaomi_aqara/test_config_flow.py
Normal file
368
tests/components/xiaomi_aqara/test_config_flow.py
Normal 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"}
|
Loading…
Add table
Reference in a new issue