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:
Mick Vleeshouwer 2023-11-22 16:53:17 +01:00 committed by GitHub
parent 01c49ba0e4
commit 75f237b587
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1130 additions and 233 deletions

View file

@ -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
)

View file

@ -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

View file

@ -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)

View file

@ -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 = {}

View file

@ -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

View file

@ -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*"
}
]
}

View file

@ -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",

View file

@ -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",

View file

@ -508,6 +508,12 @@ ZEROCONF = {
"name": "gateway*",
},
],
"_kizboxdev._tcp.local.": [
{
"domain": "overkiz",
"name": "gateway*",
},
],
"_lookin._tcp.local.": [
{
"domain": "lookin",

View file

@ -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},
)

View file

@ -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

View file

@ -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)