"""Support for Velbus devices.""" from __future__ import annotations import logging from velbusaio.channels import Channel as VelbusChannel from velbusaio.controller import Velbus import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_PORT, Platform from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( CONF_INTERFACE, CONF_MEMO_TEXT, DOMAIN, SERVICE_SCAN, SERVICE_SET_MEMO_TEXT, SERVICE_SYNC, ) _LOGGER = logging.getLogger(__name__) PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.COVER, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH, ] async def velbus_connect_task( controller: Velbus, hass: HomeAssistant, entry_id: str ) -> None: """Task to offload the long running connect.""" await controller.connect() def _migrate_device_identifiers(hass: HomeAssistant, entry_id: str) -> None: """Migrate old device indentifiers.""" dev_reg = device_registry.async_get(hass) devices: list[DeviceEntry] = device_registry.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_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Establish connection with velbus.""" hass.data.setdefault(DOMAIN, {}) controller = Velbus( entry.data[CONF_PORT], cache_dir=hass.config.path(".storage/velbuscache/"), ) hass.data[DOMAIN][entry.entry_id] = {} hass.data[DOMAIN][entry.entry_id]["cntrl"] = controller hass.data[DOMAIN][entry.entry_id]["tsk"] = hass.async_create_task( velbus_connect_task(controller, hass, entry.entry_id) ) _migrate_device_identifiers(hass, entry.entry_id) hass.config_entries.async_setup_platforms(entry, PLATFORMS) if hass.services.has_service(DOMAIN, SERVICE_SCAN): return True def check_entry_id(interface: str) -> str: for entry in hass.config_entries.async_entries(DOMAIN): if "port" in entry.data and entry.data["port"] == interface: return entry.entry_id raise vol.Invalid( "The interface provided is not defined as a port in a Velbus integration" ) async def scan(call: ServiceCall) -> None: await hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"].scan() hass.services.async_register( DOMAIN, SERVICE_SCAN, scan, vol.Schema({vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id)}), ) async def syn_clock(call: ServiceCall) -> None: await hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"].sync_clock() hass.services.async_register( DOMAIN, SERVICE_SYNC, syn_clock, vol.Schema({vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id)}), ) async def set_memo_text(call: ServiceCall) -> None: """Handle Memo Text service call.""" memo_text = call.data[CONF_MEMO_TEXT] memo_text.hass = hass await hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"].get_module( call.data[CONF_ADDRESS] ).set_memo_text(memo_text.async_render()) hass.services.async_register( DOMAIN, SERVICE_SET_MEMO_TEXT, set_memo_text, vol.Schema( { vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id), vol.Required(CONF_ADDRESS): vol.All( vol.Coerce(int), vol.Range(min=0, max=255) ), vol.Optional(CONF_MEMO_TEXT, default=""): cv.template, } ), ) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Remove the velbus connection.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) await hass.data[DOMAIN][entry.entry_id]["cntrl"].stop() hass.data[DOMAIN].pop(entry.entry_id) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) hass.services.async_remove(DOMAIN, SERVICE_SCAN) hass.services.async_remove(DOMAIN, SERVICE_SYNC) hass.services.async_remove(DOMAIN, SERVICE_SET_MEMO_TEXT) return unload_ok class VelbusEntity(Entity): """Representation of a Velbus entity.""" _attr_should_poll: bool = False def __init__(self, channel: VelbusChannel) -> None: """Initialize a Velbus entity.""" self._channel = channel self._attr_name = channel.get_name() self._attr_device_info = DeviceInfo( identifiers={ (DOMAIN, str(channel.get_module_address())), }, manufacturer="Velleman", model=channel.get_module_type_name(), name=channel.get_full_name(), sw_version=channel.get_module_sw_version(), ) serial = channel.get_module_serial() or str(channel.get_module_address()) self._attr_unique_id = f"{serial}-{channel.get_channel_number()}" async def async_added_to_hass(self) -> None: """Add listener for state changes.""" self._channel.on_status_update(self._on_update) async def _on_update(self) -> None: self.async_write_ha_state()