Add local API support to Overkiz integration (Somfy TaHoma Developer Mode) (#71644)
* Add initial config flow implementation * Add initial config flow implementation * Add todos * Bugfixes * Add first zeroconf code * Fixes for new firmware * Bugfixes for local integration * Delete local token * Fix diagnostics * Update translations and improve code * Update translations and improve code * Add local integration updates * Add local integration updates * Small tweaks * Add comments * Bugfix * Small code improvements * Small code improvements * Small code improvements * Small code improvements * Small code improvements * Small code improvements * Bugfixes * Small code improvements * Small code improvements * Change Config Flow (breaking change) * Remove token when integration is unloaded * Remove print * Simplify * Bugfixes * Improve configflow * Clean up unnecessary things * Catch nosuchtoken exception * Add migration for Config Flow * Add version 2 migration * Revert change in Config Flow * Fix api type * Update strings * Improve migrate entry * Implement changes * add more comments * Extend diagnostics * Ruff fixes * Clean up code * Bugfixes * Set gateway id * Start writing tests * Add first local test * Code coverage to 64% * Fixes * Remove local token on remove entry * Add debug logging + change manifest * Add developer mode check * Fix not_such_token issue * Small text changes * Bugfix * Fix tests * Address feedback * DRY * Test coverage to 77% * Coverage to 78% * Remove token removal by UUID * Add better retry methods * Clean up * Remove old data * 87% coverage * 90% code coverage * 100% code coverage * Use patch.multiple * Improve tests * Apply pre-commit after rebase * Fix breaking changes in ZeroconfServiceInfo * Add verify_ssl * Fix test import * Fix tests * Catch SSL verify failed * Revert hub to server rename * Move Config Flow version back to 1 * Add diagnostics tests * Fix tests * Fix strings * Implement feedback * Add debug logging for local connection errors * Simplify Config Flow and fix tests * Simplify Config Flow * Fix verify_ssl * Fix rebase mistake * Address feedback * Apply suggestions from code review * Update tests/components/overkiz/test_config_flow.py --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
01c49ba0e4
commit
75f237b587
13 changed files with 1130 additions and 233 deletions
|
@ -9,23 +9,32 @@ from typing import cast
|
|||
from aiohttp import ClientError
|
||||
from pyoverkiz.client import OverkizClient
|
||||
from pyoverkiz.const import SUPPORTED_SERVERS
|
||||
from pyoverkiz.enums import OverkizState, UIClass, UIWidget
|
||||
from pyoverkiz.enums import APIType, OverkizState, UIClass, UIWidget
|
||||
from pyoverkiz.exceptions import (
|
||||
BadCredentialsException,
|
||||
MaintenanceException,
|
||||
NotSuchTokenException,
|
||||
TooManyRequestsException,
|
||||
)
|
||||
from pyoverkiz.models import Device, Scenario, Setup
|
||||
from pyoverkiz.models import Device, OverkizServer, Scenario, Setup
|
||||
from pyoverkiz.utils import generate_local_server
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_TOKEN,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import (
|
||||
CONF_API_TYPE,
|
||||
CONF_HUB,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
|
@ -48,15 +57,26 @@ class HomeAssistantOverkizData:
|
|||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Overkiz from a config entry."""
|
||||
username = entry.data[CONF_USERNAME]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
server = SUPPORTED_SERVERS[entry.data[CONF_HUB]]
|
||||
client: OverkizClient | None = None
|
||||
api_type = entry.data.get(CONF_API_TYPE, APIType.CLOUD)
|
||||
|
||||
# To allow users with multiple accounts/hubs, we create a new session so they have separate cookies
|
||||
session = async_create_clientsession(hass)
|
||||
client = OverkizClient(
|
||||
username=username, password=password, session=session, server=server
|
||||
)
|
||||
# Local API
|
||||
if api_type == APIType.LOCAL:
|
||||
client = create_local_client(
|
||||
hass,
|
||||
host=entry.data[CONF_HOST],
|
||||
token=entry.data[CONF_TOKEN],
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
)
|
||||
|
||||
# Overkiz Cloud API
|
||||
else:
|
||||
client = create_cloud_client(
|
||||
hass,
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
server=SUPPORTED_SERVERS[entry.data[CONF_HUB]],
|
||||
)
|
||||
|
||||
await _async_migrate_entries(hass, entry)
|
||||
|
||||
|
@ -211,3 +231,31 @@ async def _async_migrate_entries(
|
|||
await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def create_local_client(
|
||||
hass: HomeAssistant, host: str, token: str, verify_ssl: bool
|
||||
) -> OverkizClient:
|
||||
"""Create Overkiz local client."""
|
||||
session = async_create_clientsession(hass, verify_ssl=verify_ssl)
|
||||
|
||||
return OverkizClient(
|
||||
username="",
|
||||
password="",
|
||||
token=token,
|
||||
session=session,
|
||||
server=generate_local_server(host=host),
|
||||
verify_ssl=verify_ssl,
|
||||
)
|
||||
|
||||
|
||||
def create_cloud_client(
|
||||
hass: HomeAssistant, username: str, password: str, server: OverkizServer
|
||||
) -> OverkizClient:
|
||||
"""Create Overkiz cloud client."""
|
||||
# To allow users with multiple accounts/hubs, we create a new session so they have separate cookies
|
||||
session = async_create_clientsession(hass)
|
||||
|
||||
return OverkizClient(
|
||||
username=username, password=password, session=session, server=server
|
||||
)
|
||||
|
|
|
@ -1,31 +1,46 @@
|
|||
"""Config flow for Overkiz (by Somfy) integration."""
|
||||
"""Config flow for Overkiz integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aiohttp import ClientConnectorCertificateError, ClientError
|
||||
from pyoverkiz.client import OverkizClient
|
||||
from pyoverkiz.const import SUPPORTED_SERVERS
|
||||
from pyoverkiz.const import SERVERS_WITH_LOCAL_API, SUPPORTED_SERVERS
|
||||
from pyoverkiz.enums import APIType, Server
|
||||
from pyoverkiz.exceptions import (
|
||||
BadCredentialsException,
|
||||
CozyTouchBadCredentialsException,
|
||||
MaintenanceException,
|
||||
NotSuchTokenException,
|
||||
TooManyAttemptsBannedException,
|
||||
TooManyRequestsException,
|
||||
UnknownUserException,
|
||||
)
|
||||
from pyoverkiz.models import obfuscate_id
|
||||
from pyoverkiz.models import OverkizServer
|
||||
from pyoverkiz.obfuscate import obfuscate_id
|
||||
from pyoverkiz.utils import generate_local_server, is_overkiz_gateway
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import dhcp, zeroconf
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_TOKEN,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import CONF_HUB, DEFAULT_HUB, DOMAIN, LOGGER
|
||||
from .const import CONF_API_TYPE, CONF_HUB, DEFAULT_SERVER, DOMAIN, LOGGER
|
||||
|
||||
|
||||
class DeveloperModeDisabled(HomeAssistantError):
|
||||
"""Error to indicate Somfy Developer Mode is disabled."""
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
@ -34,45 +49,112 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
VERSION = 1
|
||||
|
||||
_config_entry: ConfigEntry | None
|
||||
_default_user: None | str
|
||||
_default_hub: str
|
||||
_api_type: APIType
|
||||
_user: None | str
|
||||
_server: str
|
||||
_host: str
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Overkiz Config Flow."""
|
||||
super().__init__()
|
||||
|
||||
self._config_entry = None
|
||||
self._default_user = None
|
||||
self._default_hub = DEFAULT_HUB
|
||||
self._api_type = APIType.CLOUD
|
||||
self._user = None
|
||||
self._server = DEFAULT_SERVER
|
||||
self._host = "gateway-xxxx-xxxx-xxxx.local:8443"
|
||||
|
||||
async def async_validate_input(self, user_input: dict[str, Any]) -> None:
|
||||
async def async_validate_input(self, user_input: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate user credentials."""
|
||||
username = user_input[CONF_USERNAME]
|
||||
password = user_input[CONF_PASSWORD]
|
||||
server = SUPPORTED_SERVERS[user_input[CONF_HUB]]
|
||||
session = async_create_clientsession(self.hass)
|
||||
user_input[CONF_API_TYPE] = self._api_type
|
||||
|
||||
client = OverkizClient(
|
||||
username=username, password=password, server=server, session=session
|
||||
client = self._create_cloud_client(
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
server=SUPPORTED_SERVERS[user_input[CONF_HUB]],
|
||||
)
|
||||
|
||||
await client.login(register_event_listener=False)
|
||||
|
||||
# Set first gateway id as unique id
|
||||
# For Local API, we create and activate a local token
|
||||
if self._api_type == APIType.LOCAL:
|
||||
user_input[CONF_TOKEN] = await self._create_local_api_token(
|
||||
cloud_client=client,
|
||||
host=user_input[CONF_HOST],
|
||||
verify_ssl=user_input[CONF_VERIFY_SSL],
|
||||
)
|
||||
|
||||
# Set main gateway id as unique id
|
||||
if gateways := await client.get_gateways():
|
||||
gateway_id = gateways[0].id
|
||||
await self.async_set_unique_id(gateway_id)
|
||||
for gateway in gateways:
|
||||
if is_overkiz_gateway(gateway.id):
|
||||
gateway_id = gateway.id
|
||||
await self.async_set_unique_id(gateway_id)
|
||||
|
||||
return user_input
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step via config flow."""
|
||||
errors = {}
|
||||
if user_input:
|
||||
self._server = user_input[CONF_HUB]
|
||||
|
||||
# Some Overkiz hubs do support a local API
|
||||
# Users can choose between local or cloud API.
|
||||
if self._server in SERVERS_WITH_LOCAL_API:
|
||||
return await self.async_step_local_or_cloud()
|
||||
|
||||
return await self.async_step_cloud()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HUB, default=self._server): vol.In(
|
||||
{key: hub.name for key, hub in SUPPORTED_SERVERS.items()}
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_local_or_cloud(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Users can choose between local API or cloud API via config flow."""
|
||||
if user_input:
|
||||
self._api_type = user_input[CONF_API_TYPE]
|
||||
|
||||
if self._api_type == APIType.LOCAL:
|
||||
return await self.async_step_local()
|
||||
|
||||
return await self.async_step_cloud()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="local_or_cloud",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_TYPE): vol.In(
|
||||
{
|
||||
APIType.LOCAL: "Local API",
|
||||
APIType.CLOUD: "Cloud API",
|
||||
}
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_cloud(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the cloud authentication step via config flow."""
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders = {}
|
||||
|
||||
if user_input:
|
||||
self._default_user = user_input[CONF_USERNAME]
|
||||
self._default_hub = user_input[CONF_HUB]
|
||||
self._user = user_input[CONF_USERNAME]
|
||||
|
||||
# inherit the server from previous step
|
||||
user_input[CONF_HUB] = self._server
|
||||
|
||||
try:
|
||||
await self.async_validate_input(user_input)
|
||||
|
@ -81,7 +163,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
except BadCredentialsException as exception:
|
||||
# If authentication with CozyTouch auth server is valid, but token is invalid
|
||||
# for Overkiz API server, the hardware is not supported.
|
||||
if user_input[CONF_HUB] == "atlantic_cozytouch" and not isinstance(
|
||||
if user_input[CONF_HUB] == Server.ATLANTIC_COZYTOUCH and not isinstance(
|
||||
exception, CozyTouchBadCredentialsException
|
||||
):
|
||||
description_placeholders["unsupported_device"] = "CozyTouch"
|
||||
|
@ -99,9 +181,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
# the Overkiz API server. Login will return unknown user.
|
||||
description_placeholders["unsupported_device"] = "Somfy Protect"
|
||||
errors["base"] = "unsupported_hardware"
|
||||
except Exception as exception: # pylint: disable=broad-except
|
||||
except Exception: # pylint: disable=broad-except
|
||||
errors["base"] = "unknown"
|
||||
LOGGER.exception(exception)
|
||||
LOGGER.exception("Unknown error")
|
||||
else:
|
||||
if self._config_entry:
|
||||
if self._config_entry.unique_id != self.unique_id:
|
||||
|
@ -132,14 +214,96 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
step_id="cloud",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME, default=self._default_user): str,
|
||||
vol.Required(CONF_USERNAME, default=self._user): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_HUB, default=self._default_hub): vol.In(
|
||||
{key: hub.name for key, hub in SUPPORTED_SERVERS.items()}
|
||||
),
|
||||
}
|
||||
),
|
||||
description_placeholders=description_placeholders,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_local(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the local authentication step via config flow."""
|
||||
errors = {}
|
||||
description_placeholders = {}
|
||||
|
||||
if user_input:
|
||||
self._host = user_input[CONF_HOST]
|
||||
self._user = user_input[CONF_USERNAME]
|
||||
|
||||
# inherit the server from previous step
|
||||
user_input[CONF_HUB] = self._server
|
||||
|
||||
try:
|
||||
user_input = await self.async_validate_input(user_input)
|
||||
except TooManyRequestsException:
|
||||
errors["base"] = "too_many_requests"
|
||||
except BadCredentialsException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except ClientConnectorCertificateError as exception:
|
||||
errors["base"] = "certificate_verify_failed"
|
||||
LOGGER.debug(exception)
|
||||
except (TimeoutError, ClientError) as exception:
|
||||
errors["base"] = "cannot_connect"
|
||||
LOGGER.debug(exception)
|
||||
except MaintenanceException:
|
||||
errors["base"] = "server_in_maintenance"
|
||||
except TooManyAttemptsBannedException:
|
||||
errors["base"] = "too_many_attempts"
|
||||
except NotSuchTokenException:
|
||||
errors["base"] = "no_such_token"
|
||||
except DeveloperModeDisabled:
|
||||
errors["base"] = "developer_mode_disabled"
|
||||
except UnknownUserException:
|
||||
# Somfy Protect accounts are not supported since they don't use
|
||||
# the Overkiz API server. Login will return unknown user.
|
||||
description_placeholders["unsupported_device"] = "Somfy Protect"
|
||||
errors["base"] = "unsupported_hardware"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
errors["base"] = "unknown"
|
||||
LOGGER.exception("Unknown error")
|
||||
else:
|
||||
if self._config_entry:
|
||||
if self._config_entry.unique_id != self.unique_id:
|
||||
return self.async_abort(reason="reauth_wrong_account")
|
||||
|
||||
# Update existing entry during reauth
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self._config_entry,
|
||||
data={
|
||||
**self._config_entry.data,
|
||||
**user_input,
|
||||
},
|
||||
)
|
||||
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(
|
||||
self._config_entry.entry_id
|
||||
)
|
||||
)
|
||||
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
# Create new entry
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_HOST], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="local",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST, default=self._host): str,
|
||||
vol.Required(CONF_USERNAME, default=self._user): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_VERIFY_SSL, default=True): bool,
|
||||
}
|
||||
),
|
||||
description_placeholders=description_placeholders,
|
||||
|
@ -150,6 +314,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
"""Handle DHCP discovery."""
|
||||
hostname = discovery_info.hostname
|
||||
gateway_id = hostname[8:22]
|
||||
self._host = f"gateway-{gateway_id}.local:8443"
|
||||
|
||||
LOGGER.debug("DHCP discovery detected gateway %s", obfuscate_id(gateway_id))
|
||||
return await self._process_discovery(gateway_id)
|
||||
|
@ -160,8 +325,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
"""Handle ZeroConf discovery."""
|
||||
properties = discovery_info.properties
|
||||
gateway_id = properties["gateway_pin"]
|
||||
hostname = discovery_info.hostname
|
||||
|
||||
LOGGER.debug(
|
||||
"ZeroConf discovery detected gateway %s on %s (%s)",
|
||||
obfuscate_id(gateway_id),
|
||||
hostname,
|
||||
discovery_info.type,
|
||||
)
|
||||
|
||||
if discovery_info.type == "_kizbox._tcp.local.":
|
||||
self._host = f"gateway-{gateway_id}.local:8443"
|
||||
|
||||
if discovery_info.type == "_kizboxdev._tcp.local.":
|
||||
self._host = f"{discovery_info.hostname[:-1]}:{discovery_info.port}"
|
||||
self._api_type = APIType.LOCAL
|
||||
|
||||
LOGGER.debug("ZeroConf discovery detected gateway %s", obfuscate_id(gateway_id))
|
||||
return await self._process_discovery(gateway_id)
|
||||
|
||||
async def _process_discovery(self, gateway_id: str) -> FlowResult:
|
||||
|
@ -183,7 +362,63 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
"gateway_id": self._config_entry.unique_id
|
||||
}
|
||||
|
||||
self._default_user = self._config_entry.data[CONF_USERNAME]
|
||||
self._default_hub = self._config_entry.data[CONF_HUB]
|
||||
self._user = self._config_entry.data[CONF_USERNAME]
|
||||
self._server = self._config_entry.data[CONF_HUB]
|
||||
self._api_type = self._config_entry.data[CONF_API_TYPE]
|
||||
|
||||
if self._config_entry.data[CONF_API_TYPE] == APIType.LOCAL:
|
||||
self._host = self._config_entry.data[CONF_HOST]
|
||||
|
||||
return await self.async_step_user(dict(entry_data))
|
||||
|
||||
def _create_cloud_client(
|
||||
self, username: str, password: str, server: OverkizServer
|
||||
) -> OverkizClient:
|
||||
session = async_create_clientsession(self.hass)
|
||||
client = OverkizClient(
|
||||
username=username, password=password, server=server, session=session
|
||||
)
|
||||
|
||||
return client
|
||||
|
||||
async def _create_local_api_token(
|
||||
self, cloud_client: OverkizClient, host: str, verify_ssl: bool
|
||||
) -> str:
|
||||
"""Create local API token."""
|
||||
# Create session on Somfy cloud server to generate an access token for local API
|
||||
gateways = await cloud_client.get_gateways()
|
||||
|
||||
gateway_id = ""
|
||||
for gateway in gateways:
|
||||
# Overkiz can return multiple gateways, but we only can generate a token
|
||||
# for the main gateway.
|
||||
if is_overkiz_gateway(gateway.id):
|
||||
gateway_id = gateway.id
|
||||
|
||||
developer_mode = await cloud_client.get_setup_option(
|
||||
f"developerMode-{gateway_id}"
|
||||
)
|
||||
|
||||
if developer_mode is None:
|
||||
raise DeveloperModeDisabled
|
||||
|
||||
token = await cloud_client.generate_local_token(gateway_id)
|
||||
await cloud_client.activate_local_token(
|
||||
gateway_id=gateway_id, token=token, label="Home Assistant/local"
|
||||
)
|
||||
|
||||
session = async_create_clientsession(self.hass, verify_ssl=verify_ssl)
|
||||
|
||||
# Local API
|
||||
local_client = OverkizClient(
|
||||
username="",
|
||||
password="",
|
||||
token=token,
|
||||
session=session,
|
||||
server=generate_local_server(host=host),
|
||||
verify_ssl=verify_ssl,
|
||||
)
|
||||
|
||||
await local_client.login()
|
||||
|
||||
return token
|
||||
|
|
|
@ -5,7 +5,13 @@ from datetime import timedelta
|
|||
import logging
|
||||
from typing import Final
|
||||
|
||||
from pyoverkiz.enums import MeasuredValueType, OverkizCommandParam, UIClass, UIWidget
|
||||
from pyoverkiz.enums import (
|
||||
MeasuredValueType,
|
||||
OverkizCommandParam,
|
||||
Server,
|
||||
UIClass,
|
||||
UIWidget,
|
||||
)
|
||||
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
|
@ -31,8 +37,10 @@ from homeassistant.const import (
|
|||
DOMAIN: Final = "overkiz"
|
||||
LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
CONF_API_TYPE: Final = "api_type"
|
||||
CONF_HUB: Final = "hub"
|
||||
DEFAULT_HUB: Final = "somfy_europe"
|
||||
DEFAULT_SERVER: Final = Server.SOMFY_EUROPE
|
||||
DEFAULT_HOST: Final = "gateway-xxxx-xxxx-xxxx.local:8443"
|
||||
|
||||
UPDATE_INTERVAL: Final = timedelta(seconds=30)
|
||||
UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60)
|
||||
|
|
|
@ -6,7 +6,7 @@ from datetime import timedelta
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ServerDisconnectedError
|
||||
from aiohttp import ClientConnectorError, ServerDisconnectedError
|
||||
from pyoverkiz.client import OverkizClient
|
||||
from pyoverkiz.enums import EventName, ExecutionState, Protocol
|
||||
from pyoverkiz.exceptions import (
|
||||
|
@ -79,7 +79,7 @@ class OverkizDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Device]]):
|
|||
raise UpdateFailed("Server is down for maintenance.") from exception
|
||||
except InvalidEventListenerIdException as exception:
|
||||
raise UpdateFailed(exception) from exception
|
||||
except TimeoutError as exception:
|
||||
except (TimeoutError, ClientConnectorError) as exception:
|
||||
raise UpdateFailed("Failed to connect.") from exception
|
||||
except (ServerDisconnectedError, NotAuthenticatedException):
|
||||
self.executions = {}
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import Any
|
||||
|
||||
from pyoverkiz.enums import APIType
|
||||
from pyoverkiz.obfuscate import obfuscate_id
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
@ -10,7 +11,7 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from . import HomeAssistantOverkizData
|
||||
from .const import CONF_HUB, DOMAIN
|
||||
from .const import CONF_API_TYPE, CONF_HUB, DOMAIN
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
|
@ -23,11 +24,16 @@ async def async_get_config_entry_diagnostics(
|
|||
data = {
|
||||
"setup": await client.get_diagnostic_data(),
|
||||
"server": entry.data[CONF_HUB],
|
||||
"execution_history": [
|
||||
repr(execution) for execution in await client.get_execution_history()
|
||||
],
|
||||
"api_type": entry.data.get(CONF_API_TYPE, APIType.CLOUD),
|
||||
}
|
||||
|
||||
# Only Overkiz cloud servers expose an endpoint with execution history
|
||||
if client.api_type == APIType.CLOUD:
|
||||
execution_history = [
|
||||
repr(execution) for execution in await client.get_execution_history()
|
||||
]
|
||||
data["execution_history"] = execution_history
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
@ -49,11 +55,15 @@ async def async_get_device_diagnostics(
|
|||
},
|
||||
"setup": await client.get_diagnostic_data(),
|
||||
"server": entry.data[CONF_HUB],
|
||||
"execution_history": [
|
||||
"api_type": entry.data.get(CONF_API_TYPE, APIType.CLOUD),
|
||||
}
|
||||
|
||||
# Only Overkiz cloud servers expose an endpoint with execution history
|
||||
if client.api_type == APIType.CLOUD:
|
||||
data["execution_history"] = [
|
||||
repr(execution)
|
||||
for execution in await client.get_execution_history()
|
||||
if any(command.device_url == device_url for command in execution.commands)
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
return data
|
||||
|
|
|
@ -11,13 +11,17 @@
|
|||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/overkiz",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.13.3"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_kizbox._tcp.local.",
|
||||
"name": "gateway*"
|
||||
},
|
||||
{
|
||||
"type": "_kizboxdev._tcp.local.",
|
||||
"name": "gateway*"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -3,18 +3,40 @@
|
|||
"flow_title": "Gateway: {gateway_id}",
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "The Overkiz platform is used by various vendors like Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) and Atlantic (Cozytouch). Enter your application credentials and select your hub.",
|
||||
"description": "Select your server. The Overkiz platform is used by various vendors like Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo) and Atlantic (Cozytouch).",
|
||||
"data": {
|
||||
"hub": "Server"
|
||||
}
|
||||
},
|
||||
"local_or_cloud": {
|
||||
"description": "Choose between local or cloud API. Local API supports TaHoma Connexoon, TaHoma v2, and TaHoma Switch. Climate devices are not supported in local API.",
|
||||
"data": {
|
||||
"api_type": "API type"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Enter your application credentials.",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
},
|
||||
"local": {
|
||||
"description": "By activating the [Developer Mode of your TaHoma box](https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode#getting-started), you can authorize third-party software (like Home Assistant) to connect to it via your local network. \n\n After activation, enter your application credentials and change the host to include your gateway-pin or enter the IP address of your gateway.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"hub": "Hub"
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"certificate_verify_failed": "Cannot connect to host, certificate verify failed.",
|
||||
"developer_mode_disabled": "Developer Mode disabled. Activate the Developer Mode of your Somfy TaHoma box first.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"no_such_token": "Cannot create a token for this gateway. Please confirm if the account is linked to this gateway.",
|
||||
"server_in_maintenance": "Server is down for maintenance",
|
||||
"too_many_attempts": "Too many attempts with an invalid token, temporarily banned",
|
||||
"too_many_requests": "Too many requests, try again later",
|
||||
|
|
|
@ -4156,7 +4156,7 @@
|
|||
"name": "Overkiz",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"ovo_energy": {
|
||||
"name": "OVO Energy",
|
||||
|
|
|
@ -508,6 +508,12 @@ ZEROCONF = {
|
|||
"name": "gateway*",
|
||||
},
|
||||
],
|
||||
"_kizboxdev._tcp.local.": [
|
||||
{
|
||||
"domain": "overkiz",
|
||||
"name": "gateway*",
|
||||
},
|
||||
],
|
||||
"_lookin._tcp.local.": [
|
||||
{
|
||||
"domain": "lookin",
|
||||
|
|
|
@ -12,8 +12,8 @@ from tests.components.overkiz import load_setup_fixture
|
|||
from tests.components.overkiz.test_config_flow import (
|
||||
TEST_EMAIL,
|
||||
TEST_GATEWAY_ID,
|
||||
TEST_HUB,
|
||||
TEST_PASSWORD,
|
||||
TEST_SERVER,
|
||||
)
|
||||
|
||||
MOCK_SETUP_RESPONSE = Mock(devices=[], gateways=[])
|
||||
|
@ -26,7 +26,7 @@ def mock_config_entry() -> MockConfigEntry:
|
|||
title="Somfy TaHoma Switch",
|
||||
domain=DOMAIN,
|
||||
unique_id=TEST_GATEWAY_ID,
|
||||
data={"username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_HUB},
|
||||
data={"username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_SERVER},
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# serializer version: 1
|
||||
# name: test_device_diagnostics
|
||||
dict({
|
||||
'api_type': 'cloud',
|
||||
'device': dict({
|
||||
'controllable_name': 'rts:RollerShutterRTSComponent',
|
||||
'device_url': 'rts://****-****-6867/16756006',
|
||||
|
@ -969,6 +970,7 @@
|
|||
# ---
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'api_type': 'cloud',
|
||||
'execution_history': list([
|
||||
]),
|
||||
'server': 'somfy_europe',
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .test_config_flow import TEST_EMAIL, TEST_GATEWAY_ID, TEST_HUB, TEST_PASSWORD
|
||||
from .test_config_flow import TEST_EMAIL, TEST_GATEWAY_ID, TEST_PASSWORD, TEST_SERVER
|
||||
|
||||
from tests.common import MockConfigEntry, mock_registry
|
||||
|
||||
|
@ -23,7 +23,7 @@ async def test_unique_id_migration(hass: HomeAssistant) -> None:
|
|||
mock_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=TEST_GATEWAY_ID,
|
||||
data={"username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_HUB},
|
||||
data={"username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_SERVER},
|
||||
)
|
||||
|
||||
mock_entry.add_to_hass(hass)
|
||||
|
|
Loading…
Add table
Reference in a new issue