Config flow for elkm1 (#33297)
* Config flow for elkm1 * As entity ids can now be changed, the “alarm_control_panel” attribute “changed_by_entity_id” is now “changed_by_keypad” and will show the name of the Elk keypad instead of the entity id. * An auto configure mode has been introduced which avoids the need to setup the complex include and exclude filters. This functionality still exists when configuring from yaml for power users who want more control over which entities elkm1 generates. * restore _has_all_unique_prefixes * preserve legacy behavior of creating alarm_control_panels that have no linked keypads when auto_configure is False * unroll looppull/33315/head
parent
c629e7dc0e
commit
18a4829314
|
@ -99,6 +99,7 @@ homeassistant/components/edl21/* @mtdcr
|
|||
homeassistant/components/egardia/* @jeroenterheerdt
|
||||
homeassistant/components/eight_sleep/* @mezz64
|
||||
homeassistant/components/elgato/* @frenck
|
||||
homeassistant/components/elkm1/* @bdraco
|
||||
homeassistant/components/elv/* @majuss
|
||||
homeassistant/components/emby/* @mezz64
|
||||
homeassistant/components/emoncms/* @borpin
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "Elk-M1 Control",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to Elk-M1 Control",
|
||||
"description": "The address string must be in the form 'address[:port]' for 'secure' and 'non-secure'. Example: '192.168.1.1'. The port is optional and defaults to 2101 for 'non-secure' and 2601 for 'secure'. For the serial protocol, the address must be in the form 'tty[:baud]'. Example: '/dev/ttyS1'. The baud is optional and defaults to 115200.",
|
||||
"data": {
|
||||
"protocol": "Protocol",
|
||||
"address": "The IP address or domain or serial port if connecting via serial.",
|
||||
"username": "Username (secure only).",
|
||||
"password": "Password (secure only).",
|
||||
"prefix": "A unique prefix (leave blank if you only have one ElkM1).",
|
||||
"temperature_unit": "The temperature unit ElkM1 uses."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect, please try again",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "An ElkM1 with this prefix is already configured",
|
||||
"address_already_configured": "An ElkM1 with this address is already configured"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
"""Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels."""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
import async_timeout
|
||||
import elkm1_lib as elkm1
|
||||
from elkm1_lib.const import Max
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_EXCLUDE,
|
||||
CONF_HOST,
|
||||
|
@ -15,23 +17,29 @@ from homeassistant.const import (
|
|||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "elkm1"
|
||||
from .const import (
|
||||
CONF_AREA,
|
||||
CONF_AUTO_CONFIGURE,
|
||||
CONF_COUNTER,
|
||||
CONF_ENABLED,
|
||||
CONF_KEYPAD,
|
||||
CONF_OUTPUT,
|
||||
CONF_PLC,
|
||||
CONF_PREFIX,
|
||||
CONF_SETTING,
|
||||
CONF_TASK,
|
||||
CONF_THERMOSTAT,
|
||||
CONF_ZONE,
|
||||
DOMAIN,
|
||||
ELK_ELEMENTS,
|
||||
)
|
||||
|
||||
CONF_AREA = "area"
|
||||
CONF_COUNTER = "counter"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_KEYPAD = "keypad"
|
||||
CONF_OUTPUT = "output"
|
||||
CONF_PLC = "plc"
|
||||
CONF_SETTING = "setting"
|
||||
CONF_TASK = "task"
|
||||
CONF_THERMOSTAT = "thermostat"
|
||||
CONF_ZONE = "zone"
|
||||
CONF_PREFIX = "prefix"
|
||||
SYNC_TIMEOUT = 55
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -110,6 +118,7 @@ DEVICE_SCHEMA = vol.Schema(
|
|||
vol.Optional(CONF_PREFIX, default=""): vol.All(cv.string, vol.Lower),
|
||||
vol.Optional(CONF_USERNAME, default=""): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=""): cv.string,
|
||||
vol.Optional(CONF_AUTO_CONFIGURE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_TEMPERATURE_UNIT, default="F"): cv.temperature_unit,
|
||||
vol.Optional(CONF_AREA, default={}): DEVICE_SCHEMA_SUBDOMAIN,
|
||||
vol.Optional(CONF_COUNTER, default={}): DEVICE_SCHEMA_SUBDOMAIN,
|
||||
|
@ -132,34 +141,53 @@ CONFIG_SCHEMA = vol.Schema(
|
|||
|
||||
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
|
||||
"""Set up the Elk M1 platform."""
|
||||
devices = {}
|
||||
elk_datas = {}
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
_create_elk_services(hass)
|
||||
|
||||
configs = {
|
||||
CONF_AREA: Max.AREAS.value,
|
||||
CONF_COUNTER: Max.COUNTERS.value,
|
||||
CONF_KEYPAD: Max.KEYPADS.value,
|
||||
CONF_OUTPUT: Max.OUTPUTS.value,
|
||||
CONF_PLC: Max.LIGHTS.value,
|
||||
CONF_SETTING: Max.SETTINGS.value,
|
||||
CONF_TASK: Max.TASKS.value,
|
||||
CONF_THERMOSTAT: Max.THERMOSTATS.value,
|
||||
CONF_ZONE: Max.ZONES.value,
|
||||
}
|
||||
|
||||
def _included(ranges, set_to, values):
|
||||
for rng in ranges:
|
||||
if not rng[0] <= rng[1] <= len(values):
|
||||
raise vol.Invalid(f"Invalid range {rng}")
|
||||
values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1)
|
||||
if DOMAIN not in hass_config:
|
||||
return True
|
||||
|
||||
for index, conf in enumerate(hass_config[DOMAIN]):
|
||||
_LOGGER.debug("Setting up elkm1 #%d - %s", index, conf["host"])
|
||||
_LOGGER.debug("Importing elkm1 #%d - %s", index, conf[CONF_HOST])
|
||||
current_config_entry = _async_find_matching_config_entry(
|
||||
hass, conf[CONF_PREFIX]
|
||||
)
|
||||
if current_config_entry:
|
||||
# If they alter the yaml config we import the changes
|
||||
# since there currently is no practical way to do an options flow
|
||||
# with the large amount of include/exclude/enabled options that elkm1 has.
|
||||
hass.config_entries.async_update_entry(current_config_entry, data=conf)
|
||||
continue
|
||||
|
||||
config = {"temperature_unit": conf[CONF_TEMPERATURE_UNIT]}
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf,
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
def _async_find_matching_config_entry(hass, prefix):
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if entry.unique_id == prefix:
|
||||
return entry
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Elk-M1 Control from a config entry."""
|
||||
|
||||
conf = entry.data
|
||||
|
||||
_LOGGER.debug("Setting up elkm1 %s", conf["host"])
|
||||
|
||||
config = {"temperature_unit": conf[CONF_TEMPERATURE_UNIT]}
|
||||
|
||||
if not conf[CONF_AUTO_CONFIGURE]:
|
||||
# With elkm1-lib==0.7.16 and later auto configure is available
|
||||
config["panel"] = {"enabled": True, "included": [True]}
|
||||
|
||||
for item, max_ in configs.items():
|
||||
for item, max_ in ELK_ELEMENTS.items():
|
||||
config[item] = {
|
||||
"enabled": conf[item][CONF_ENABLED],
|
||||
"included": [not conf[item]["include"]] * max_,
|
||||
|
@ -171,39 +199,92 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
|
|||
_LOGGER.error("Config item: %s; %s", item, err)
|
||||
return False
|
||||
|
||||
prefix = conf[CONF_PREFIX]
|
||||
elk = elkm1.Elk(
|
||||
{
|
||||
"url": conf[CONF_HOST],
|
||||
"userid": conf[CONF_USERNAME],
|
||||
"password": conf[CONF_PASSWORD],
|
||||
}
|
||||
)
|
||||
elk.connect()
|
||||
|
||||
devices[prefix] = elk
|
||||
elk_datas[prefix] = {
|
||||
"elk": elk,
|
||||
"prefix": prefix,
|
||||
"config": config,
|
||||
"keypads": {},
|
||||
elk = elkm1.Elk(
|
||||
{
|
||||
"url": conf[CONF_HOST],
|
||||
"userid": conf[CONF_USERNAME],
|
||||
"password": conf[CONF_PASSWORD],
|
||||
}
|
||||
)
|
||||
elk.connect()
|
||||
|
||||
_create_elk_services(hass, devices)
|
||||
if not await async_wait_for_elk_to_sync(elk, SYNC_TIMEOUT):
|
||||
_LOGGER.error(
|
||||
"Timed out after %d seconds while trying to sync with ElkM1", SYNC_TIMEOUT,
|
||||
)
|
||||
elk.disconnect()
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
if elk.invalid_auth:
|
||||
_LOGGER.error("Authentication failed for ElkM1")
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
"elk": elk,
|
||||
"prefix": conf[CONF_PREFIX],
|
||||
"auto_configure": conf[CONF_AUTO_CONFIGURE],
|
||||
"config": config,
|
||||
"keypads": {},
|
||||
}
|
||||
|
||||
hass.data[DOMAIN] = elk_datas
|
||||
for component in SUPPORTED_DOMAINS:
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(hass, component, DOMAIN, {}, hass_config)
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _create_elk_services(hass, elks):
|
||||
def _included(ranges, set_to, values):
|
||||
for rng in ranges:
|
||||
if not rng[0] <= rng[1] <= len(values):
|
||||
raise vol.Invalid(f"Invalid range {rng}")
|
||||
values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1)
|
||||
|
||||
|
||||
def _find_elk_by_prefix(hass, prefix):
|
||||
"""Search all config entries for a given prefix."""
|
||||
for entry_id in hass.data[DOMAIN]:
|
||||
if hass.data[DOMAIN][entry_id]["prefix"] == prefix:
|
||||
return hass.data[DOMAIN][entry_id]["elk"]
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in SUPPORTED_DOMAINS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# disconnect cleanly
|
||||
hass.data[DOMAIN][entry.entry_id]["elk"].disconnect()
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_wait_for_elk_to_sync(elk, timeout):
|
||||
"""Wait until the elk system has finished sync."""
|
||||
try:
|
||||
with async_timeout.timeout(timeout):
|
||||
await elk.sync_complete()
|
||||
return True
|
||||
except asyncio.TimeoutError:
|
||||
elk.disconnect()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _create_elk_services(hass):
|
||||
def _speak_word_service(service):
|
||||
prefix = service.data["prefix"]
|
||||
elk = elks.get(prefix)
|
||||
elk = _find_elk_by_prefix(hass, prefix)
|
||||
if elk is None:
|
||||
_LOGGER.error("No elk m1 with prefix for speak_word: '%s'", prefix)
|
||||
return
|
||||
|
@ -211,7 +292,7 @@ def _create_elk_services(hass, elks):
|
|||
|
||||
def _speak_phrase_service(service):
|
||||
prefix = service.data["prefix"]
|
||||
elk = elks.get(prefix)
|
||||
elk = _find_elk_by_prefix(hass, prefix)
|
||||
if elk is None:
|
||||
_LOGGER.error("No elk m1 with prefix for speak_phrase: '%s'", prefix)
|
||||
return
|
||||
|
@ -227,12 +308,23 @@ def _create_elk_services(hass, elks):
|
|||
|
||||
def create_elk_entities(elk_data, elk_elements, element_type, class_, entities):
|
||||
"""Create the ElkM1 devices of a particular class."""
|
||||
if elk_data["config"][element_type]["enabled"]:
|
||||
elk = elk_data["elk"]
|
||||
_LOGGER.debug("Creating elk entities for %s", elk)
|
||||
for element in elk_elements:
|
||||
if elk_data["config"][element_type]["included"][element.index]:
|
||||
entities.append(class_(element, elk, elk_data))
|
||||
auto_configure = elk_data["auto_configure"]
|
||||
|
||||
if not auto_configure and not elk_data["config"][element_type]["enabled"]:
|
||||
return
|
||||
|
||||
elk = elk_data["elk"]
|
||||
_LOGGER.debug("Creating elk entities for %s", elk)
|
||||
|
||||
for element in elk_elements:
|
||||
if auto_configure:
|
||||
if not element.configured:
|
||||
continue
|
||||
# Only check the included list if auto configure is not
|
||||
elif not elk_data["config"][element_type]["included"][element.index]:
|
||||
continue
|
||||
|
||||
entities.append(class_(element, elk, elk_data))
|
||||
return entities
|
||||
|
||||
|
||||
|
@ -297,9 +389,34 @@ class ElkEntity(Entity):
|
|||
def _element_callback(self, element, changeset):
|
||||
"""Handle callback from an Elk element that has changed."""
|
||||
self._element_changed(element, changeset)
|
||||
self.async_schedule_update_ha_state(True)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callback for ElkM1 changes and update entity state."""
|
||||
self._element.add_callback(self._element_callback)
|
||||
self._element_callback(self._element, {})
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Device info connecting via the ElkM1 system."""
|
||||
return {
|
||||
"via_device": (DOMAIN, f"{self._prefix}_system"),
|
||||
}
|
||||
|
||||
|
||||
class ElkAttachedEntity(ElkEntity):
|
||||
"""An elk entity that is attached to the elk system."""
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Device info for the underlying ElkM1 system."""
|
||||
device_name = "ElkM1"
|
||||
if self._prefix:
|
||||
device_name += f" {self._prefix}"
|
||||
return {
|
||||
"name": device_name,
|
||||
"identifiers": {(DOMAIN, f"{self._prefix}_system")},
|
||||
"sw_version": self._elk.panel.elkm1_version,
|
||||
"manufacturer": "ELK Products, Inc.",
|
||||
"model": "M1",
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
"""Each ElkM1 area will be created as a separate alarm_control_panel."""
|
||||
import logging
|
||||
|
||||
from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -22,24 +24,18 @@ from homeassistant.const import (
|
|||
STATE_ALARM_PENDING,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.helpers import entity_platform
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
SERVICE_ALARM_ARM_HOME_INSTANT,
|
||||
SERVICE_ALARM_ARM_NIGHT_INSTANT,
|
||||
SERVICE_ALARM_ARM_VACATION,
|
||||
SERVICE_ALARM_DISPLAY_MESSAGE,
|
||||
ElkEntity,
|
||||
ElkAttachedEntity,
|
||||
create_elk_entities,
|
||||
)
|
||||
|
||||
SIGNAL_ARM_ENTITY = "elkm1_arm"
|
||||
SIGNAL_DISPLAY_MESSAGE = "elkm1_display_message"
|
||||
from .const import DOMAIN
|
||||
|
||||
ELK_ALARM_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
|
@ -61,69 +57,57 @@ DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema(
|
|||
}
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the ElkM1 alarm platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
elk_datas = hass.data[DOMAIN]
|
||||
elk_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entities = []
|
||||
for elk_data in elk_datas.values():
|
||||
elk = elk_data["elk"]
|
||||
entities = create_elk_entities(elk_data, elk.areas, "area", ElkArea, entities)
|
||||
|
||||
elk = elk_data["elk"]
|
||||
areas_with_keypad = set()
|
||||
for keypad in elk.keypads:
|
||||
areas_with_keypad.add(keypad.area)
|
||||
|
||||
areas = []
|
||||
for area in elk.areas:
|
||||
if area.index in areas_with_keypad or elk_data["auto_configure"] is False:
|
||||
areas.append(area)
|
||||
create_elk_entities(elk_data, areas, "area", ElkArea, entities)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
def _dispatch(signal, entity_ids, *args):
|
||||
for entity_id in entity_ids:
|
||||
async_dispatcher_send(hass, f"{signal}_{entity_id}", *args)
|
||||
platform = entity_platform.current_platform.get()
|
||||
|
||||
def _arm_service(service):
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID, [])
|
||||
arm_level = _arm_services().get(service.service)
|
||||
args = (arm_level, service.data.get(ATTR_CODE))
|
||||
_dispatch(SIGNAL_ARM_ENTITY, entity_ids, *args)
|
||||
|
||||
for service in _arm_services():
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA
|
||||
)
|
||||
|
||||
def _display_message_service(service):
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID, [])
|
||||
data = service.data
|
||||
args = (
|
||||
data["clear"],
|
||||
data["beep"],
|
||||
data["timeout"],
|
||||
data["line1"],
|
||||
data["line2"],
|
||||
)
|
||||
_dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_VACATION,
|
||||
ELK_ALARM_SERVICE_SCHEMA,
|
||||
"async_alarm_arm_vacation",
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_HOME_INSTANT,
|
||||
ELK_ALARM_SERVICE_SCHEMA,
|
||||
"async_alarm_arm_home_instant",
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_NIGHT_INSTANT,
|
||||
ELK_ALARM_SERVICE_SCHEMA,
|
||||
"async_alarm_arm_night_instant",
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ALARM_DISPLAY_MESSAGE,
|
||||
_display_message_service,
|
||||
DISPLAY_MESSAGE_SERVICE_SCHEMA,
|
||||
"async_display_message",
|
||||
)
|
||||
|
||||
|
||||
def _arm_services():
|
||||
return {
|
||||
SERVICE_ALARM_ARM_VACATION: ArmLevel.ARMED_VACATION.value,
|
||||
SERVICE_ALARM_ARM_HOME_INSTANT: ArmLevel.ARMED_STAY_INSTANT.value,
|
||||
SERVICE_ALARM_ARM_NIGHT_INSTANT: ArmLevel.ARMED_NIGHT_INSTANT.value,
|
||||
}
|
||||
|
||||
|
||||
class ElkArea(ElkEntity, AlarmControlPanel):
|
||||
class ElkArea(ElkAttachedEntity, AlarmControlPanel):
|
||||
"""Representation of an Area / Partition within the ElkM1 alarm panel."""
|
||||
|
||||
def __init__(self, element, elk, elk_data):
|
||||
"""Initialize Area as Alarm Control Panel."""
|
||||
super().__init__(element, elk, elk_data)
|
||||
self._changed_by_entity_id = ""
|
||||
self._changed_by_keypad = None
|
||||
self._state = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
|
@ -131,23 +115,13 @@ class ElkArea(ElkEntity, AlarmControlPanel):
|
|||
await super().async_added_to_hass()
|
||||
for keypad in self._elk.keypads:
|
||||
keypad.add_callback(self._watch_keypad)
|
||||
async_dispatcher_connect(
|
||||
self.hass, f"{SIGNAL_ARM_ENTITY}_{self.entity_id}", self._arm_service
|
||||
)
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{SIGNAL_DISPLAY_MESSAGE}_{self.entity_id}",
|
||||
self._display_message,
|
||||
)
|
||||
|
||||
def _watch_keypad(self, keypad, changeset):
|
||||
if keypad.area != self._element.index:
|
||||
return
|
||||
if changeset.get("last_user") is not None:
|
||||
self._changed_by_entity_id = self.hass.data[DOMAIN][self._prefix][
|
||||
"keypads"
|
||||
].get(keypad.index, "")
|
||||
self.async_schedule_update_ha_state(True)
|
||||
self._changed_by_keypad = keypad.name
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
|
@ -178,7 +152,7 @@ class ElkArea(ElkEntity, AlarmControlPanel):
|
|||
attrs["arm_up_state"] = ArmUpState(elmt.arm_up_state).name.lower()
|
||||
if elmt.alarm_state is not None:
|
||||
attrs["alarm_state"] = AlarmState(elmt.alarm_state).name.lower()
|
||||
attrs["changed_by_entity_id"] = self._changed_by_entity_id
|
||||
attrs["changed_by_keypad"] = self._changed_by_keypad
|
||||
return attrs
|
||||
|
||||
def _element_changed(self, element, changeset):
|
||||
|
@ -225,9 +199,18 @@ class ElkArea(ElkEntity, AlarmControlPanel):
|
|||
"""Send arm night command."""
|
||||
self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code))
|
||||
|
||||
async def _arm_service(self, arm_level, code):
|
||||
self._element.arm(arm_level, code)
|
||||
async def async_alarm_arm_home_instant(self, code=None):
|
||||
"""Send arm stay instant command."""
|
||||
self._element.arm(ArmLevel.ARMED_STAY_INSTANT.value, int(code))
|
||||
|
||||
async def _display_message(self, clear, beep, timeout, line1, line2):
|
||||
async def async_alarm_arm_night_instant(self, code=None):
|
||||
"""Send arm night instant command."""
|
||||
self._element.arm(ArmLevel.ARMED_NIGHT_INSTANT.value, int(code))
|
||||
|
||||
async def async_alarm_arm_vacation(self, code=None):
|
||||
"""Send arm vacation command."""
|
||||
self._element.arm(ArmLevel.ARMED_VACATION.value, int(code))
|
||||
|
||||
async def async_display_message(self, clear, beep, timeout, line1, line2):
|
||||
"""Display a message on all keypads for the area."""
|
||||
self._element.display_message(clear, beep, timeout, line1, line2)
|
||||
|
|
|
@ -14,9 +14,10 @@ from homeassistant.components.climate.const import (
|
|||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||
)
|
||||
from homeassistant.const import PRECISION_WHOLE, STATE_ON
|
||||
from homeassistant.const import PRECISION_WHOLE, STATE_ON, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
|
||||
from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities
|
||||
from . import ElkEntity, create_elk_entities
|
||||
from .const import DOMAIN
|
||||
|
||||
SUPPORT_HVAC = [
|
||||
HVAC_MODE_OFF,
|
||||
|
@ -27,18 +28,14 @@ SUPPORT_HVAC = [
|
|||
]
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Create the Elk-M1 thermostat platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
elk_datas = hass.data[ELK_DOMAIN]
|
||||
elk_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entities = []
|
||||
for elk_data in elk_datas.values():
|
||||
elk = elk_data["elk"]
|
||||
entities = create_elk_entities(
|
||||
elk_data, elk.thermostats, "thermostat", ElkThermostat, entities
|
||||
)
|
||||
elk = elk_data["elk"]
|
||||
create_elk_entities(
|
||||
elk_data, elk.thermostats, "thermostat", ElkThermostat, entities
|
||||
)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
|
@ -58,7 +55,7 @@ class ElkThermostat(ElkEntity, ClimateDevice):
|
|||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the temperature unit."""
|
||||
return self._temperature_unit
|
||||
return TEMP_FAHRENHEIT if self._temperature_unit == "F" else TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
"""Config flow for Elk-M1 Control integration."""
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import elkm1_lib as elkm1
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, exceptions
|
||||
from homeassistant.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PROTOCOL,
|
||||
CONF_TEMPERATURE_UNIT,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import async_wait_for_elk_to_sync
|
||||
from .const import CONF_AUTO_CONFIGURE, CONF_PREFIX
|
||||
from .const import DOMAIN # pylint:disable=unused-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PROTOCOL_MAP = {"secure": "elks://", "non-secure": "elk://", "serial": "serial://"}
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PROTOCOL, default="secure"): vol.In(
|
||||
["secure", "non-secure", "serial"]
|
||||
),
|
||||
vol.Required(CONF_ADDRESS): str,
|
||||
vol.Optional(CONF_USERNAME, default=""): str,
|
||||
vol.Optional(CONF_PASSWORD, default=""): str,
|
||||
vol.Optional(CONF_PREFIX, default=""): str,
|
||||
vol.Optional(CONF_TEMPERATURE_UNIT, default="F"): vol.In(["F", "C"]),
|
||||
}
|
||||
)
|
||||
|
||||
VALIDATE_TIMEOUT = 35
|
||||
|
||||
|
||||
async def validate_input(data):
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
userid = data.get(CONF_USERNAME)
|
||||
password = data.get(CONF_PASSWORD)
|
||||
|
||||
prefix = data[CONF_PREFIX]
|
||||
url = _make_url_from_data(data)
|
||||
requires_password = url.startswith("elks://")
|
||||
|
||||
if requires_password and (not userid or not password):
|
||||
raise InvalidAuth
|
||||
|
||||
elk = elkm1.Elk(
|
||||
{"url": url, "userid": userid, "password": password, "element_list": ["panel"]}
|
||||
)
|
||||
elk.connect()
|
||||
|
||||
timed_out = False
|
||||
if not await async_wait_for_elk_to_sync(elk, VALIDATE_TIMEOUT):
|
||||
_LOGGER.error(
|
||||
"Timed out after %d seconds while trying to sync with elkm1",
|
||||
VALIDATE_TIMEOUT,
|
||||
)
|
||||
timed_out = True
|
||||
|
||||
elk.disconnect()
|
||||
|
||||
if timed_out:
|
||||
raise CannotConnect
|
||||
if elk.invalid_auth:
|
||||
raise InvalidAuth
|
||||
|
||||
device_name = data[CONF_PREFIX] if data[CONF_PREFIX] else "ElkM1"
|
||||
# Return info that you want to store in the config entry.
|
||||
return {"title": device_name, CONF_HOST: url, CONF_PREFIX: slugify(prefix)}
|
||||
|
||||
|
||||
def _make_url_from_data(data):
|
||||
host = data.get(CONF_HOST)
|
||||
if host:
|
||||
return host
|
||||
|
||||
protocol = PROTOCOL_MAP[data[CONF_PROTOCOL]]
|
||||
address = data[CONF_ADDRESS]
|
||||
return f"{protocol}{address}"
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Elk-M1 Control."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the elkm1 config flow."""
|
||||
self.importing = False
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
if self._url_already_configured(_make_url_from_data(user_input)):
|
||||
return self.async_abort(reason="address_already_configured")
|
||||
|
||||
try:
|
||||
info = await validate_input(user_input)
|
||||
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if "base" not in errors:
|
||||
await self.async_set_unique_id(user_input[CONF_PREFIX])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
if self.importing:
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=info["title"],
|
||||
data={
|
||||
CONF_HOST: info[CONF_HOST],
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
CONF_AUTO_CONFIGURE: True,
|
||||
CONF_TEMPERATURE_UNIT: user_input[CONF_TEMPERATURE_UNIT],
|
||||
CONF_PREFIX: info[CONF_PREFIX],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, user_input):
|
||||
"""Handle import."""
|
||||
self.importing = True
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
def _url_already_configured(self, url):
|
||||
"""See if we already have a elkm1 matching user input configured."""
|
||||
existing_hosts = {
|
||||
urlparse(entry.data[CONF_HOST]).hostname
|
||||
for entry in self._async_current_entries()
|
||||
}
|
||||
return urlparse(url).hostname in existing_hosts
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(exceptions.HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
|
@ -0,0 +1,31 @@
|
|||
"""Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels."""
|
||||
|
||||
from elkm1_lib.const import Max
|
||||
|
||||
DOMAIN = "elkm1"
|
||||
|
||||
CONF_AUTO_CONFIGURE = "auto_configure"
|
||||
CONF_AREA = "area"
|
||||
CONF_COUNTER = "counter"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_KEYPAD = "keypad"
|
||||
CONF_OUTPUT = "output"
|
||||
CONF_PLC = "plc"
|
||||
CONF_SETTING = "setting"
|
||||
CONF_TASK = "task"
|
||||
CONF_THERMOSTAT = "thermostat"
|
||||
CONF_ZONE = "zone"
|
||||
CONF_PREFIX = "prefix"
|
||||
|
||||
|
||||
ELK_ELEMENTS = {
|
||||
CONF_AREA: Max.AREAS.value,
|
||||
CONF_COUNTER: Max.COUNTERS.value,
|
||||
CONF_KEYPAD: Max.KEYPADS.value,
|
||||
CONF_OUTPUT: Max.OUTPUTS.value,
|
||||
CONF_PLC: Max.LIGHTS.value,
|
||||
CONF_SETTING: Max.SETTINGS.value,
|
||||
CONF_TASK: Max.TASKS.value,
|
||||
CONF_THERMOSTAT: Max.THERMOSTATS.value,
|
||||
CONF_ZONE: Max.ZONES.value,
|
||||
}
|
|
@ -1,18 +1,17 @@
|
|||
"""Support for control of ElkM1 lighting (X10, UPB, etc)."""
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light
|
||||
|
||||
from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities
|
||||
from . import ElkEntity, create_elk_entities
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Elk light platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
elk_datas = hass.data[ELK_DOMAIN]
|
||||
elk_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entities = []
|
||||
for elk_data in elk_datas.values():
|
||||
elk = elk_data["elk"]
|
||||
create_elk_entities(elk_data, elk.lights, "plc", ElkLight, entities)
|
||||
elk = elk_data["elk"]
|
||||
create_elk_entities(elk_data, elk.lights, "plc", ElkLight, entities)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,12 @@
|
|||
"domain": "elkm1",
|
||||
"name": "Elk-M1 Control",
|
||||
"documentation": "https://www.home-assistant.io/integrations/elkm1",
|
||||
"requirements": ["elkm1-lib==0.7.15"],
|
||||
"requirements": [
|
||||
"elkm1-lib==0.7.17"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
"codeowners": [
|
||||
"@bdraco"
|
||||
],
|
||||
"config_flow": true
|
||||
}
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
"""Support for control of ElkM1 tasks ("macros")."""
|
||||
from homeassistant.components.scene import Scene
|
||||
|
||||
from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities
|
||||
from . import ElkAttachedEntity, create_elk_entities
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Create the Elk-M1 scene platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
elk_datas = hass.data[ELK_DOMAIN]
|
||||
elk_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entities = []
|
||||
for elk_data in elk_datas.values():
|
||||
elk = elk_data["elk"]
|
||||
entities = create_elk_entities(elk_data, elk.tasks, "task", ElkTask, entities)
|
||||
elk = elk_data["elk"]
|
||||
create_elk_entities(elk_data, elk.tasks, "task", ElkTask, entities)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class ElkTask(ElkEntity, Scene):
|
||||
class ElkTask(ElkAttachedEntity, Scene):
|
||||
"""Elk-M1 task as scene."""
|
||||
|
||||
async def async_activate(self):
|
||||
|
|
|
@ -7,31 +7,22 @@ from elkm1_lib.const import (
|
|||
)
|
||||
from elkm1_lib.util import pretty_const, username
|
||||
|
||||
from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities
|
||||
from . import ElkAttachedEntity, create_elk_entities
|
||||
from .const import DOMAIN
|
||||
|
||||
UNDEFINED_TEMPATURE = -40
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Create the Elk-M1 sensor platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
elk_datas = hass.data[ELK_DOMAIN]
|
||||
elk_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entities = []
|
||||
for elk_data in elk_datas.values():
|
||||
elk = elk_data["elk"]
|
||||
entities = create_elk_entities(
|
||||
elk_data, elk.counters, "counter", ElkCounter, entities
|
||||
)
|
||||
entities = create_elk_entities(
|
||||
elk_data, elk.keypads, "keypad", ElkKeypad, entities
|
||||
)
|
||||
entities = create_elk_entities(
|
||||
elk_data, [elk.panel], "panel", ElkPanel, entities
|
||||
)
|
||||
entities = create_elk_entities(
|
||||
elk_data, elk.settings, "setting", ElkSetting, entities
|
||||
)
|
||||
entities = create_elk_entities(elk_data, elk.zones, "zone", ElkZone, entities)
|
||||
elk = elk_data["elk"]
|
||||
create_elk_entities(elk_data, elk.counters, "counter", ElkCounter, entities)
|
||||
create_elk_entities(elk_data, elk.keypads, "keypad", ElkKeypad, entities)
|
||||
create_elk_entities(elk_data, [elk.panel], "panel", ElkPanel, entities)
|
||||
create_elk_entities(elk_data, elk.settings, "setting", ElkSetting, entities)
|
||||
create_elk_entities(elk_data, elk.zones, "zone", ElkZone, entities)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
|
@ -40,7 +31,7 @@ def temperature_to_state(temperature, undefined_temperature):
|
|||
return temperature if temperature > undefined_temperature else None
|
||||
|
||||
|
||||
class ElkSensor(ElkEntity):
|
||||
class ElkSensor(ElkAttachedEntity):
|
||||
"""Base representation of Elk-M1 sensor."""
|
||||
|
||||
def __init__(self, element, elk, elk_data):
|
||||
|
@ -89,7 +80,7 @@ class ElkKeypad(ElkSensor):
|
|||
"""Attributes of the sensor."""
|
||||
attrs = self.initial_attrs()
|
||||
attrs["area"] = self._element.area + 1
|
||||
attrs["temperature"] = self._element.temperature
|
||||
attrs["temperature"] = self._state
|
||||
attrs["last_user_time"] = self._element.last_user_time.isoformat()
|
||||
attrs["last_user"] = self._element.last_user + 1
|
||||
attrs["code"] = self._element.code
|
||||
|
@ -98,14 +89,9 @@ class ElkKeypad(ElkSensor):
|
|||
return attrs
|
||||
|
||||
def _element_changed(self, element, changeset):
|
||||
self._state = temperature_to_state(self._element.temperature, -40)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callback for ElkM1 changes and update entity state."""
|
||||
await super().async_added_to_hass()
|
||||
elk_datas = self.hass.data[ELK_DOMAIN]
|
||||
for elk_data in elk_datas.values():
|
||||
elk_data["keypads"][self._element.index] = self.entity_id
|
||||
self._state = temperature_to_state(
|
||||
self._element.temperature, UNDEFINED_TEMPATURE
|
||||
)
|
||||
|
||||
|
||||
class ElkPanel(ElkSensor):
|
||||
|
@ -214,7 +200,9 @@ class ElkZone(ElkSensor):
|
|||
|
||||
def _element_changed(self, element, changeset):
|
||||
if self._element.definition == ZoneType.TEMPERATURE.value:
|
||||
self._state = temperature_to_state(self._element.temperature, -60)
|
||||
self._state = temperature_to_state(
|
||||
self._element.temperature, UNDEFINED_TEMPATURE
|
||||
)
|
||||
elif self._element.definition == ZoneType.ANALOG_ZONE.value:
|
||||
self._state = self._element.voltage
|
||||
else:
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "Elk-M1 Control",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to Elk-M1 Control",
|
||||
"description": "The address string must be in the form 'address[:port]' for 'secure' and 'non-secure'. Example: '192.168.1.1'. The port is optional and defaults to 2101 for 'non-secure' and 2601 for 'secure'. For the serial protocol, the address must be in the form 'tty[:baud]'. Example: '/dev/ttyS1'. The baud is optional and defaults to 115200.",
|
||||
"data": {
|
||||
"protocol": "Protocol",
|
||||
"address": "The IP address or domain or serial port if connecting via serial.",
|
||||
"username": "Username (secure only).",
|
||||
"password": "Password (secure only).",
|
||||
"prefix": "A unique prefix (leave blank if you only have one ElkM1).",
|
||||
"temperature_unit": "The temperature unit ElkM1 uses."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect, please try again",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "An ElkM1 with this prefix is already configured",
|
||||
"address_already_configured": "An ElkM1 with this address is already configured"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +1,20 @@
|
|||
"""Support for control of ElkM1 outputs (relays)."""
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
|
||||
from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities
|
||||
from . import ElkAttachedEntity, create_elk_entities
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Create the Elk-M1 switch platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
elk_datas = hass.data[ELK_DOMAIN]
|
||||
elk_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entities = []
|
||||
for elk_data in elk_datas.values():
|
||||
elk = elk_data["elk"]
|
||||
entities = create_elk_entities(
|
||||
elk_data, elk.outputs, "output", ElkOutput, entities
|
||||
)
|
||||
elk = elk_data["elk"]
|
||||
create_elk_entities(elk_data, elk.outputs, "output", ElkOutput, entities)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class ElkOutput(ElkEntity, SwitchDevice):
|
||||
class ElkOutput(ElkAttachedEntity, SwitchDevice):
|
||||
"""Elk output as switch."""
|
||||
|
||||
@property
|
||||
|
|
|
@ -28,6 +28,7 @@ FLOWS = [
|
|||
"dynalite",
|
||||
"ecobee",
|
||||
"elgato",
|
||||
"elkm1",
|
||||
"emulated_roku",
|
||||
"esphome",
|
||||
"freebox",
|
||||
|
|
|
@ -495,7 +495,7 @@ elgato==0.2.0
|
|||
eliqonline==1.2.2
|
||||
|
||||
# homeassistant.components.elkm1
|
||||
elkm1-lib==0.7.15
|
||||
elkm1-lib==0.7.17
|
||||
|
||||
# homeassistant.components.emulated_roku
|
||||
emulated_roku==0.2.1
|
||||
|
|
|
@ -198,6 +198,9 @@ eebrightbox==0.0.4
|
|||
# homeassistant.components.elgato
|
||||
elgato==0.2.0
|
||||
|
||||
# homeassistant.components.elkm1
|
||||
elkm1-lib==0.7.17
|
||||
|
||||
# homeassistant.components.emulated_roku
|
||||
emulated_roku==0.2.1
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Elk-M1 Control integration."""
|
|
@ -0,0 +1,269 @@
|
|||
"""Test the Elk-M1 Control config flow."""
|
||||
|
||||
from asynctest import CoroutineMock, MagicMock, PropertyMock, patch
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
from homeassistant.components.elkm1.const import DOMAIN
|
||||
|
||||
|
||||
def mock_elk(invalid_auth=None, sync_complete=None):
|
||||
"""Mock m1lib Elk."""
|
||||
mocked_elk = MagicMock()
|
||||
type(mocked_elk).invalid_auth = PropertyMock(return_value=invalid_auth)
|
||||
type(mocked_elk).sync_complete = CoroutineMock()
|
||||
return mocked_elk
|
||||
|
||||
|
||||
async def test_form_user_with_secure_elk(hass):
|
||||
"""Test we can setup a secure elk."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk,
|
||||
), patch(
|
||||
"homeassistant.components.elkm1.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry", return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"protocol": "secure",
|
||||
"address": "1.2.3.4",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"temperature_unit": "F",
|
||||
"prefix": "",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "ElkM1"
|
||||
assert result2["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elks://1.2.3.4",
|
||||
"password": "test-password",
|
||||
"prefix": "",
|
||||
"temperature_unit": "F",
|
||||
"username": "test-username",
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_user_with_non_secure_elk(hass):
|
||||
"""Test we can setup a non-secure elk."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk,
|
||||
), patch(
|
||||
"homeassistant.components.elkm1.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry", return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"protocol": "non-secure",
|
||||
"address": "1.2.3.4",
|
||||
"temperature_unit": "F",
|
||||
"prefix": "guest_house",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "guest_house"
|
||||
assert result2["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elk://1.2.3.4",
|
||||
"prefix": "guest_house",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"temperature_unit": "F",
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_user_with_serial_elk(hass):
|
||||
"""Test we can setup a serial elk."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk,
|
||||
), patch(
|
||||
"homeassistant.components.elkm1.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry", return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"protocol": "serial",
|
||||
"address": "/dev/ttyS0:115200",
|
||||
"temperature_unit": "F",
|
||||
"prefix": "",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "ElkM1"
|
||||
assert result2["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "serial:///dev/ttyS0:115200",
|
||||
"prefix": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"temperature_unit": "F",
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass):
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk,
|
||||
), patch(
|
||||
"homeassistant.components.elkm1.config_flow.async_wait_for_elk_to_sync",
|
||||
return_value=False,
|
||||
): # async_wait_for_elk_to_sync is being patched to avoid making the test wait 45s
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"protocol": "secure",
|
||||
"address": "1.2.3.4",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"temperature_unit": "F",
|
||||
"prefix": "",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_invalid_auth(hass):
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"protocol": "secure",
|
||||
"address": "1.2.3.4",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"temperature_unit": "F",
|
||||
"prefix": "",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_import(hass):
|
||||
"""Test we get the form with import source."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False)
|
||||
with patch(
|
||||
"homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk,
|
||||
), patch(
|
||||
"homeassistant.components.elkm1.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry", return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
"host": "elks://1.2.3.4",
|
||||
"username": "friend",
|
||||
"password": "love",
|
||||
"temperature_unit": "C",
|
||||
"auto_configure": False,
|
||||
"keypad": {
|
||||
"enabled": True,
|
||||
"exclude": [],
|
||||
"include": [[1, 1], [2, 2], [3, 3]],
|
||||
},
|
||||
"output": {"enabled": False, "exclude": [], "include": []},
|
||||
"counter": {"enabled": False, "exclude": [], "include": []},
|
||||
"plc": {"enabled": False, "exclude": [], "include": []},
|
||||
"prefix": "ohana",
|
||||
"setting": {"enabled": False, "exclude": [], "include": []},
|
||||
"area": {"enabled": False, "exclude": [], "include": []},
|
||||
"task": {"enabled": False, "exclude": [], "include": []},
|
||||
"thermostat": {"enabled": False, "exclude": [], "include": []},
|
||||
"zone": {
|
||||
"enabled": True,
|
||||
"exclude": [[15, 15], [28, 208]],
|
||||
"include": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == "ohana"
|
||||
|
||||
assert result["data"] == {
|
||||
"auto_configure": False,
|
||||
"host": "elks://1.2.3.4",
|
||||
"keypad": {"enabled": True, "exclude": [], "include": [[1, 1], [2, 2], [3, 3]]},
|
||||
"output": {"enabled": False, "exclude": [], "include": []},
|
||||
"password": "love",
|
||||
"plc": {"enabled": False, "exclude": [], "include": []},
|
||||
"prefix": "ohana",
|
||||
"setting": {"enabled": False, "exclude": [], "include": []},
|
||||
"area": {"enabled": False, "exclude": [], "include": []},
|
||||
"counter": {"enabled": False, "exclude": [], "include": []},
|
||||
"task": {"enabled": False, "exclude": [], "include": []},
|
||||
"temperature_unit": "C",
|
||||
"thermostat": {"enabled": False, "exclude": [], "include": []},
|
||||
"username": "friend",
|
||||
"zone": {"enabled": True, "exclude": [[15, 15], [28, 208]], "include": []},
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
Loading…
Reference in New Issue