core/homeassistant/components/velbus/__init__.py

174 lines
5.6 KiB
Python

"""Support for Velbus devices."""
from __future__ import annotations
import asyncio
from dataclasses import dataclass
import logging
import os
import shutil
from velbusaio.controller import Velbus
from velbusaio.exceptions import VelbusConnectionFailed
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, PlatformNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type VelbusConfigEntry = ConfigEntry[VelbusData]
@dataclass
class VelbusData:
"""Runtime data for the Velbus config entry."""
controller: Velbus
scan_task: asyncio.Task
async def velbus_scan_task(
controller: Velbus, hass: HomeAssistant, entry_id: str
) -> None:
"""Task to offload the long running scan."""
try:
await controller.start()
except ConnectionError as ex:
raise PlatformNotReady(
f"Connection error while connecting to Velbus {entry_id}: {ex}"
) from ex
# create all modules
dev_reg = dr.async_get(hass)
for module in controller.get_modules().values():
dev_reg.async_get_or_create(
config_entry_id=entry_id,
identifiers={
(DOMAIN, str(module.get_addresses()[0])),
},
manufacturer="Velleman",
model=module.get_type_name(),
model_id=str(module.get_type()),
name=f"{module.get_name()} ({module.get_type_name()})",
sw_version=module.get_sw_version(),
serial_number=module.get_serial(),
)
def _migrate_device_identifiers(hass: HomeAssistant, entry_id: str) -> None:
"""Migrate old device identifiers."""
dev_reg = dr.async_get(hass)
devices: list[dr.DeviceEntry] = dr.async_entries_for_config_entry(dev_reg, entry_id)
for device in devices:
old_identifier = list(next(iter(device.identifiers)))
if len(old_identifier) > 2:
new_identifier = {(old_identifier.pop(0), old_identifier.pop(0))}
_LOGGER.debug(
"migrate identifier '%s' to '%s'", device.identifiers, new_identifier
)
dev_reg.async_update_device(device.id, new_identifiers=new_identifier)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the actions for the Velbus component."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: VelbusConfigEntry) -> bool:
"""Establish connection with velbus."""
controller = Velbus(
entry.data[CONF_PORT],
cache_dir=hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}"),
)
try:
await controller.connect()
except VelbusConnectionFailed as error:
raise ConfigEntryNotReady("Cannot connect to Velbus") from error
task = hass.async_create_task(velbus_scan_task(controller, hass, entry.entry_id))
entry.runtime_data = VelbusData(controller=controller, scan_task=task)
_migrate_device_identifiers(hass, entry.entry_id)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: VelbusConfigEntry) -> bool:
"""Unload (close) the velbus connection."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
await entry.runtime_data.controller.stop()
return unload_ok
async def async_remove_entry(hass: HomeAssistant, entry: VelbusConfigEntry) -> None:
"""Remove the velbus entry, so we also have to cleanup the cache dir."""
await hass.async_add_executor_job(
shutil.rmtree,
hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}"),
)
async def async_migrate_entry(
hass: HomeAssistant, config_entry: VelbusConfigEntry
) -> bool:
"""Migrate old entry."""
_LOGGER.error(
"Migrating from version %s.%s", config_entry.version, config_entry.minor_version
)
# This is the config entry migration for adding the new program selection
# migrate from 1.x to 2.1
if config_entry.version < 2:
# clean the velbusCache
cache_path = hass.config.path(
STORAGE_DIR, f"velbuscache-{config_entry.entry_id}/"
)
if os.path.isdir(cache_path):
await hass.async_add_executor_job(shutil.rmtree, cache_path)
# This is the config entry migration for swapping the usb unique id to the serial number
# migrate from 2.1 to 2.2
if (
config_entry.version < 3
and config_entry.minor_version == 1
and config_entry.unique_id is not None
):
# not all velbus devices have a unique id, so handle this correctly
parts = config_entry.unique_id.split("_")
# old one should have 4 item
if len(parts) == 4:
hass.config_entries.async_update_entry(config_entry, unique_id=parts[1])
# update the config entry
hass.config_entries.async_update_entry(config_entry, version=2, minor_version=2)
_LOGGER.error(
"Migration to version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True