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/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
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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):
|
||||||
|
|
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 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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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."]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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):
|
||||||
|
|
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 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):
|
||||||
|
|
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",
|
"wiffi",
|
||||||
"withings",
|
"withings",
|
||||||
"wled",
|
"wled",
|
||||||
|
"xiaomi_aqara",
|
||||||
"xiaomi_miio",
|
"xiaomi_miio",
|
||||||
"zerproc",
|
"zerproc",
|
||||||
"zha",
|
"zha",
|
||||||
|
|
|
@ -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.": [
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
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
Add a link
Reference in a new issue