From 18a4829314341de38238728beae07970c3f42858 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Fri, 27 Mar 2020 15:38:35 -0500
Subject: [PATCH] Config flow for elkm1 (#33297)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* 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 loop
---
 CODEOWNERS                                    |   1 +
 .../components/elkm1/.translations/en.json    |  28 ++
 homeassistant/components/elkm1/__init__.py    | 249 +++++++++++-----
 .../components/elkm1/alarm_control_panel.py   | 129 ++++-----
 homeassistant/components/elkm1/climate.py     |  23 +-
 homeassistant/components/elkm1/config_flow.py | 164 +++++++++++
 homeassistant/components/elkm1/const.py       |  31 ++
 homeassistant/components/elkm1/light.py       |  15 +-
 homeassistant/components/elkm1/manifest.json  |   9 +-
 homeassistant/components/elkm1/scene.py       |  16 +-
 homeassistant/components/elkm1/sensor.py      |  52 ++--
 homeassistant/components/elkm1/strings.json   |  28 ++
 homeassistant/components/elkm1/switch.py      |  18 +-
 homeassistant/generated/config_flows.py       |   1 +
 requirements_all.txt                          |   2 +-
 requirements_test_all.txt                     |   3 +
 tests/components/elkm1/__init__.py            |   1 +
 tests/components/elkm1/test_config_flow.py    | 269 ++++++++++++++++++
 18 files changed, 824 insertions(+), 215 deletions(-)
 create mode 100644 homeassistant/components/elkm1/.translations/en.json
 create mode 100644 homeassistant/components/elkm1/config_flow.py
 create mode 100644 homeassistant/components/elkm1/const.py
 create mode 100644 homeassistant/components/elkm1/strings.json
 create mode 100644 tests/components/elkm1/__init__.py
 create mode 100644 tests/components/elkm1/test_config_flow.py

diff --git a/CODEOWNERS b/CODEOWNERS
index 62e6f0d8e66..cbd4ae11c24 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -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
diff --git a/homeassistant/components/elkm1/.translations/en.json b/homeassistant/components/elkm1/.translations/en.json
new file mode 100644
index 00000000000..a5246a004c3
--- /dev/null
+++ b/homeassistant/components/elkm1/.translations/en.json
@@ -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"
+    }
+  }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py
index 2acb8030cf1..2f08b046d9c 100644
--- a/homeassistant/components/elkm1/__init__.py
+++ b/homeassistant/components/elkm1/__init__.py
@@ -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 = {}
-
-    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,
-    }
+    hass.data.setdefault(DOMAIN, {})
+    _create_elk_services(hass)
 
-    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]}
-        config["panel"] = {"enabled": True, "included": [True]}
+        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
 
-        for item, max_ in configs.items():
+    _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 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",
+        }
diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py
index de1cb62234c..d7cd5cf2ad0 100644
--- a/homeassistant/components/elkm1/alarm_control_panel.py
+++ b/homeassistant/components/elkm1/alarm_control_panel.py
@@ -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):
-    """Set up the ElkM1 alarm platform."""
-    if discovery_info is None:
-        return
 
-    elk_datas = hass.data[DOMAIN]
+async def async_setup_entry(hass, config_entry, async_add_entities):
+    """Set up the ElkM1 alarm platform."""
+    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)
-
-    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 = entity_platform.current_platform.get()
+
+    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 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 _display_message(self, clear, beep, timeout, line1, line2):
+    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)
diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py
index abc9dc0933c..3c5c70b2bd0 100644
--- a/homeassistant/components/elkm1/climate.py
+++ b/homeassistant/components/elkm1/climate.py
@@ -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):
diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py
new file mode 100644
index 00000000000..cad3ecac42a
--- /dev/null
+++ b/homeassistant/components/elkm1/config_flow.py
@@ -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."""
diff --git a/homeassistant/components/elkm1/const.py b/homeassistant/components/elkm1/const.py
new file mode 100644
index 00000000000..bad6d7fbcf1
--- /dev/null
+++ b/homeassistant/components/elkm1/const.py
@@ -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,
+}
diff --git a/homeassistant/components/elkm1/light.py b/homeassistant/components/elkm1/light.py
index 10a9ae1b931..b7cfe20dfd8 100644
--- a/homeassistant/components/elkm1/light.py
+++ b/homeassistant/components/elkm1/light.py
@@ -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)
 
 
diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json
index c75da1ef039..17b016fcb8b 100644
--- a/homeassistant/components/elkm1/manifest.json
+++ b/homeassistant/components/elkm1/manifest.json
@@ -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
 }
diff --git a/homeassistant/components/elkm1/scene.py b/homeassistant/components/elkm1/scene.py
index dc5ea39d154..1f894cc7681 100644
--- a/homeassistant/components/elkm1/scene.py
+++ b/homeassistant/components/elkm1/scene.py
@@ -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):
diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py
index df29e1cda7e..79987d806a1 100644
--- a/homeassistant/components/elkm1/sensor.py
+++ b/homeassistant/components/elkm1/sensor.py
@@ -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):
-    """Create the Elk-M1 sensor platform."""
-    if discovery_info is None:
-        return
 
-    elk_datas = hass.data[ELK_DOMAIN]
+async def async_setup_entry(hass, config_entry, async_add_entities):
+    """Create the Elk-M1 sensor platform."""
+    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:
diff --git a/homeassistant/components/elkm1/strings.json b/homeassistant/components/elkm1/strings.json
new file mode 100644
index 00000000000..a5246a004c3
--- /dev/null
+++ b/homeassistant/components/elkm1/strings.json
@@ -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"
+    }
+  }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elkm1/switch.py b/homeassistant/components/elkm1/switch.py
index e6dd82dc0ac..af32e81bc4c 100644
--- a/homeassistant/components/elkm1/switch.py
+++ b/homeassistant/components/elkm1/switch.py
@@ -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
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 8c03702e8f9..05bc4a7ba4a 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -28,6 +28,7 @@ FLOWS = [
     "dynalite",
     "ecobee",
     "elgato",
+    "elkm1",
     "emulated_roku",
     "esphome",
     "freebox",
diff --git a/requirements_all.txt b/requirements_all.txt
index f0ba017f574..8d377e3602d 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -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
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f94245b6df5..7bded1c1d21 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -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
 
diff --git a/tests/components/elkm1/__init__.py b/tests/components/elkm1/__init__.py
new file mode 100644
index 00000000000..8ae7f6d7b49
--- /dev/null
+++ b/tests/components/elkm1/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Elk-M1 Control integration."""
diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py
new file mode 100644
index 00000000000..466005f3d43
--- /dev/null
+++ b/tests/components/elkm1/test_config_flow.py
@@ -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
-- 
GitLab