Open garage, add config flow (#55290)

pull/56829/head
Daniel Hjelseth Høyer 2021-09-29 17:43:51 +02:00 committed by GitHub
parent 40ecf22bac
commit d5c3d234ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 428 additions and 38 deletions

View File

@ -764,6 +764,7 @@ omit =
homeassistant/components/opencv/*
homeassistant/components/openevse/sensor.py
homeassistant/components/openexchangerates/sensor.py
homeassistant/components/opengarage/__init__.py
homeassistant/components/opengarage/cover.py
homeassistant/components/openhome/__init__.py
homeassistant/components/openhome/media_player.py

View File

@ -1 +1,38 @@
"""The opengarage component."""
"""The OpenGarage integration."""
from __future__ import annotations
import opengarage
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_DEVICE_KEY, DOMAIN
PLATFORMS = ["cover"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up OpenGarage from a config entry."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = opengarage.OpenGarage(
f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}",
entry.data[CONF_DEVICE_KEY],
entry.data[CONF_VERIFY_SSL],
async_get_clientsession(hass),
)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,107 @@
"""Config flow for OpenGarage integration."""
from __future__ import annotations
import logging
from typing import Any
import aiohttp
import opengarage
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from .const import CONF_DEVICE_KEY, DEFAULT_PORT, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_KEY): str,
vol.Required(CONF_HOST, default="http://"): str,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
vol.Optional(CONF_VERIFY_SSL, default=False): bool,
}
)
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
open_garage = opengarage.OpenGarage(
f"{data[CONF_HOST]}:{data[CONF_PORT]}",
data[CONF_DEVICE_KEY],
data[CONF_VERIFY_SSL],
async_get_clientsession(hass),
)
try:
status = await open_garage.update_state()
except aiohttp.ClientError as exp:
raise CannotConnect from exp
if status is None:
raise InvalidAuth
return {"title": status.get("name"), "unique_id": format_mac(status["mac"])}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for OpenGarage."""
VERSION = 1
async def async_step_import(self, import_info):
"""Set the config entry up from yaml."""
import_info[CONF_HOST] = (
f"{'https' if import_info[CONF_SSL] else 'http'}://"
f"{import_info.get(CONF_HOST)}"
)
del import_info[CONF_SSL]
return await self.async_step_user(import_info)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
errors = {}
try:
info = await validate_input(self.hass, 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"
else:
await self.async_set_unique_id(info["unique_id"])
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@ -0,0 +1,11 @@
"""Constants for the OpenGarage integration."""
ATTR_DISTANCE_SENSOR = "distance_sensor"
ATTR_DOOR_STATE = "door_state"
ATTR_SIGNAL_STRENGTH = "wifi_signal"
CONF_DEVICE_KEY = "device_key"
DEFAULT_NAME = "OpenGarage"
DEFAULT_PORT = 80
DOMAIN = "opengarage"

View File

@ -1,9 +1,9 @@
"""Platform for the opengarage.io cover component."""
import logging
import opengarage
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.cover import (
DEVICE_CLASS_GARAGE,
PLATFORM_SCHEMA,
@ -23,21 +23,19 @@ from homeassistant.const import (
STATE_OPEN,
STATE_OPENING,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from .const import (
ATTR_DISTANCE_SENSOR,
ATTR_DOOR_STATE,
ATTR_SIGNAL_STRENGTH,
CONF_DEVICE_KEY,
DEFAULT_PORT,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
ATTR_DISTANCE_SENSOR = "distance_sensor"
ATTR_DOOR_STATE = "door_state"
ATTR_SIGNAL_STRENGTH = "wifi_signal"
CONF_DEVICE_KEY = "device_key"
DEFAULT_NAME = "OpenGarage"
DEFAULT_PORT = 80
STATES_MAP = {0: STATE_CLOSED, 1: STATE_OPEN}
COVER_SCHEMA = vol.Schema(
@ -58,29 +56,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the OpenGarage covers."""
covers = []
devices = config.get(CONF_COVERS)
for device_config in devices.values():
opengarage_url = (
f"{'https' if device_config[CONF_SSL] else 'http'}://"
f"{device_config.get(CONF_HOST)}:{device_config.get(CONF_PORT)}"
)
open_garage = opengarage.OpenGarage(
opengarage_url,
device_config[CONF_DEVICE_KEY],
device_config[CONF_VERIFY_SSL],
async_get_clientsession(hass),
)
status = await open_garage.update_state()
covers.append(
OpenGarageCover(
device_config.get(CONF_NAME), open_garage, format_mac(status["mac"])
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=device_config,
)
)
async_add_entities(covers, True)
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the OpenGarage covers."""
async_add_entities(
[OpenGarageCover(hass.data[DOMAIN][entry.entry_id], entry.unique_id)], True
)
class OpenGarageCover(CoverEntity):
@ -89,14 +80,13 @@ class OpenGarageCover(CoverEntity):
_attr_device_class = DEVICE_CLASS_GARAGE
_attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
def __init__(self, name, open_garage, device_id):
def __init__(self, open_garage, device_id):
"""Initialize the cover."""
self._attr_name = name
self._open_garage = open_garage
self._state = None
self._state_before_move = None
self._extra_state_attributes = {}
self._attr_unique_id = device_id
self._attr_unique_id = self._device_id = device_id
@property
def extra_state_attributes(self):
@ -183,3 +173,13 @@ class OpenGarageCover(CoverEntity):
self._state = self._state_before_move
self._state_before_move = None
@property
def device_info(self):
"""Return the device_info of the device."""
device_info = {
"identifiers": {(DOMAIN, self._device_id)},
"name": self.name,
"manufacturer": "Open Garage",
}
return device_info

View File

@ -2,7 +2,12 @@
"domain": "opengarage",
"name": "OpenGarage",
"documentation": "https://www.home-assistant.io/integrations/opengarage",
"codeowners": ["@danielhiversen"],
"requirements": ["open-garage==0.1.5"],
"iot_class": "local_polling"
}
"codeowners": [
"@danielhiversen"
],
"requirements": [
"open-garage==0.1.5"
],
"iot_class": "local_polling",
"config_flow": true
}

View File

@ -0,0 +1,22 @@
{
"config": {
"step": {
"user": {
"data": {
"device_key": "Device key",
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@ -201,6 +201,7 @@ FLOWS = [
"ondilo_ico",
"onewire",
"onvif",
"opengarage",
"opentherm_gw",
"openuv",
"openweathermap",

View File

@ -660,6 +660,9 @@ ondilo==0.2.0
# homeassistant.components.onvif
onvif-zeep-async==1.2.0
# homeassistant.components.opengarage
open-garage==0.1.5
# homeassistant.components.openerz
openerz-api==0.1.0

View File

@ -0,0 +1 @@
"""Tests for the OpenGarage integration."""

View File

@ -0,0 +1,202 @@
"""Test the OpenGarage config flow."""
from unittest.mock import patch
import aiohttp
from homeassistant import config_entries, setup
from homeassistant.components.opengarage.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from tests.common import MockConfigEntry
async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
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"] == RESULT_TYPE_FORM
assert result["errors"] is None
with patch(
"opengarage.OpenGarage.update_state",
return_value={"name": "Name of the device", "mac": "unique"},
), patch(
"homeassistant.components.opengarage.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "Name of the device"
assert result2["data"] == {
"host": "http://1.1.1.1",
"device_key": "AfsasdnfkjDD",
"port": 80,
"verify_ssl": False,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
"""Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"opengarage.OpenGarage.update_state",
return_value=None,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"},
)
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "invalid_auth"}
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"opengarage.OpenGarage.update_state",
side_effect=aiohttp.ClientError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"},
)
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_unknown_error(hass: HomeAssistant) -> None:
"""Test we handle unknown error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"opengarage.OpenGarage.update_state",
side_effect=Exception,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"},
)
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "unknown"}
async def test_flow_entry_already_exists(hass: HomeAssistant) -> None:
"""Test user input for config_entry that already exists."""
first_entry = MockConfigEntry(
domain="opengarage",
data={
"host": "http://1.1.1.1",
"device_key": "AfsasdnfkjDD",
},
unique_id="unique",
)
first_entry.add_to_hass(hass)
with patch(
"opengarage.OpenGarage.update_state",
return_value={"name": "Name of the device", "mac": "unique"},
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data={
"host": "http://1.1.1.1",
"device_key": "AfsasdnfkjDD",
"port": 80,
"verify_ssl": False,
},
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_step_import(hass: HomeAssistant) -> None:
"""Test when import configuring from yaml."""
with patch(
"opengarage.OpenGarage.update_state",
return_value={"name": "Name of the device", "mac": "unique"},
), patch(
"homeassistant.components.opengarage.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": "1.1.1.1",
"device_key": "AfsasdnfkjDD",
"port": 1234,
"verify_ssl": False,
"ssl": False,
},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Name of the device"
assert result["data"] == {
"host": "http://1.1.1.1",
"device_key": "AfsasdnfkjDD",
"port": 1234,
"verify_ssl": False,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_step_import_ssl(hass: HomeAssistant) -> None:
"""Test when import configuring from yaml."""
with patch(
"opengarage.OpenGarage.update_state",
return_value={"name": "Name of the device", "mac": "unique"},
), patch(
"homeassistant.components.opengarage.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": "1.1.1.1",
"device_key": "AfsasdnfkjDD",
"port": 1234,
"verify_ssl": False,
"ssl": True,
},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Name of the device"
assert result["data"] == {
"host": "https://1.1.1.1",
"device_key": "AfsasdnfkjDD",
"port": 1234,
"verify_ssl": False,
}
assert len(mock_setup_entry.mock_calls) == 1