core/homeassistant/components/overkiz/__init__.py

259 lines
8.5 KiB
Python

"""The Overkiz (by Somfy) integration."""
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
from aiohttp import ClientError
from pyoverkiz.client import OverkizClient
from pyoverkiz.const import SUPPORTED_SERVERS
from pyoverkiz.enums import APIType, OverkizState, UIClass, UIWidget
from pyoverkiz.exceptions import (
BadCredentialsException,
MaintenanceException,
NotSuchTokenException,
TooManyRequestsException,
)
from pyoverkiz.models import Device, OverkizServer, Scenario
from pyoverkiz.utils import generate_local_server
from homeassistant.config_entries import ConfigEntry
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,
OVERKIZ_DEVICE_TO_PLATFORM,
PLATFORMS,
UPDATE_INTERVAL,
UPDATE_INTERVAL_ALL_ASSUMED_STATE,
)
from .coordinator import OverkizDataUpdateCoordinator
@dataclass
class HomeAssistantOverkizData:
"""Overkiz data stored in the Home Assistant data object."""
coordinator: OverkizDataUpdateCoordinator
platforms: defaultdict[Platform, list[Device]]
scenarios: list[Scenario]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Overkiz from a config entry."""
client: OverkizClient | None = None
api_type = entry.data.get(CONF_API_TYPE, APIType.CLOUD)
# 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)
try:
await client.login()
setup = await client.get_setup()
# Local API does expose scenarios, but they are not functional.
# Tracked in https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode/issues/21
if api_type == APIType.CLOUD:
scenarios = await client.get_scenarios()
else:
scenarios = []
except (BadCredentialsException, NotSuchTokenException) as exception:
raise ConfigEntryAuthFailed("Invalid authentication") from exception
except TooManyRequestsException as exception:
raise ConfigEntryNotReady("Too many requests, try again later") from exception
except (TimeoutError, ClientError) as exception:
raise ConfigEntryNotReady("Failed to connect") from exception
except MaintenanceException as exception:
raise ConfigEntryNotReady("Server is down for maintenance") from exception
coordinator = OverkizDataUpdateCoordinator(
hass,
LOGGER,
name="device events",
client=client,
devices=setup.devices,
places=setup.root_place,
update_interval=UPDATE_INTERVAL,
config_entry_id=entry.entry_id,
)
await coordinator.async_config_entry_first_refresh()
if coordinator.is_stateless:
LOGGER.debug(
(
"All devices have an assumed state. Update interval has been reduced"
" to: %s"
),
UPDATE_INTERVAL_ALL_ASSUMED_STATE,
)
coordinator.update_interval = UPDATE_INTERVAL_ALL_ASSUMED_STATE
platforms: defaultdict[Platform, list[Device]] = defaultdict(list)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantOverkizData(
coordinator=coordinator, platforms=platforms, scenarios=scenarios
)
# Map Overkiz entities to Home Assistant platform
for device in coordinator.data.values():
LOGGER.debug(
(
"The following device has been retrieved. Report an issue if not"
" supported correctly (%s)"
),
device,
)
if platform := OVERKIZ_DEVICE_TO_PLATFORM.get(
device.widget
) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class):
platforms[platform].append(device)
device_registry = dr.async_get(hass)
for gateway in setup.gateways:
LOGGER.debug("Added gateway (%s)", gateway)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, gateway.id)},
model=gateway.sub_type.beautify_name if gateway.sub_type else None,
manufacturer=client.server.manufacturer,
name=gateway.type.beautify_name if gateway.type else gateway.id,
sw_version=gateway.connectivity.protocol_version,
configuration_url=client.server.configuration_url,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def _async_migrate_entries(
hass: HomeAssistant, config_entry: ConfigEntry
) -> bool:
"""Migrate old entries to new unique IDs."""
entity_registry = er.async_get(hass)
@callback
def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None:
# Python 3.11 treats (str, Enum) and StrEnum in a different way
# Since pyOverkiz switched to StrEnum, we need to rewrite the unique ids once to the new style
#
# io://xxxx-xxxx-xxxx/3541212-OverkizState.CORE_DISCRETE_RSSI_LEVEL -> io://xxxx-xxxx-xxxx/3541212-core:DiscreteRSSILevelState
# internal://xxxx-xxxx-xxxx/alarm/0-UIWidget.TSKALARM_CONTROLLER -> internal://xxxx-xxxx-xxxx/alarm/0-TSKAlarmController
# io://xxxx-xxxx-xxxx/xxxxxxx-UIClass.ON_OFF -> io://xxxx-xxxx-xxxx/xxxxxxx-OnOff
if (key := entry.unique_id.split("-")[-1]).startswith(
("OverkizState", "UIWidget", "UIClass")
):
state = key.split(".")[1]
new_key = ""
if key.startswith("UIClass"):
new_key = UIClass[state]
elif key.startswith("UIWidget"):
new_key = UIWidget[state]
else:
new_key = OverkizState[state]
new_unique_id = entry.unique_id.replace(key, new_key)
LOGGER.debug(
"Migrating entity '%s' unique_id from '%s' to '%s'",
entry.entity_id,
entry.unique_id,
new_unique_id,
)
if existing_entity_id := entity_registry.async_get_entity_id(
entry.domain, entry.platform, new_unique_id
):
LOGGER.debug(
"Cannot migrate to unique_id '%s', already exists for '%s'. Entity will be removed",
new_unique_id,
existing_entity_id,
)
entity_registry.async_remove(entry.entity_id)
return None
return {
"new_unique_id": new_unique_id,
}
return None
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
)