Open garage, add config flow (#55290)
parent
40ecf22bac
commit
d5c3d234ec
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."""
|
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -201,6 +201,7 @@ FLOWS = [
|
|||
"ondilo_ico",
|
||||
"onewire",
|
||||
"onvif",
|
||||
"opengarage",
|
||||
"opentherm_gw",
|
||||
"openuv",
|
||||
"openweathermap",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the OpenGarage integration."""
|
|
@ -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
|
Loading…
Reference in New Issue