Skip to content

Commit

Permalink
Add auth config flow (#6)
Browse files Browse the repository at this point in the history
* Update backend - documentation - version v0.2.0

* Add support cloud config

* Documentation update

* Add tests
  • Loading branch information
jbouwh authored Aug 1, 2022
1 parent 9bfe533 commit 4d5adc8
Show file tree
Hide file tree
Showing 12 changed files with 407 additions and 31 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ Platform | Description
`sensor` | Adds a `device state` sensor, a `battery level` sensor and a `signal` sensor (disabled by default) for each device.
`siren` | Represents Elro Connects alarms as a siren. Turn the siren `ON` to test it. Turn it `OFF` to silence the (test) alarm.

The `device_state` sensor can have of the following states:
- `FAULT`
- `SILENCE`
- `TEST ALARM`
- `FIRE ALARM`
- `ALARM`
- `NORMAL`
- `UNKNOWN`
- `OFFLINE`

Note that the sensors are polled about every 15 seconds. So it might take some time before an alarm state will be propagated. If an unknown state is found that is not supported yet, the hexadecimal code will be assigned as state. Please open an issue [here](https://github.com/jbouwh/lib-elro-connects/issues/new) if a new state needs to be supported.

The `siren` platform (for enabling a test alarm) was tested and is supported for Fire, Heat, CO and Water alarms.

## Installation
Expand All @@ -40,6 +52,13 @@ The `siren` platform (for enabling a test alarm) was tested and is supported for

Using your HA configuration directory (folder) as a starting point you should now also have something like this:

#### Configuring the integration

1. You need the IP-address and your Elro Connects cloud credentials (`username` and `password`) to setup the integration. This will get the `connector_id` and `api_key` for local access of your connector. After the setup has finished setup, the cloud credentials will not be used during operation.
2. An alternative is a manual setup. For this you need to fill in the `connector_id`, leave `username` and `password` fields open. which can be obtained from the Elro Connects app. Go to the `home` tab and click on the settings wheel. Select `current connector`. A list will be shown with your connectors. The ID starts with `ST_xxx...`.
3. The API key is probably not needed as long as it is provided by the connector locally. This behavior might change in the future.


```text
custom_components/elro_connects/translations/en.json
custom_components/elro_connects/translations/nl.json
Expand Down
84 changes: 71 additions & 13 deletions custom_components/elro_connects/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
from typing import Any

from elro.api import K1
from elro.auth import ElroConnectsConnector, ElroConnectsSession
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
Expand All @@ -21,12 +28,17 @@

ELRO_CONNECTS_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_CONNECTOR_ID): str,
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_USERNAME): str,
vol.Optional(CONF_PASSWORD): str,
vol.Optional(CONF_CONNECTOR_ID): str,
vol.Optional(CONF_API_KEY): str,
}
)

TITLE = "Elro Connects K1 Connector"


class K1ConnectionTest:
"""Elro Connects K1 connection test."""
Expand All @@ -35,9 +47,11 @@ def __init__(self, host: str) -> None:
"""Initialize."""
self.host = host

async def async_try_connection(self, connector_id: str, port: int) -> bool:
async def async_try_connection(
self, connector_id: str, port: int, api_key: str | None = None
) -> bool:
"""Test if we can authenticate with the host."""
connector = K1(self.host, connector_id, port)
connector = K1(self.host, connector_id, port, api_key)
try:
await connector.async_connect()
except K1.K1ConnectionError:
Expand All @@ -52,12 +66,39 @@ async def async_validate_input(
) -> dict[str, Any]:
"""Validate the user input allows us to connect."""

hub = K1ConnectionTest(data["host"])
connectors: list[ElroConnectsConnector] = []
info = {}
info.update(data)

if not await hub.async_try_connection(data["connector_id"], data["port"]):
# get cloud info if username and password are given
if info.get(CONF_USERNAME) and info.get(CONF_PASSWORD):
try:
session = ElroConnectsSession()
await session.async_login(info[CONF_USERNAME], info[CONF_PASSWORD])
connectors = await session.async_get_connectors()
except Exception as exp: # pylint: disable=broad-except
raise CannotConnect from exp

# Add api key from cloud and perform connection test
while connectors:
connector = connectors.pop(0)
if CONF_CONNECTOR_ID in info:
# Match connector ID to find the key
if info[CONF_CONNECTOR_ID] == connector["dev_id"]:
info[CONF_API_KEY] = connector["ctrl_key"]
else:
# Use first connector found
info[CONF_API_KEY] = connector["ctrl_key"]
info[CONF_CONNECTOR_ID] = connector["dev_id"]
continue

hub = K1ConnectionTest(data["host"])
if CONF_CONNECTOR_ID not in info or not await hub.async_try_connection(
info[CONF_CONNECTOR_ID], data[CONF_PORT], info.get(CONF_API_KEY)
):
raise CannotConnect

return {"title": "Elro Connects K1 Connector"}
return info


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
Expand Down Expand Up @@ -90,9 +131,9 @@ async def async_step_user(
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(user_input[CONF_CONNECTOR_ID])
await self.async_set_unique_id(info[CONF_CONNECTOR_ID])
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_create_entry(title=TITLE, data=info)

return self.async_show_form(
step_id="user", data_schema=ELRO_CONNECTS_DATA_SCHEMA, errors=errors
Expand All @@ -115,19 +156,18 @@ async def async_step_init(
if user_input is not None:
changed_input = {}
changed_input.update(user_input)
changed_input[CONF_CONNECTOR_ID] = entry_data.get(CONF_CONNECTOR_ID)
try:
await async_validate_input(self.hass, changed_input)
info = await async_validate_input(self.hass, changed_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
self.hass.config_entries.async_update_entry(
self.config_entry, data=changed_input
self.config_entry, data=info
)
return self.async_create_entry(title="", data=user_input)
return self.async_create_entry(title="", data=changed_input)

return self.async_show_form(
step_id="init",
Expand All @@ -136,6 +176,24 @@ async def async_step_init(
{
vol.Required(CONF_HOST, default=entry_data.get(CONF_HOST)): str,
vol.Required(CONF_PORT, default=entry_data.get(CONF_PORT)): cv.port,
vol.Optional(
CONF_USERNAME,
description={"suggested_value": entry_data.get(CONF_USERNAME)},
): str,
vol.Optional(
CONF_PASSWORD,
description={"suggested_value": entry_data.get(CONF_PASSWORD)},
): str,
vol.Optional(
CONF_CONNECTOR_ID,
description={
"suggested_value": entry_data.get(CONF_CONNECTOR_ID)
},
): str,
vol.Optional(
CONF_API_KEY,
description={"suggested_value": entry_data.get(CONF_API_KEY)},
): str,
}
),
)
Expand Down
4 changes: 2 additions & 2 deletions custom_components/elro_connects/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"config_flow": true,
"documentation": "https://github.com/jbouwh/ha-elro-connects",
"issue_tracker": "https://github.com/jbouwh/ha-elro-connects/issues",
"requirements": ["lib-elro-connects==0.4.5"],
"requirements": ["lib-elro-connects==0.5.1"],
"codeowners": ["@jbouwh"],
"iot_class": "local_polling",
"version": "0.1.6"
"version": "0.2.0"
}
13 changes: 10 additions & 3 deletions custom_components/elro_connects/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
"step": {
"user": {
"data": {
"connector_id": "Connector ID",
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"connector_id": "Connector ID",
"api_key": "[%key:common::config_flow::data::api_key%]"
}
}
},
Expand All @@ -22,7 +25,11 @@
"init": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"connector_id": "Connector ID",
"api_key": "[%key:common::config_flow::data::api_key%]"
}
}
}
Expand Down
15 changes: 11 additions & 4 deletions custom_components/elro_connects/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
"step": {
"user": {
"data": {
"connector_id": "Connector ID",
"host": "Hostname or IP address",
"port": "Port"
"port": "Port",
"username": "Username",
"password": "Password",
"connector_id": "Connector ID",
"api_key": "API key"
}
}
}
Expand All @@ -21,8 +24,12 @@
"step": {
"init": {
"data": {
"host": "Host",
"port": "Port"
"host": "Hostname or IP address",
"port": "Port",
"username": "Username",
"password": "Password",
"connector_id": "Connector ID",
"api_key": "API key"
}
}
}
Expand Down
15 changes: 11 additions & 4 deletions custom_components/elro_connects/translations/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
"step": {
"user": {
"data": {
"connector_id": "Connector ID",
"host": "Hostnaam of IP adres",
"port": "Poort"
"port": "Poort",
"username": "Gebruikersnaam",
"password": "Wachtwoord",
"connector_id": "Connector ID",
"api_key": "API key"
}
}
}
Expand All @@ -22,8 +25,12 @@
"init": {
"data": {
"host": "Hostnaam of IP adres",
"port": "Poort"
}
"port": "Poort",
"username": "Gebruikersnaam",
"password": "Wachtwoord",
"connector_id": "Connector ID",
"api_key": "API key"
}
}
}
}
Expand Down
23 changes: 22 additions & 1 deletion info.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,36 @@ Platform | Description
`sensor` | Adds a `device state` sensor, a `battery level` sensor and a `signal` sensor (disabled by default) for each device.
`siren` | Represents Elro Connects alarms as a siren. Turn the siren `ON` to test it. Turn it `OFF` to silence the (test) alarm.

The `device_state` sensor can have of the following states:
- `FAULT`
- `SILENCE`
- `TEST ALARM`
- `FIRE ALARM`
- `ALARM`
- `NORMAL`
- `UNKNOWN`
- `OFFLINE`

Note that the sensors are polled about every 15 seconds. So it might take some time before an alarm state will be propagated. If an unknown state is found that is not supported yet, the hexadecimal code will be assigned as state. Please open an issue [here](https://github.com/jbouwh/lib-elro-connects/issues/new) if a new state needs to be supported.

The `siren` platform (for enabling a test alarm) was tested and is supported for Fire, Heat, CO and Water alarms.

{% if not installed %}

## Installation

1. Click install.
1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Elro Connects".
2. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Elro Connects" and add the integration.
3. When the installation via HACS is done, restart Home Assistant.

{% endif %}

#### Configuring the integration

1. You need the IP-address and your Elro Connects cloud credentials (`username` and `password`) to setup the integration. This will get the `connector_id` and `api_key` for local access of your connector. After the setup has finished setup, the cloud credentials will not be used during operation.
2. An alternative is a manual setup. For this you need to fill in the `connector_id`, leave `username` and `password` fields open. which can be obtained from the Elro Connects app. Go to the `home` tab and click on the settings wheel. Select `current connector`. A list will be shown with your connectors. The ID starts with `ST_xxx...`.
3. The API key is probably not needed as long as it is provided by the connector locally. This behavior might change in the future.

***

[integration_blueprint]: https://github.com/jbouwh/ha-elro/connects
Expand Down
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pre-commit
lib-elro-connects==0.4.5
lib-elro-connects==0.5.1
homeassistant
pytest-homeassistant-custom-component
2 changes: 1 addition & 1 deletion requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ fnvhash==0.1.0

lru-dict==1.1.7

lib-elro-connects==0.4.5
lib-elro-connects==0.5.1

pytest-homeassistant-custom-component
14 changes: 14 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ def auto_enable_custom_integrations(enable_custom_integrations):
yield


@pytest.fixture
def mock_get() -> AsyncMock:
"""Mock aiohttp get and post requests."""
with patch("aiohttp.ClientSession.get") as mock_session_get:
yield mock_session_get


@pytest.fixture
def mock_post() -> AsyncMock:
"""Mock aiohttp get and post requests."""
with patch("aiohttp.ClientSession.post") as mock_session_post:
yield mock_session_post


@pytest.fixture
def mock_k1_connector() -> dict[AsyncMock]:
"""Mock the Elro K1 connector class."""
Expand Down
Loading

0 comments on commit 4d5adc8

Please sign in to comment.