Refactor Enocean part 1 (#35927)

* First step of an EnOcean integration refactoring, including code reorganisation and support of a setup config flow

* Moved title to root of strings file

* Fixed pre-commit checks failures

* Fixed linter errors

* Updated formatted string format in logs

* Removed leftover comment

* Multiple changes after PR change requests.
Using an import flow for yaml config, removed unnecessary logs, added proper unload in __init__ and EnOceanDongle
Replaced config state machine by several flows.
Serial port validity check done in the EnOceanDongle class asynchronously, removed unique ID from config flow
Multiple cosmetic changes

* Multiple changes after PR change requests

* Added variable to store default value, as setdefault was caught returning None when the empty dict literal was passed as an argument

* Literal used directly

* Added tests for EnOcean config flows, changed static methods to bundle methods for bundle

* Updated variable name

* Added missing mock to test, replaced repeated magic strings by constants

* Changed imports to avoid an unused import warning from pylint on DOMAIN

* Adding pylint exception for unused import

* Added proper propagation of setup and unload to platforms, removed dead code, some syntax changes

* Removed setup_entry forwarding as the entities can only be configured using yaml

* Removed forwarding of unload

* Enabled code coverage for config flow only

* Clean up coveragerc

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
jduquennoy 2020-07-09 02:46:38 +02:00 committed by GitHub
parent 872140123d
commit af6a4bb6cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 514 additions and 82 deletions

View file

@ -214,7 +214,14 @@ omit =
homeassistant/components/emoncms_history/*
homeassistant/components/emulated_hue/upnp.py
homeassistant/components/enigma2/media_player.py
homeassistant/components/enocean/*
homeassistant/components/enocean/__init__.py
homeassistant/components/enocean/binary_sensor.py
homeassistant/components/enocean/const.py
homeassistant/components/enocean/device.py
homeassistant/components/enocean/dongle.py
homeassistant/components/enocean/light.py
homeassistant/components/enocean/sensor.py
homeassistant/components/enocean/switch.py
homeassistant/components/enphase_envoy/sensor.py
homeassistant/components/entur_public_transport/*
homeassistant/components/environment_canada/*

View file

@ -1,93 +1,57 @@
"""Support for EnOcean devices."""
import logging
from enocean.communicators.serialcommunicator import SerialCommunicator
from enocean.protocol.packet import Packet, RadioPacket
from enocean.utils import combine_hex
import voluptuous as vol
from homeassistant import config_entries, core
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_DEVICE
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
DOMAIN = "enocean"
DATA_ENOCEAN = "enocean"
from .const import DATA_ENOCEAN, DOMAIN, ENOCEAN_DONGLE
from .dongle import EnOceanDongle
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema({vol.Required(CONF_DEVICE): cv.string})}, extra=vol.ALLOW_EXTRA
)
SIGNAL_RECEIVE_MESSAGE = "enocean.receive_message"
SIGNAL_SEND_MESSAGE = "enocean.send_message"
def setup(hass, config):
async def async_setup(hass, config):
"""Set up the EnOcean component."""
serial_dev = config[DOMAIN].get(CONF_DEVICE)
dongle = EnOceanDongle(hass, serial_dev)
hass.data[DATA_ENOCEAN] = dongle
# support for text-based configuration (legacy)
if DOMAIN not in config:
return True
if hass.config_entries.async_entries(DOMAIN):
# We can only have one dongle. If there is already one in the config,
# there is no need to import the yaml based config.
return True
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
)
)
return True
class EnOceanDongle:
"""Representation of an EnOcean dongle."""
async def async_setup_entry(
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
):
"""Set up an EnOcean dongle for the given entry."""
enocean_data = hass.data.setdefault(DATA_ENOCEAN, {})
usb_dongle = EnOceanDongle(hass, config_entry.data[CONF_DEVICE])
await usb_dongle.async_setup()
enocean_data[ENOCEAN_DONGLE] = usb_dongle
def __init__(self, hass, ser):
"""Initialize the EnOcean dongle."""
self.__communicator = SerialCommunicator(port=ser, callback=self.callback)
self.__communicator.start()
self.hass = hass
self.hass.helpers.dispatcher.dispatcher_connect(
SIGNAL_SEND_MESSAGE, self._send_message_callback
)
def _send_message_callback(self, command):
"""Send a command through the EnOcean dongle."""
self.__communicator.send(command)
def callback(self, packet):
"""Handle EnOcean device's callback.
This is the callback function called by python-enocan whenever there
is an incoming packet.
"""
if isinstance(packet, RadioPacket):
_LOGGER.debug("Received radio packet: %s", packet)
self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_RECEIVE_MESSAGE, packet)
return True
class EnOceanDevice(Entity):
"""Parent class for all devices associated with the EnOcean component."""
async def async_unload_entry(hass, config_entry):
"""Unload ENOcean config entry."""
def __init__(self, dev_id, dev_name="EnOcean device"):
"""Initialize the device."""
self.dev_id = dev_id
self.dev_name = dev_name
enocean_dongle = hass.data[DATA_ENOCEAN][ENOCEAN_DONGLE]
enocean_dongle.unload()
hass.data.pop(DATA_ENOCEAN)
async def async_added_to_hass(self):
"""Register callbacks."""
self.async_on_remove(
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_RECEIVE_MESSAGE, self._message_received_callback
)
)
def _message_received_callback(self, packet):
"""Handle incoming packets."""
if packet.sender_int == combine_hex(self.dev_id):
self.value_changed(packet)
def value_changed(self, packet):
"""Update the internal state of the device when a packet arrives."""
def send_command(self, data, optional, packet_type):
"""Send a command via the EnOcean dongle."""
packet = Packet(packet_type, data=data, optional=optional)
self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_SEND_MESSAGE, packet)
return True

View file

@ -3,7 +3,6 @@ import logging
import voluptuous as vol
from homeassistant.components import enocean
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA,
@ -12,6 +11,8 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv
from .device import EnOceanEntity
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "EnOcean binary sensor"
@ -36,7 +37,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([EnOceanBinarySensor(dev_id, dev_name, device_class)])
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorEntity):
class EnOceanBinarySensor(EnOceanEntity, BinarySensorEntity):
"""Representation of EnOcean binary sensors such as wall switches.
Supported EEPs (EnOcean Equipment Profiles):

View file

@ -0,0 +1,94 @@
"""Config flows for the ENOcean integration."""
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import CONN_CLASS_ASSUMED
from homeassistant.const import CONF_DEVICE
from . import dongle
from .const import DOMAIN # pylint:disable=unused-import
from .const import ERROR_INVALID_DONGLE_PATH, LOGGER
class EnOceanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle the enOcean config flows."""
VERSION = 1
MANUAL_PATH_VALUE = "Custom path"
CONNECTION_CLASS = CONN_CLASS_ASSUMED
def __init__(self):
"""Initialize the EnOcean config flow."""
self.dongle_path = None
self.discovery_info = None
async def async_step_import(self, data=None):
"""Import a yaml configuration."""
if not await self.validate_enocean_conf(data):
LOGGER.warning(
"Cannot import yaml configuration: %s is not a valid dongle path",
data[CONF_DEVICE],
)
return self.async_abort(reason="invalid_dongle_path")
return self.create_enocean_entry(data)
async def async_step_user(self, user_input=None):
"""Handle an EnOcean config flow start."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return await self.async_step_detect()
async def async_step_detect(self, user_input=None):
"""Propose a list of detected dongles."""
errors = {}
if user_input is not None:
if user_input[CONF_DEVICE] == self.MANUAL_PATH_VALUE:
return await self.async_step_manual(None)
if await self.validate_enocean_conf(user_input):
return self.create_enocean_entry(user_input)
errors = {CONF_DEVICE: ERROR_INVALID_DONGLE_PATH}
bridges = await self.hass.async_add_executor_job(dongle.detect)
if len(bridges) == 0:
return await self.async_step_manual(user_input)
bridges.append(self.MANUAL_PATH_VALUE)
return self.async_show_form(
step_id="detect",
data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(bridges)}),
errors=errors,
)
async def async_step_manual(self, user_input=None):
"""Request manual USB dongle path."""
default_value = None
errors = {}
if user_input is not None:
if await self.validate_enocean_conf(user_input):
return self.create_enocean_entry(user_input)
default_value = user_input[CONF_DEVICE]
errors = {CONF_DEVICE: ERROR_INVALID_DONGLE_PATH}
return self.async_show_form(
step_id="manual",
data_schema=vol.Schema(
{vol.Required(CONF_DEVICE, default=default_value): str}
),
errors=errors,
)
async def validate_enocean_conf(self, user_input) -> bool:
"""Return True if the user_input contains a valid dongle path."""
dongle_path = user_input[CONF_DEVICE]
path_is_valid = await self.hass.async_add_executor_job(
dongle.validate_path, dongle_path
)
return path_is_valid
def create_enocean_entry(self, user_input):
"""Create an entry for the provided configuration."""
return self.async_create_entry(title="EnOcean", data=user_input)

View file

@ -0,0 +1,15 @@
"""Constants for the ENOcean integration."""
import logging
DOMAIN = "enocean"
DATA_ENOCEAN = "enocean"
ENOCEAN_DONGLE = "dongle"
ERROR_INVALID_DONGLE_PATH = "invalid_dongle_path"
SIGNAL_RECEIVE_MESSAGE = "enocean.receive_message"
SIGNAL_SEND_MESSAGE = "enocean.send_message"
LOGGER = logging.getLogger(__package__)
PLATFORMS = ["light", "binary_sensor", "sensor", "switch"]

View file

@ -0,0 +1,39 @@
"""Representation of an EnOcean device."""
from enocean.protocol.packet import Packet
from enocean.utils import combine_hex
from homeassistant.helpers.entity import Entity
from .const import SIGNAL_RECEIVE_MESSAGE, SIGNAL_SEND_MESSAGE
class EnOceanEntity(Entity):
"""Parent class for all entities associated with the EnOcean component."""
def __init__(self, dev_id, dev_name="EnOcean device"):
"""Initialize the device."""
self.dev_id = dev_id
self.dev_name = dev_name
async def async_added_to_hass(self):
"""Register callbacks."""
self.async_on_remove(
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_RECEIVE_MESSAGE, self._message_received_callback
)
)
def _message_received_callback(self, packet):
"""Handle incoming packets."""
if packet.sender_int == combine_hex(self.dev_id):
self.value_changed(packet)
def value_changed(self, packet):
"""Update the internal state of the device when a packet arrives."""
def send_command(self, data, optional, packet_type):
"""Send a command via the EnOcean dongle."""
packet = Packet(packet_type, data=data, optional=optional)
self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_SEND_MESSAGE, packet)

View file

@ -0,0 +1,87 @@
"""Representation of an EnOcean dongle."""
import glob
import logging
from os.path import basename, normpath
from enocean.communicators import SerialCommunicator
from enocean.protocol.packet import RadioPacket
import serial
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import SIGNAL_RECEIVE_MESSAGE, SIGNAL_SEND_MESSAGE
_LOGGER = logging.getLogger(__name__)
class EnOceanDongle:
"""Representation of an EnOcean dongle.
The dongle is responsible for receiving the ENOcean frames,
creating devices if needed, and dispatching messages to platforms.
"""
def __init__(self, hass, serial_path):
"""Initialize the EnOcean dongle."""
self._communicator = SerialCommunicator(
port=serial_path, callback=self.callback
)
self.serial_path = serial_path
self.identifier = basename(normpath(serial_path))
self.hass = hass
self.dispatcher_disconnect_handle = None
async def async_setup(self):
"""Finish the setup of the bridge and supported platforms."""
self._communicator.start()
self.dispatcher_disconnect_handle = async_dispatcher_connect(
self.hass, SIGNAL_SEND_MESSAGE, self._send_message_callback
)
def unload(self):
"""Disconnect callbacks established at init time."""
if self.dispatcher_disconnect_handle:
self.dispatcher_disconnect_handle()
self.dispatcher_disconnect_handle = None
def _send_message_callback(self, command):
"""Send a command through the EnOcean dongle."""
self._communicator.send(command)
def callback(self, packet):
"""Handle EnOcean device's callback.
This is the callback function called by python-enocan whenever there
is an incoming packet.
"""
if isinstance(packet, RadioPacket):
_LOGGER.debug("Received radio packet: %s", packet)
self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_RECEIVE_MESSAGE, packet)
def detect():
"""Return a list of candidate paths for USB ENOcean dongles.
This method is currently a bit simplistic, it may need to be
improved to support more configurations and OS.
"""
globs_to_test = ["/dev/tty*FTOA2PV*", "/dev/serial/by-id/*EnOcean*"]
found_paths = []
for current_glob in globs_to_test:
found_paths.extend(glob.glob(current_glob))
return found_paths
def validate_path(path: str):
"""Return True if the provided path points to a valid serial port, False otherwise."""
try:
# Creating the serial communicator will raise an exception
# if it cannot connect
SerialCommunicator(port=path)
return True
except serial.SerialException as exception:
_LOGGER.warning("Dongle path %s is invalid: %s", path, str(exception))
return False

View file

@ -4,7 +4,6 @@ import math
import voluptuous as vol
from homeassistant.components import enocean
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
PLATFORM_SCHEMA,
@ -14,6 +13,8 @@ from homeassistant.components.light import (
from homeassistant.const import CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv
from .device import EnOceanEntity
_LOGGER = logging.getLogger(__name__)
CONF_SENDER_ID = "sender_id"
@ -39,7 +40,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([EnOceanLight(sender_id, dev_id, dev_name)])
class EnOceanLight(enocean.EnOceanDevice, LightEntity):
class EnOceanLight(EnOceanEntity, LightEntity):
"""Representation of an EnOcean light source."""
def __init__(self, sender_id, dev_id, dev_name):

View file

@ -2,6 +2,11 @@
"domain": "enocean",
"name": "EnOcean",
"documentation": "https://www.home-assistant.io/integrations/enocean",
"requirements": ["enocean==0.50"],
"codeowners": ["@bdurrer"]
"requirements": [
"enocean==0.50"
],
"codeowners": [
"@bdurrer"
],
"config_flow": true
}

View file

@ -3,7 +3,6 @@ import logging
import voluptuous as vol
from homeassistant.components import enocean
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_DEVICE_CLASS,
@ -21,6 +20,8 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
from .device import EnOceanEntity
_LOGGER = logging.getLogger(__name__)
CONF_MAX_TEMP = "max_temp"
@ -62,7 +63,6 @@ SENSOR_TYPES = {
},
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
@ -105,7 +105,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([EnOceanWindowHandle(dev_id, dev_name)])
class EnOceanSensor(enocean.EnOceanDevice, RestoreEntity):
class EnOceanSensor(EnOceanEntity, RestoreEntity):
"""Representation of an EnOcean sensor device such as a power meter."""
def __init__(self, dev_id, dev_name, sensor_type):

View file

@ -0,0 +1,27 @@
{
"title": "EnOcean",
"config": {
"flow_title": "ENOcean setup",
"step": {
"detect": {
"title": "Select the path to you ENOcean dongle",
"data": {
"path": "USB dongle path"
}
},
"manual": {
"title": "Enter the path to you ENOcean dongle",
"data": {
"path": "USB dongle path"
}
}
},
"error": {
"invalid_dongle_path": "No valid dongle found for this path"
},
"abort": {
"invalid_dongle_path": "Invalid dongle path",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}

View file

@ -3,12 +3,13 @@ import logging
import voluptuous as vol
from homeassistant.components import enocean
from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
from .device import EnOceanEntity
_LOGGER = logging.getLogger(__name__)
CONF_CHANNEL = "channel"
@ -32,7 +33,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([EnOceanSwitch(dev_id, dev_name, channel)])
class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
class EnOceanSwitch(EnOceanEntity, ToggleEntity):
"""Representation of an EnOcean switch device."""
def __init__(self, dev_id, dev_name, channel):

View file

@ -0,0 +1,27 @@
{
"title": "EnOcean",
"config": {
"flow_title": "ENOcean setup",
"step": {
"detect": {
"title": "Select the path to you ENOcean dongle",
"data": {
"path": "USB dongle path"
}
},
"manual": {
"title": "Enter the path to you ENOcean dongle",
"data": {
"path": "USB dongle path"
}
}
},
"error": {
"invalid_dongle_path": "No valid dongle found for this path"
},
"abort": {
"invalid_dongle_path": "Invalid dongle path",
"single_instance_allowed": "An instance is already configured"
}
}
}

View file

@ -45,6 +45,7 @@ FLOWS = [
"elgato",
"elkm1",
"emulated_roku",
"enocean",
"esphome",
"flick_electric",
"flume",

View file

@ -265,6 +265,9 @@ emoji==0.5.4
# homeassistant.components.emulated_roku
emulated_roku==0.2.1
# homeassistant.components.enocean
enocean==0.50
# homeassistant.components.season
ephem==3.7.7.0

View file

@ -0,0 +1 @@
"""Tests of the EnOcean integration."""

View file

@ -0,0 +1,159 @@
"""Tests for EnOcean config flow."""
from homeassistant import data_entry_flow
from homeassistant.components.enocean.config_flow import EnOceanFlowHandler
from homeassistant.components.enocean.const import DOMAIN
from homeassistant.const import CONF_DEVICE
from tests.async_mock import Mock, patch
from tests.common import MockConfigEntry
DONGLE_VALIDATE_PATH_METHOD = "homeassistant.components.enocean.dongle.validate_path"
DONGLE_DETECT_METHOD = "homeassistant.components.enocean.dongle.detect"
async def test_user_flow_cannot_create_multiple_instances(hass):
"""Test that the user flow aborts if an instance is already configured."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_DEVICE: "/already/configured/path"}
)
entry.add_to_hass(hass)
with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=True)):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "single_instance_allowed"
async def test_user_flow_with_detected_dongle(hass):
"""Test the user flow with a detected ENOcean dongle."""
FAKE_DONGLE_PATH = "/fake/dongle"
with patch(DONGLE_DETECT_METHOD, Mock(return_value=[FAKE_DONGLE_PATH])):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "detect"
devices = result["data_schema"].schema.get("device").container
assert FAKE_DONGLE_PATH in devices
assert EnOceanFlowHandler.MANUAL_PATH_VALUE in devices
async def test_user_flow_with_no_detected_dongle(hass):
"""Test the user flow with a detected ENOcean dongle."""
with patch(DONGLE_DETECT_METHOD, Mock(return_value=[])):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "manual"
async def test_detection_flow_with_valid_path(hass):
"""Test the detection flow with a valid path selected."""
USER_PROVIDED_PATH = "/user/provided/path"
with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=True)):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "detect"}, data={CONF_DEVICE: USER_PROVIDED_PATH}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"][CONF_DEVICE] == USER_PROVIDED_PATH
async def test_detection_flow_with_custom_path(hass):
"""Test the detection flow with custom path selected."""
USER_PROVIDED_PATH = EnOceanFlowHandler.MANUAL_PATH_VALUE
FAKE_DONGLE_PATH = "/fake/dongle"
with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=True)):
with patch(DONGLE_DETECT_METHOD, Mock(return_value=[FAKE_DONGLE_PATH])):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "detect"},
data={CONF_DEVICE: USER_PROVIDED_PATH},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "manual"
async def test_detection_flow_with_invalid_path(hass):
"""Test the detection flow with an invalid path selected."""
USER_PROVIDED_PATH = "/invalid/path"
FAKE_DONGLE_PATH = "/fake/dongle"
with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=False)):
with patch(DONGLE_DETECT_METHOD, Mock(return_value=[FAKE_DONGLE_PATH])):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "detect"},
data={CONF_DEVICE: USER_PROVIDED_PATH},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "detect"
assert CONF_DEVICE in result["errors"]
async def test_manual_flow_with_valid_path(hass):
"""Test the manual flow with a valid path."""
USER_PROVIDED_PATH = "/user/provided/path"
with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=True)):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"][CONF_DEVICE] == USER_PROVIDED_PATH
async def test_manual_flow_with_invalid_path(hass):
"""Test the manual flow with an invalid path."""
USER_PROVIDED_PATH = "/user/provided/path"
with patch(
DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=False),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "manual"
assert CONF_DEVICE in result["errors"]
async def test_import_flow_with_valid_path(hass):
"""Test the import flow with a valid path."""
DATA_TO_IMPORT = {CONF_DEVICE: "/valid/path/to/import"}
with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=True)):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "import"}, data=DATA_TO_IMPORT
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"][CONF_DEVICE] == DATA_TO_IMPORT[CONF_DEVICE]
async def test_import_flow_with_invalid_path(hass):
"""Test the import flow with an invalid path."""
DATA_TO_IMPORT = {CONF_DEVICE: "/invalid/path/to/import"}
with patch(
DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=False),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "import"}, data=DATA_TO_IMPORT
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "invalid_dongle_path"