mirror of
https://github.com/home-assistant/core.git
synced 2025-04-27 18:57:57 +00:00
Add Totalconnect config flow (#32126)
* Bump skybellpy to 0.4.0 * Bump skybellpy to 0.4.0 in requirements_all.txt * Added extra states for STATE_ALARM_TRIGGERED to allow users to know if it is a burglar or fire or carbon monoxide so automations can take appropriate actions. Updated TotalConnect component to handle these new states. * Fix const import * Fix const import * Fix const imports * Bump total-connect-client to 0.26. * Catch details of alarm trigger in state attributes. Also bumps total_connect_client to 0.27. * Change state_attributes() to device_state_attributes() * Move totalconnect component toward being a multi-platform integration. Bump total_connect_client to 0.28. * add missing total-connect alarm state mappings * Made recommended changes of MartinHjelmare at https://github.com/home-assistant/home-assistant/pull/24427 * Update __init__.py * Updates per MartinHjelmare comments * flake8/pydocstyle fixes * removed . at end of log message * added blank line between logging and voluptuous * more fixes * Adding totalconnect zones as HA binary_sensors * fix manifest.json * flake8/pydocstyle fixes. Added codeowner. * Update formatting per @springstan guidance. * Fixed pylint * Add zone ID to log message for easier troubleshooting * Account for bypassed zones in update() * More status handling fixes. * Fixed flake8 error * Another attempt at black/isort fixes. * Bump total-connect-client to 0.50. Simplify code using new functions in total-connect-client package instead of importing constants. Run black and isort. * Fix manifest file * Another manifest fix * one more manifest fix * more manifest changes. * sync up * fix indent * one more pylint fix * Hopefully the last pylint fix * make variable names understandable * create and fill dict in one step * Fix name and attributes * rename to logical variable in alarm_control_panel * Remove location_name from alarm_control_panel attributes since it is already the name of the alarm. * Multiple fixes to improve code per @springstan suggestions * Update homeassistant/components/totalconnect/binary_sensor.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Multiple changes per @MartinHjelmare review * simplify alarm adding * Fix binary_sensor.py is_on * Move DOMAIN to .const in line with examples. * Move to async_setup * Simplify code using new features of total-connect-client 0.51 * First crack at config flow for totalconnect * bump totalconnect to 0.52 * use client.is_logged_in() to avoid total-connect-client details. * updated generated/config_flow.py * use is_logged_in() * Hopefully final touches for config flow * Add tests for config flow * Updated requirements for test * Fixes to test_config_flow * Removed leftover comments and code * fix const.py flake8 error * Simplify text per @Kane610 https://github.com/home-assistant/home-assistant/pull/32126#pullrequestreview-364652949 * Remove .get() to speed things up since the required items should always be available. * Move CONF_USERNAME and CONF_PASSWORD into .const to eliminate extra I/O to import from homeassistant.const * Fix I/O async issues * Fix flake8 and black errors * Mock the I/O in tests. * Fix isort error * Empty commit to re-start azure pipelines (per discord) * bump total-connect-client to 0.53 * Update homeassistant/components/totalconnect/__init__.py Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io> * Update homeassistant/components/totalconnect/config_flow.py Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io> * Fixes per @balloob comments * Fix imports * fix isort error * Fix async_unload_entry It still referenced CONF_USERNAME instead of entry.entity_id * Added async_setup so not breaking change. Fixed imports. * Update tests/components/totalconnect/test_config_flow.py Co-Authored-By: Martin Hjelmare <marhje52@gmail.com> * Remove TotalConnectSystem() per @MartinHjelmare suggestion * Moved from is_logged_in() to is_valid_credentials() The second is more accurate for what we are checking for, because is_logged_in() could return False due to connection error. * Fix import in test * remove commented code and decorator * Update tests/components/totalconnect/test_config_flow.py Co-Authored-By: Martin Hjelmare <marhje52@gmail.com> * fix test_config_flow.py * bump total-connect-client to 0.54 * remove un-needed import of mock_coro * bump to total-connect-client 0.54.1 * re-add CONFIG_SCHEMA * disable pylint on line 10 to avoid pylint bug Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
ab1eda7412
commit
c7ab5de07c
20
homeassistant/components/totalconnect/.translations/en.json
Normal file
20
homeassistant/components/totalconnect/.translations/en.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Account already configured"
|
||||
},
|
||||
"error": {
|
||||
"login": "Login error: please check your username & password"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
},
|
||||
"title": "Total Connect"
|
||||
}
|
||||
},
|
||||
"title": "Total Connect"
|
||||
}
|
||||
}
|
@ -1,16 +1,20 @@
|
||||
"""The totalconnect component."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from total_connect_client import TotalConnectClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "totalconnect"
|
||||
PLATFORMS = ["alarm_control_panel", "binary_sensor"]
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
@ -20,39 +24,61 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
}
|
||||
)
|
||||
|
||||
TOTALCONNECT_PLATFORMS = ["alarm_control_panel", "binary_sensor"]
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up by configuration file."""
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up TotalConnect component."""
|
||||
conf = config[DOMAIN]
|
||||
|
||||
username = conf[CONF_USERNAME]
|
||||
password = conf[CONF_PASSWORD]
|
||||
|
||||
client = TotalConnectClient.TotalConnectClient(username, password)
|
||||
|
||||
if client.token is False:
|
||||
_LOGGER.error("TotalConnect authentication failed")
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN] = TotalConnectSystem(username, password, client)
|
||||
|
||||
for platform in TOTALCONNECT_PLATFORMS:
|
||||
discovery.load_platform(hass, platform, DOMAIN, {}, config)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN],
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class TotalConnectSystem:
|
||||
"""TotalConnect System class."""
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up upon config entry in user interface."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
def __init__(self, username, password, client):
|
||||
"""Initialize the TotalConnect system."""
|
||||
self._username = username
|
||||
self._password = password
|
||||
self.client = client
|
||||
conf = entry.data
|
||||
username = conf[CONF_USERNAME]
|
||||
password = conf[CONF_PASSWORD]
|
||||
|
||||
client = await hass.async_add_executor_job(
|
||||
TotalConnectClient.TotalConnectClient, username, password
|
||||
)
|
||||
|
||||
if not client.is_valid_credentials():
|
||||
_LOGGER.error("TotalConnect authentication failed")
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = client
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, platform)
|
||||
for platform in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
@ -23,19 +23,17 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up an alarm control panel for a TotalConnect device."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities) -> None:
|
||||
"""Set up TotalConnect alarm panels based on a config entry."""
|
||||
alarms = []
|
||||
|
||||
client = hass.data[DOMAIN].client
|
||||
client = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
for location_id, location in client.locations.items():
|
||||
location_name = location.location_name
|
||||
alarms.append(TotalConnectAlarm(location_name, location_id, client))
|
||||
add_entities(alarms)
|
||||
|
||||
async_add_entities(alarms, True)
|
||||
|
||||
|
||||
class TotalConnectAlarm(alarm.AlarmControlPanel):
|
||||
|
@ -8,24 +8,22 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice,
|
||||
)
|
||||
|
||||
from . import DOMAIN as TOTALCONNECT_DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up a sensor for a TotalConnect device."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities) -> None:
|
||||
"""Set up TotalConnect device sensors based on a config entry."""
|
||||
sensors = []
|
||||
|
||||
client_locations = hass.data[TOTALCONNECT_DOMAIN].client.locations
|
||||
client_locations = hass.data[DOMAIN][entry.entry_id].locations
|
||||
|
||||
for location_id, location in client_locations.items():
|
||||
for zone_id, zone in location.zones.items():
|
||||
sensors.append(TotalConnectBinarySensor(zone_id, location_id, zone))
|
||||
add_entities(sensors, True)
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
||||
class TotalConnectBinarySensor(BinarySensorDevice):
|
||||
|
60
homeassistant/components/totalconnect/config_flow.py
Normal file
60
homeassistant/components/totalconnect/config_flow.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Config flow for the Total Connect component."""
|
||||
import logging
|
||||
|
||||
from total_connect_client import TotalConnectClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from .const import DOMAIN # pylint: disable=unused-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Total Connect config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initiated by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
# Validate user input
|
||||
username = user_input[CONF_USERNAME]
|
||||
password = user_input[CONF_PASSWORD]
|
||||
|
||||
await self.async_set_unique_id(username)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
valid = await self.is_valid(username, password)
|
||||
|
||||
if valid:
|
||||
# authentication success / valid
|
||||
return self.async_create_entry(
|
||||
title="Total Connect",
|
||||
data={CONF_USERNAME: username, CONF_PASSWORD: password},
|
||||
)
|
||||
# authentication failed / invalid
|
||||
errors["base"] = "login"
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=data_schema, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, user_input):
|
||||
"""Import a config entry."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def is_valid(self, username="", password=""):
|
||||
"""Return true if the given username and password are valid."""
|
||||
client = await self.hass.async_add_executor_job(
|
||||
TotalConnectClient.TotalConnectClient, username, password
|
||||
)
|
||||
return client.is_valid_credentials()
|
@ -3,5 +3,7 @@
|
||||
"name": "Honeywell Total Connect Alarm",
|
||||
"documentation": "https://www.home-assistant.io/integrations/totalconnect",
|
||||
"requirements": ["total_connect_client==0.54.1"],
|
||||
"codeowners": ["@austinmroczek"]
|
||||
"dependencies": [],
|
||||
"codeowners": ["@austinmroczek"],
|
||||
"config_flow": true
|
||||
}
|
||||
|
20
homeassistant/components/totalconnect/strings.json
Normal file
20
homeassistant/components/totalconnect/strings.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Total Connect",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Total Connect",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"login": "Login error: please check your username & password"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Account already configured"
|
||||
}
|
||||
}
|
||||
}
|
@ -117,6 +117,7 @@ FLOWS = [
|
||||
"tellduslive",
|
||||
"tesla",
|
||||
"toon",
|
||||
"totalconnect",
|
||||
"tplink",
|
||||
"traccar",
|
||||
"tradfri",
|
||||
|
@ -761,6 +761,9 @@ teslajsonpy==0.6.0
|
||||
# homeassistant.components.toon
|
||||
toonapilib==3.2.4
|
||||
|
||||
# homeassistant.components.totalconnect
|
||||
total_connect_client==0.54.1
|
||||
|
||||
# homeassistant.components.transmission
|
||||
transmissionrpc==0.11
|
||||
|
||||
|
1
tests/components/totalconnect/__init__.py
Normal file
1
tests/components/totalconnect/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the totalconnect component."""
|
104
tests/components/totalconnect/test_config_flow.py
Normal file
104
tests/components/totalconnect/test_config_flow.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""Tests for the iCloud config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.totalconnect.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
USERNAME = "username@me.com"
|
||||
PASSWORD = "password"
|
||||
|
||||
|
||||
async def test_user(hass):
|
||||
"""Test user config."""
|
||||
# no data provided so show the form
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
# now data is provided, so check if login is correct and create the entry
|
||||
with patch(
|
||||
"homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient"
|
||||
) as client_mock:
|
||||
client_mock.return_value.is_valid_credentials.return_value = True
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_import(hass):
|
||||
"""Test import step with good username and password."""
|
||||
with patch(
|
||||
"homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient"
|
||||
) as client_mock:
|
||||
client_mock.return_value.is_valid_credentials.return_value = True
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_abort_if_already_setup(hass):
|
||||
"""Test abort if the account is already setup."""
|
||||
MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
|
||||
unique_id=USERNAME,
|
||||
).add_to_hass(hass)
|
||||
|
||||
# Should fail, same USERNAME (import)
|
||||
with patch(
|
||||
"homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient"
|
||||
) as client_mock:
|
||||
client_mock.return_value.is_valid_credentials.return_value = True
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
# Should fail, same USERNAME (flow)
|
||||
with patch(
|
||||
"homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient"
|
||||
) as client_mock:
|
||||
client_mock.return_value.is_valid_credentials.return_value = True
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_login_failed(hass):
|
||||
"""Test when we have errors during login."""
|
||||
with patch(
|
||||
"homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient"
|
||||
) as client_mock:
|
||||
client_mock.return_value.is_valid_credentials.return_value = False
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "login"}
|
Loading…
x
Reference in New Issue
Block a user