Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xiaomi Miio zeroconf discovery #35352

Merged
merged 41 commits into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
2ca7402
Xiaomi Miio zeroconf discovery
starkillerOG May 7, 2020
15a0283
Add Xiaomi Miio Zeroconf Discovery
starkillerOG May 7, 2020
e6cafab
Add Xiaomi Miio Zeroconf Discovery
starkillerOG May 7, 2020
fd6d790
Add Xiaomi Miio Zeroconf Discovery
starkillerOG May 7, 2020
3a5b7e1
Add Xiaomi Miio Zeroconf Discovery
starkillerOG May 7, 2020
1e4f4ce
black styling
starkillerOG May 7, 2020
f885c23
remove not shown abort
starkillerOG May 8, 2020
e397045
remove not shown abort
starkillerOG May 8, 2020
559e03f
add debug message on unknown discovered device
starkillerOG May 8, 2020
a413c3c
fix var name
starkillerOG May 8, 2020
250a43c
Add Xiaomi Miio Zeroconf tests
starkillerOG May 8, 2020
5b18d59
fix spelling
starkillerOG May 8, 2020
15b875e
Add Xiaomi Miio Zeroconf discovery
starkillerOG May 8, 2020
9ae9349
Add Xiaomi Miio Zeroconf discovery
starkillerOG May 8, 2020
e371c47
Add Xiaomi Miio Zeroconf discovery
starkillerOG May 8, 2020
f256618
remove backspace
starkillerOG May 8, 2020
2dcaa9d
no backspace
starkillerOG May 8, 2020
3e67214
black formatting
starkillerOG May 8, 2020
117c712
simplify unique_id
starkillerOG May 8, 2020
4efe70f
Merge branch 'patch-25' of https://github.com/starkillerOG/home-assis…
starkillerOG May 8, 2020
25fb107
fix tests
starkillerOG May 8, 2020
c337cec
fix double discovery
starkillerOG May 9, 2020
7ffc859
include "already_in_progress"
starkillerOG May 9, 2020
f645aa6
update alarm at startup
starkillerOG May 9, 2020
6f4245b
Update tests/components/xiaomi_miio/test_config_flow.py
starkillerOG May 13, 2020
b17e700
Update homeassistant/components/xiaomi_miio/config_flow.py
starkillerOG May 13, 2020
480cc59
Update homeassistant/components/xiaomi_miio/config_flow.py
starkillerOG May 13, 2020
466faaa
Update homeassistant/components/xiaomi_miio/__init__.py
starkillerOG May 13, 2020
03ef644
Update strings.json
starkillerOG May 13, 2020
5b7992e
Update en.json
starkillerOG May 13, 2020
1c53c14
use update_before_add=True
starkillerOG May 13, 2020
08bee1c
use format_mac
starkillerOG May 13, 2020
ed2e7bb
fix host -> self.host
starkillerOG May 13, 2020
e87987f
fix black
starkillerOG May 13, 2020
6f40602
Update homeassistant/components/xiaomi_miio/config_flow.py
starkillerOG May 13, 2020
f5c153d
Update homeassistant/components/xiaomi_miio/config_flow.py
starkillerOG May 13, 2020
8903626
Update homeassistant/components/xiaomi_miio/config_flow.py
starkillerOG May 13, 2020
a1df865
fix tests - lowercase mac
starkillerOG May 13, 2020
3f43cc1
fix tests - semicoln mac
starkillerOG May 13, 2020
a6c6aba
fix tests
starkillerOG May 13, 2020
29fbb20
use common async_show_form call
starkillerOG May 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion homeassistant/components/xiaomi_miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ async def async_setup_gateway_entry(
host = entry.data[CONF_HOST]
token = entry.data[CONF_TOKEN]
name = entry.title
gateway_id = entry.data["gateway_id"]
gateway_id = entry.unique_id

# For backwards compat
if entry.unique_id.endswith("-gateway"):
hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"])

# Connect to gateway
gateway = ConnectXiaomiGateway(hass)
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/xiaomi_miio/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
f"{config_entry.title} Alarm",
config_entry.data["model"],
config_entry.data["mac"],
config_entry.data["gateway_id"],
config_entry.unique_id,
)
entities.append(entity)
async_add_entities(entities)
async_add_entities(entities, update_before_add=True)


class XiaomiGatewayAlarm(AlarmControlPanelEntity):
Expand Down
63 changes: 50 additions & 13 deletions homeassistant/components/xiaomi_miio/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
from homeassistant.helpers.device_registry import format_mac

# pylint: disable=unused-import
from .const import DOMAIN
Expand All @@ -15,14 +16,13 @@
CONF_FLOW_TYPE = "config_flow_device"
CONF_GATEWAY = "gateway"
DEFAULT_GATEWAY_NAME = "Xiaomi Gateway"
ZEROCONF_GATEWAY = "lumi-gateway"

GATEWAY_CONFIG = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_GATEWAY_NAME): str,
}
)
GATEWAY_SETTINGS = {
vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_GATEWAY_NAME): str,
}
GATEWAY_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(GATEWAY_SETTINGS)

CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_GATEWAY, default=False): bool})

Expand All @@ -33,6 +33,10 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

def __init__(self):
"""Initialize."""
self.host = None

