mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
Add integration for Vogel's MotionMount (#103498)
* Skeleton for Vogel's MotionMount support. * Generated updates. * Add validation of the discovered information. * Add manual configuration * Use a mac address as a unique id * Add tests for config_flow * Add a 'turn' sensor entity. * Add all needed sensors. * Add number and select entity for control of MotionMount * Update based on development checklist * Preset selector now updates when a preset is chosen * Fix adding presets selector to device * Remove irrelevant TODO * Bump python-MotionMount requirement * Invert direction of turn slider * Prepare for PR * Make sure entities have correct values when created * Use device's mac address as unique id for entities. * Fix missing files in .coveragerc * Remove typing ignore from device library. Improved typing also gave rise to the need to improve the callback mechanism * Improve typing * Convert property to shorthand form * Remove unneeded CONF_NAME in ConfigEntry * Add small comment * Refresh coordinator on notification from MotionMount * Use translation for entity * Bump python-MotionMount * Raise `ConfigEntryNotReady` when connect fails * Use local variable * Improve exception handling * Reduce duplicate code * Make better use of constants * Remove unneeded callback * Remove other occurrence of unneeded callback * Improve removal of suffix * Catch 'getaddrinfo' exception * Add config flow tests for invalid hostname * Abort if device with same hostname is already configured * Make sure we connect to a device with the same unique id as configured * Convert function names to snake_case * Remove unneeded commented-out code * Use tuple * Make us of config_entry id when mac is missing * Prevent update of entities when nothing changed * Don't store data in `hass.data` until we know we will proceed * Remove coordinator * Handle situation where mac is EMPTY_MAC * Disable polling * Fix failing hassfest * Avoid calling unique-id-less discovery handler for situations where we've an unique id
This commit is contained in:
parent
c824d06a8c
commit
2c2e6171e2
@ -757,6 +757,9 @@ omit =
|
||||
homeassistant/components/motion_blinds/cover.py
|
||||
homeassistant/components/motion_blinds/entity.py
|
||||
homeassistant/components/motion_blinds/sensor.py
|
||||
homeassistant/components/motionmount/__init__.py
|
||||
homeassistant/components/motionmount/entity.py
|
||||
homeassistant/components/motionmount/number.py
|
||||
homeassistant/components/mpd/media_player.py
|
||||
homeassistant/components/mqtt_room/sensor.py
|
||||
homeassistant/components/msteams/notify.py
|
||||
|
@ -238,6 +238,7 @@ homeassistant.components.modbus.*
|
||||
homeassistant.components.modem_callerid.*
|
||||
homeassistant.components.moon.*
|
||||
homeassistant.components.mopeka.*
|
||||
homeassistant.components.motionmount.*
|
||||
homeassistant.components.mqtt.*
|
||||
homeassistant.components.mysensors.*
|
||||
homeassistant.components.nam.*
|
||||
|
@ -809,6 +809,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/motion_blinds/ @starkillerOG
|
||||
/homeassistant/components/motioneye/ @dermotduffy
|
||||
/tests/components/motioneye/ @dermotduffy
|
||||
/homeassistant/components/motionmount/ @RJPoelstra
|
||||
/tests/components/motionmount/ @RJPoelstra
|
||||
/homeassistant/components/mqtt/ @emontnemery @jbouwh
|
||||
/tests/components/mqtt/ @emontnemery @jbouwh
|
||||
/homeassistant/components/msteams/ @peroyvind
|
||||
|
61
homeassistant/components/motionmount/__init__.py
Normal file
61
homeassistant/components/motionmount/__init__.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""The Vogel's MotionMount integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
|
||||
import motionmount
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import DOMAIN, EMPTY_MAC
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.NUMBER,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Vogel's MotionMount from a config entry."""
|
||||
|
||||
host = entry.data[CONF_HOST]
|
||||
|
||||
# Create API instance
|
||||
mm = motionmount.MotionMount(host, entry.data[CONF_PORT])
|
||||
|
||||
# Validate the API connection
|
||||
try:
|
||||
await mm.connect()
|
||||
except (ConnectionError, TimeoutError, socket.gaierror) as ex:
|
||||
raise ConfigEntryNotReady(f"Failed to connect to {host}") from ex
|
||||
|
||||
found_mac = format_mac(mm.mac.hex())
|
||||
if found_mac not in (EMPTY_MAC, entry.unique_id):
|
||||
# If the mac address of the device does not match the unique_id
|
||||
# of the config entry, it likely means the DHCP lease has expired
|
||||
# and the device has been assigned a new IP address. We need to
|
||||
# wait for the next discovery to find the device at its new address
|
||||
# and update the config entry so we do not mix up devices.
|
||||
await mm.disconnect()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unexpected device found at {host}; expected {entry.unique_id}, found {found_mac}"
|
||||
)
|
||||
|
||||
# Store an API object for your platforms to access
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = mm
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
mm: motionmount.MotionMount = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
await mm.disconnect()
|
||||
|
||||
return unload_ok
|
176
homeassistant/components/motionmount/config_flow.py
Normal file
176
homeassistant/components/motionmount/config_flow.py
Normal file
@ -0,0 +1,176 @@
|
||||
"""Config flow for Vogel's MotionMount."""
|
||||
import logging
|
||||
import socket
|
||||
from typing import Any
|
||||
|
||||
import motionmount
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_UUID
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import DOMAIN, EMPTY_MAC
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# A MotionMount can be in four states:
|
||||
# 1. Old CE and old Pro FW -> It doesn't supply any kind of mac
|
||||
# 2. Old CE but new Pro FW -> It supplies its mac using DNS-SD, but a read of the mac fails
|
||||
# 3. New CE but old Pro FW -> It doesn't supply the mac using DNS-SD but we can read it (returning the EMPTY_MAC)
|
||||
# 4. New CE and new Pro FW -> Both DNS-SD and a read gives us the mac
|
||||
# If we can't get the mac, we use DEFAULT_DISCOVERY_UNIQUE_ID as an ID, so we can always configure a single MotionMount. Most households will only have a single MotionMount
|
||||
class MotionMountFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Vogel's MotionMount config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Set up the instance."""
|
||||
self.discovery_info: dict[str, Any] = {}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initiated by the user."""
|
||||
if user_input is None:
|
||||
return self._show_setup_form()
|
||||
|
||||
info = {}
|
||||
try:
|
||||
info = await self._validate_input(user_input)
|
||||
except (ConnectionError, socket.gaierror):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except TimeoutError:
|
||||
return self.async_abort(reason="time_out")
|
||||
except motionmount.NotConnectedError:
|
||||
return self.async_abort(reason="not_connected")
|
||||
except motionmount.MotionMountResponseError:
|
||||
# This is most likely due to missing support for the mac address property
|
||||
# Abort if the handler has config entries already
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
# Otherwise we try to continue with the generic uid
|
||||
info[CONF_UUID] = config_entries.DEFAULT_DISCOVERY_UNIQUE_ID
|
||||
|
||||
# If the device mac is valid we use it, otherwise we use the default id
|
||||
if info.get(CONF_UUID, EMPTY_MAC) != EMPTY_MAC:
|
||||
unique_id = info[CONF_UUID]
|
||||
else:
|
||||
unique_id = config_entries.DEFAULT_DISCOVERY_UNIQUE_ID
|
||||
|
||||
name = info.get(CONF_NAME, user_input[CONF_HOST])
|
||||
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_create_entry(title=name, data=user_input)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
) -> FlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
|
||||
# Extract information from discovery
|
||||
host = discovery_info.hostname
|
||||
port = discovery_info.port
|
||||
zctype = discovery_info.type
|
||||
name = discovery_info.name.removesuffix(f".{zctype}")
|
||||
unique_id = discovery_info.properties.get("mac")
|
||||
|
||||
self.discovery_info.update(
|
||||
{
|
||||
CONF_HOST: host,
|
||||
CONF_PORT: port,
|
||||
CONF_NAME: name,
|
||||
}
|
||||
)
|
||||
|
||||
if unique_id:
|
||||
# If we already have the unique id, try to set it now
|
||||
# so we can avoid probing the device if its already
|
||||
# configured or ignored
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: host, CONF_PORT: port}
|
||||
)
|
||||
else:
|
||||
# Avoid probing devices that already have an entry
|
||||
self._async_abort_entries_match({CONF_HOST: host})
|
||||
|
||||
self.context.update({"title_placeholders": {"name": name}})
|
||||
|
||||
try:
|
||||
info = await self._validate_input(self.discovery_info)
|
||||
except (ConnectionError, socket.gaierror):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except TimeoutError:
|
||||
return self.async_abort(reason="time_out")
|
||||
except motionmount.NotConnectedError:
|
||||
return self.async_abort(reason="not_connected")
|
||||
except motionmount.MotionMountResponseError:
|
||||
info = {}
|
||||
# We continue as we want to be able to connect with older FW that does not support MAC address
|
||||
|
||||
# If the device supplied as with a valid MAC we use that
|
||||
if info.get(CONF_UUID, EMPTY_MAC) != EMPTY_MAC:
|
||||
unique_id = info[CONF_UUID]
|
||||
|
||||
if unique_id:
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: host, CONF_PORT: port}
|
||||
)
|
||||
else:
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
|
||||
async def async_step_zeroconf_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a confirmation flow initiated by zeroconf."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
description_placeholders={CONF_NAME: self.discovery_info[CONF_NAME]},
|
||||
errors={},
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self.discovery_info[CONF_NAME],
|
||||
data=self.discovery_info,
|
||||
)
|
||||
|
||||
async def _validate_input(self, data: dict) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
mm = motionmount.MotionMount(data[CONF_HOST], data[CONF_PORT])
|
||||
try:
|
||||
await mm.connect()
|
||||
finally:
|
||||
await mm.disconnect()
|
||||
|
||||
return {CONF_UUID: format_mac(mm.mac.hex()), CONF_NAME: mm.name}
|
||||
|
||||
def _show_setup_form(self, errors: dict[str, str] | None = None) -> FlowResult:
|
||||
"""Show the setup form to the user."""
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=23): int,
|
||||
}
|
||||
),
|
||||
errors=errors or {},
|
||||
)
|
5
homeassistant/components/motionmount/const.py
Normal file
5
homeassistant/components/motionmount/const.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""Constants for the Vogel's MotionMount integration."""
|
||||
|
||||
DOMAIN = "motionmount"
|
||||
|
||||
EMPTY_MAC = "00:00:00:00:00:00"
|
53
homeassistant/components/motionmount/entity.py
Normal file
53
homeassistant/components/motionmount/entity.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""Support for MotionMount sensors."""
|
||||
|
||||
import motionmount
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_CONNECTIONS, ATTR_IDENTIFIERS
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN, EMPTY_MAC
|
||||
|
||||
|
||||
class MotionMountEntity(Entity):
|
||||
"""Representation of a MotionMount entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, mm: motionmount.MotionMount, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize general MotionMount entity."""
|
||||
self.mm = mm
|
||||
mac = format_mac(mm.mac.hex())
|
||||
|
||||
# Create a base unique id
|
||||
if mac == EMPTY_MAC:
|
||||
self._base_unique_id = config_entry.entry_id
|
||||
else:
|
||||
self._base_unique_id = mac
|
||||
|
||||
# Set device info
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=mm.name,
|
||||
manufacturer="Vogel's",
|
||||
model="TVM 7675",
|
||||
)
|
||||
|
||||
if mac == EMPTY_MAC:
|
||||
self._attr_device_info[ATTR_IDENTIFIERS] = {(DOMAIN, config_entry.entry_id)}
|
||||
else:
|
||||
self._attr_device_info[ATTR_CONNECTIONS] = {
|
||||
(dr.CONNECTION_NETWORK_MAC, mac)
|
||||
}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Store register state change callback."""
|
||||
self.mm.add_listener(self.async_write_ha_state)
|
||||
await super().async_added_to_hass()
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove register state change callback."""
|
||||
self.mm.remove_listener(self.async_write_ha_state)
|
||||
await super().async_will_remove_from_hass()
|
11
homeassistant/components/motionmount/manifest.json
Normal file
11
homeassistant/components/motionmount/manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "motionmount",
|
||||
"name": "Vogel's MotionMount",
|
||||
"codeowners": ["@RJPoelstra"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/motionmount",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["python-MotionMount==0.3.1"],
|
||||
"zeroconf": ["_tvm._tcp.local."]
|
||||
}
|
71
homeassistant/components/motionmount/number.py
Normal file
71
homeassistant/components/motionmount/number.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""Support for MotionMount numeric control."""
|
||||
import motionmount
|
||||
|
||||
from homeassistant.components.number import NumberEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import MotionMountEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up Vogel's MotionMount from a config entry."""
|
||||
mm: motionmount.MotionMount = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
(
|
||||
MotionMountExtension(mm, entry),
|
||||
MotionMountTurn(mm, entry),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class MotionMountExtension(MotionMountEntity, NumberEntity):
|
||||
"""The target extension position of a MotionMount."""
|
||||
|
||||
_attr_native_max_value = 100
|
||||
_attr_native_min_value = 0
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_translation_key = "motionmount_extension"
|
||||
|
||||
def __init__(self, mm: motionmount.MotionMount, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize Extension number."""
|
||||
super().__init__(mm, config_entry)
|
||||
self._attr_unique_id = f"{self._base_unique_id}-extension"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Get native value."""
|
||||
return float(self.mm.extension or 0)
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set the new value for extension."""
|
||||
await self.mm.set_extension(int(value))
|
||||
|
||||
|
||||
class MotionMountTurn(MotionMountEntity, NumberEntity):
|
||||
"""The target turn position of a MotionMount."""
|
||||
|
||||
_attr_native_max_value = 100
|
||||
_attr_native_min_value = -100
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_translation_key = "motionmount_turn"
|
||||
|
||||
def __init__(self, mm: motionmount.MotionMount, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize Turn number."""
|
||||
super().__init__(mm, config_entry)
|
||||
self._attr_unique_id = f"{self._base_unique_id}-turn"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Get native value."""
|
||||
return float(self.mm.turn or 0) * -1
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set the new value for turn."""
|
||||
await self.mm.set_turn(int(value * -1))
|
37
homeassistant/components/motionmount/strings.json
Normal file
37
homeassistant/components/motionmount/strings.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Link your MotionMount",
|
||||
"description": "Set up your MotionMount to integrate with Home Assistant.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to set up {name}?",
|
||||
"title": "Discovered MotionMount"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"time_out": "Failed to connect due to a time out.",
|
||||
"not_connected": "Failed to connect.",
|
||||
"invalid_response": "Failed to connect due to an invalid response from the MotionMount."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"number": {
|
||||
"motionmount_extension": {
|
||||
"name": "Extension"
|
||||
},
|
||||
"motionmount_turn": {
|
||||
"name": "Turn"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -304,6 +304,7 @@ FLOWS = {
|
||||
"mopeka",
|
||||
"motion_blinds",
|
||||
"motioneye",
|
||||
"motionmount",
|
||||
"mqtt",
|
||||
"mullvad",
|
||||
"mutesync",
|
||||
|
@ -3613,6 +3613,12 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"motionmount": {
|
||||
"name": "Vogel's MotionMount",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"mpd": {
|
||||
"name": "Music Player Daemon (MPD)",
|
||||
"integration_type": "hub",
|
||||
|
@ -705,6 +705,11 @@ ZEROCONF = {
|
||||
"domain": "apple_tv",
|
||||
},
|
||||
],
|
||||
"_tvm._tcp.local.": [
|
||||
{
|
||||
"domain": "motionmount",
|
||||
},
|
||||
],
|
||||
"_uzg-01._tcp.local.": [
|
||||
{
|
||||
"domain": "zha",
|
||||
|
10
mypy.ini
10
mypy.ini
@ -2141,6 +2141,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.motionmount.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.mqtt.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
@ -2132,6 +2132,9 @@ pytfiac==0.4
|
||||
# homeassistant.components.thinkingcleaner
|
||||
pythinkingcleaner==0.0.3
|
||||
|
||||
# homeassistant.components.motionmount
|
||||
python-MotionMount==0.3.1
|
||||
|
||||
# homeassistant.components.awair
|
||||
python-awair==0.2.4
|
||||
|
||||
|
@ -1622,6 +1622,9 @@ pytankerkoenig==0.0.6
|
||||
# homeassistant.components.tautulli
|
||||
pytautulli==23.1.1
|
||||
|
||||
# homeassistant.components.motionmount
|
||||
python-MotionMount==0.3.1
|
||||
|
||||
# homeassistant.components.awair
|
||||
python-awair==0.2.4
|
||||
|
||||
|
42
tests/components/motionmount/__init__.py
Normal file
42
tests/components/motionmount/__init__.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""Tests for the Vogel's MotionMount integration."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
|
||||
HOST = "192.168.1.31"
|
||||
PORT = 23
|
||||
|
||||
TVM_ZEROCONF_SERVICE_TYPE = "_tvm._tcp.local."
|
||||
|
||||
ZEROCONF_NAME = "My MotionMount"
|
||||
ZEROCONF_HOST = HOST
|
||||
ZEROCONF_HOSTNAME = "MMF8A55F.local."
|
||||
ZEROCONF_PORT = PORT
|
||||
ZEROCONF_MAC = "c4:dd:57:f8:a5:5f"
|
||||
|
||||
MOCK_USER_INPUT = {
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
}
|
||||
|
||||
MOCK_ZEROCONF_TVM_SERVICE_INFO_V1 = zeroconf.ZeroconfServiceInfo(
|
||||
type=TVM_ZEROCONF_SERVICE_TYPE,
|
||||
name=f"{ZEROCONF_NAME}.{TVM_ZEROCONF_SERVICE_TYPE}",
|
||||
ip_address=ip_address(ZEROCONF_HOST),
|
||||
ip_addresses=[ip_address(ZEROCONF_HOST)],
|
||||
hostname=ZEROCONF_HOSTNAME,
|
||||
port=ZEROCONF_PORT,
|
||||
properties={"txtvers": "1", "model": "TVM 7675"},
|
||||
)
|
||||
|
||||
MOCK_ZEROCONF_TVM_SERVICE_INFO_V2 = zeroconf.ZeroconfServiceInfo(
|
||||
type=TVM_ZEROCONF_SERVICE_TYPE,
|
||||
name=f"{ZEROCONF_NAME}.{TVM_ZEROCONF_SERVICE_TYPE}",
|
||||
ip_address=ip_address(ZEROCONF_HOST),
|
||||
ip_addresses=[ip_address(ZEROCONF_HOST)],
|
||||
hostname=ZEROCONF_HOSTNAME,
|
||||
port=ZEROCONF_PORT,
|
||||
properties={"mac": ZEROCONF_MAC, "txtvers": "2", "model": "TVM 7675"},
|
||||
)
|
44
tests/components/motionmount/conftest.py
Normal file
44
tests/components/motionmount/conftest.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Fixtures for Vogel's MotionMount integration tests."""
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.motionmount.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
|
||||
from . import HOST, PORT, ZEROCONF_MAC, ZEROCONF_NAME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
title=ZEROCONF_NAME,
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: HOST, CONF_PORT: PORT},
|
||||
unique_id=ZEROCONF_MAC,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Mock setting up a config entry."""
|
||||
with patch(
|
||||
"homeassistant.components.motionmount.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_motionmount_config_flow() -> Generator[None, MagicMock, None]:
|
||||
"""Return a mocked MotionMount config flow."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.motionmount.config_flow.motionmount.MotionMount",
|
||||
autospec=True,
|
||||
) as motionmount_mock:
|
||||
client = motionmount_mock.return_value
|
||||
yield client
|
488
tests/components/motionmount/test_config_flow.py
Normal file
488
tests/components/motionmount/test_config_flow.py
Normal file
@ -0,0 +1,488 @@
|
||||
"""Tests for the Vogel's MotionMount config flow."""
|
||||
import dataclasses
|
||||
import socket
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import motionmount
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.motionmount.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import (
|
||||
HOST,
|
||||
MOCK_USER_INPUT,
|
||||
MOCK_ZEROCONF_TVM_SERVICE_INFO_V1,
|
||||
MOCK_ZEROCONF_TVM_SERVICE_INFO_V2,
|
||||
PORT,
|
||||
ZEROCONF_HOSTNAME,
|
||||
ZEROCONF_MAC,
|
||||
ZEROCONF_NAME,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
MAC = bytes.fromhex("c4dd57f8a55f")
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||
|
||||
|
||||
async def test_show_user_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the user set up form is served."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
|
||||
|
||||
async def test_user_connection_error(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow is aborted when there is an connection error."""
|
||||
mock_motionmount_config_flow.connect.side_effect = ConnectionRefusedError()
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_user_connection_error_invalid_hostname(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow is aborted when an invalid hostname is provided."""
|
||||
mock_motionmount_config_flow.connect.side_effect = socket.gaierror()
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_user_timeout_error(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow is aborted when there is a timeout error."""
|
||||
mock_motionmount_config_flow.connect.side_effect = TimeoutError()
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "time_out"
|
||||
|
||||
|
||||
async def test_user_not_connected_error(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow is aborted when there is a not connected error."""
|
||||
mock_motionmount_config_flow.connect.side_effect = motionmount.NotConnectedError()
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "not_connected"
|
||||
|
||||
|
||||
async def test_user_response_error_single_device_old_ce_old_new_pro(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow creates an entry when there is a response error."""
|
||||
mock_motionmount_config_flow.connect.side_effect = (
|
||||
motionmount.MotionMountResponseError(motionmount.MotionMountResponse.NotFound)
|
||||
)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == HOST
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
assert result["data"][CONF_PORT] == PORT
|
||||
|
||||
assert result["result"]
|
||||
|
||||
|
||||
async def test_user_response_error_single_device_new_ce_old_pro(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow creates an entry when there is a response error."""
|
||||
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
||||
type(mock_motionmount_config_flow).mac = PropertyMock(
|
||||
return_value=b"\x00\x00\x00\x00\x00\x00"
|
||||
)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == ZEROCONF_NAME
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
assert result["data"][CONF_PORT] == PORT
|
||||
|
||||
assert result["result"]
|
||||
|
||||
|
||||
async def test_user_response_error_single_device_new_ce_new_pro(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow creates an entry when there is a response error."""
|
||||
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
||||
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == ZEROCONF_NAME
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
assert result["data"][CONF_PORT] == PORT
|
||||
|
||||
assert result["result"]
|
||||
assert result["result"].unique_id == ZEROCONF_MAC
|
||||
|
||||
|
||||
async def test_user_response_error_multi_device_old_ce_old_new_pro(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow is aborted when there are multiple devices."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
mock_motionmount_config_flow.connect.side_effect = (
|
||||
motionmount.MotionMountResponseError(motionmount.MotionMountResponse.NotFound)
|
||||
)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_user_response_error_multi_device_new_ce_new_pro(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow is aborted when there are multiple devices."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
||||
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_connection_error(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow is aborted when there is an connection error."""
|
||||
mock_motionmount_config_flow.connect.side_effect = ConnectionRefusedError()
|
||||
|
||||
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V1)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_zeroconf_connection_error_invalid_hostname(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow is aborted when there is an connection error."""
|
||||
mock_motionmount_config_flow.connect.side_effect = socket.gaierror()
|
||||
|
||||
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V1)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_zeroconf_timout_error(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow is aborted when there is a timeout error."""
|
||||
mock_motionmount_config_flow.connect.side_effect = TimeoutError()
|
||||
|
||||
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V1)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "time_out"
|
||||
|
||||
|
||||
async def test_zeroconf_not_connected_error(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the flow is aborted when there is a not connected error."""
|
||||
mock_motionmount_config_flow.connect.side_effect = motionmount.NotConnectedError()
|
||||
|
||||
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V1)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "not_connected"
|
||||
|
||||
|
||||
async def test_show_zeroconf_form_old_ce_old_pro(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
mock_motionmount_config_flow.connect.side_effect = (
|
||||
motionmount.MotionMountResponseError(motionmount.MotionMountResponse.NotFound)
|
||||
)
|
||||
|
||||
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V1)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"}
|
||||
|
||||
|
||||
async def test_show_zeroconf_form_old_ce_new_pro(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
mock_motionmount_config_flow.connect.side_effect = (
|
||||
motionmount.MotionMountResponseError(motionmount.MotionMountResponse.NotFound)
|
||||
)
|
||||
|
||||
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V2)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"}
|
||||
|
||||
|
||||
async def test_show_zeroconf_form_new_ce_old_pro(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
type(mock_motionmount_config_flow).mac = PropertyMock(
|
||||
return_value=b"\x00\x00\x00\x00\x00\x00"
|
||||
)
|
||||
|
||||
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V1)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"}
|
||||
|
||||
|
||||
async def test_show_zeroconf_form_new_ce_new_pro(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||
|
||||
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V2)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"}
|
||||
|
||||
|
||||
async def test_zeroconf_device_exists_abort(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow if device already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V2)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_full_user_flow_implementation(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
||||
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_USER_INPUT.copy(),
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == ZEROCONF_NAME
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
assert result["data"][CONF_PORT] == PORT
|
||||
|
||||
assert result["result"]
|
||||
assert result["result"].unique_id == ZEROCONF_MAC
|
||||
|
||||
|
||||
async def test_full_zeroconf_flow_implementation(
|
||||
hass: HomeAssistant,
|
||||
mock_motionmount_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
||||
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||
|
||||
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V2)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == ZEROCONF_NAME
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == ZEROCONF_HOSTNAME
|
||||
assert result["data"][CONF_PORT] == PORT
|
||||
assert result["data"][CONF_NAME] == ZEROCONF_NAME
|
||||
|
||||
assert result["result"]
|
||||
assert result["result"].unique_id == ZEROCONF_MAC
|
Loading…
x
Reference in New Issue
Block a user