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:
parent
872140123d
commit
af6a4bb6cf
17 changed files with 514 additions and 82 deletions
|
@ -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/*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
94
homeassistant/components/enocean/config_flow.py
Normal file
94
homeassistant/components/enocean/config_flow.py
Normal 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)
|
15
homeassistant/components/enocean/const.py
Normal file
15
homeassistant/components/enocean/const.py
Normal 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"]
|
39
homeassistant/components/enocean/device.py
Normal file
39
homeassistant/components/enocean/device.py
Normal 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)
|
87
homeassistant/components/enocean/dongle.py
Normal file
87
homeassistant/components/enocean/dongle.py
Normal 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
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
27
homeassistant/components/enocean/strings.json
Normal file
27
homeassistant/components/enocean/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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):
|
||||
|
|
27
homeassistant/components/enocean/translations/en.json
Normal file
27
homeassistant/components/enocean/translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,6 +45,7 @@ FLOWS = [
|
|||
"elgato",
|
||||
"elkm1",
|
||||
"emulated_roku",
|
||||
"enocean",
|
||||
"esphome",
|
||||
"flick_electric",
|
||||
"flume",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
1
tests/components/enocean/__init__.py
Normal file
1
tests/components/enocean/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests of the EnOcean integration."""
|
159
tests/components/enocean/test_config_flow.py
Normal file
159
tests/components/enocean/test_config_flow.py
Normal 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"
|
Loading…
Add table
Add a link
Reference in a new issue