async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
errors = {}
Expand All @@ -47,36 +51,69 @@ async def async_step_user(self, user_input=None):
step_id="user", data_schema=CONFIG_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_miio")

# Check which device is discovered.
if name.startswith(ZEROCONF_GATEWAY):
unique_id = format_mac(mac_address)
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured({CONF_HOST: self.host})

return await self.async_step_gateway()

# Discovered device is not yet supported
_LOGGER.debug(
"Not yet supported Xiaomi Miio device '%s' discovered with host %s",
name,
self.host,
)
return self.async_abort(reason="not_xiaomi_miio")

async def async_step_gateway(self, user_input=None):
"""Handle a flow initialized by the user to configure a gateway."""
errors = {}
if user_input is not None:
host = user_input[CONF_HOST]
token = user_input[CONF_TOKEN]
if user_input.get(CONF_HOST):
self.host = user_input[CONF_HOST]

# Try to connect to a Xiaomi Gateway.
connect_gateway_class = ConnectXiaomiGateway(self.hass)
await connect_gateway_class.async_connect_gateway(host, token)
await connect_gateway_class.async_connect_gateway(self.host, token)
gateway_info = connect_gateway_class.gateway_info

if gateway_info is not None:
unique_id = f"{gateway_info.model}-{gateway_info.mac_address}-gateway"
mac = format_mac(gateway_info.mac_address)
unique_id = mac
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_NAME],
data={
CONF_FLOW_TYPE: CONF_GATEWAY,
CONF_HOST: host,
CONF_HOST: self.host,
CONF_TOKEN: token,
"gateway_id": unique_id,
"model": gateway_info.model,
"mac": gateway_info.mac_address,
"mac": mac,
},
)

errors["base"] = "connect_error"

if self.host:
return self.async_show_form(
starkillerOG marked this conversation as resolved.
Show resolved Hide resolved
step_id="gateway",
data_schema=vol.Schema(GATEWAY_SETTINGS),
errors=errors,
)

return self.async_show_form(
step_id="gateway", data_schema=GATEWAY_CONFIG, errors=errors
)
3 changes: 2 additions & 1 deletion homeassistant/components/xiaomi_miio/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
"requirements": ["construct==2.9.45", "python-miio==0.5.0.1"],
"codeowners": ["@rytilahti", "@syssi"]
"codeowners": ["@rytilahti", "@syssi"],
"zeroconf": ["_miio._udp.local."]
}
3 changes: 2 additions & 1 deletion homeassistant/components/xiaomi_miio/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"no_device_selected": "No device selected, please select one device."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "Config flow for this Xiaomi Miio device is already in progress."
}
}
}
5 changes: 3 additions & 2 deletions homeassistant/components/xiaomi_miio/translations/en.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
"already_configured": "Device is already configured",
"already_in_progress": "Config flow for this Xiaomi Miio device is already in progress."
},
"error": {
"connect_error": "Failed to connect, please try again",
Expand All @@ -26,4 +27,4 @@
}
}
}
}
}
3 changes: 3 additions & 0 deletions homeassistant/generated/zeroconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"_ipps._tcp.local.": [
"ipp"
],
"_miio._udp.local.": [
"xiaomi_miio"
],
"_printer._tcp.local.": [
"brother"
],
Expand Down
88 changes: 85 additions & 3 deletions tests/components/xiaomi_miio/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@
from miio import DeviceException

from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.xiaomi_miio import config_flow, const
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN

from tests.async_mock import Mock, patch

ZEROCONF_NAME = "name"
ZEROCONF_PROP = "properties"
ZEROCONF_MAC = "mac"

TEST_HOST = "1.2.3.4"
TEST_TOKEN = "12345678901234567890123456789012"
TEST_NAME = "Test_Gateway"
TEST_MODEL = "model5"
TEST_MAC = "AB-CD-EF-GH-IJ-KL"
TEST_GATEWAY_ID = f"{TEST_MODEL}-{TEST_MAC}-gateway"
TEST_MAC = "ab:cd:ef:gh:ij:kl"
TEST_GATEWAY_ID = TEST_MAC
TEST_HARDWARE_VERSION = "AB123"
TEST_FIRMWARE_VERSION = "1.2.3_456"
TEST_ZEROCONF_NAME = "lumi-gateway-v3_miio12345678._miio._udp.local."


def get_mock_info(
Expand Down Expand Up @@ -119,7 +125,83 @@ async def test_config_flow_gateway_success(hass):
config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
"gateway_id": TEST_GATEWAY_ID,
"model": TEST_MODEL,
"mac": TEST_MAC,
}


async def test_zeroconf_gateway_success(hass):
"""Test a successful zeroconf discovery of a 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"] == "gateway"
assert result["errors"] == {}

mock_info = get_mock_info()

with patch(
"homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info",
return_value=mock_info,
), patch(
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN},
)

assert result["type"] == "create_entry"
assert result["title"] == TEST_NAME
assert result["data"] == {
config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
"model": TEST_MODEL,
"mac": TEST_MAC,
}


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-miio-device",
ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC},
},
)

assert result["type"] == "abort"
assert result["reason"] == "not_xiaomi_miio"


async def test_zeroconf_no_data(hass):
"""Test a failed zeroconf discovery because of no data."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data={}
)

assert result["type"] == "abort"
assert result["reason"] == "not_xiaomi_miio"


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_miio"