Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Velbus devices."""
2 
3 from __future__ import annotations
4 
5 from contextlib import suppress
6 import logging
7 import os
8 import shutil
9 
10 from velbusaio.controller import Velbus
11 import voluptuous as vol
12 
13 from homeassistant.config_entries import ConfigEntry
14 from homeassistant.const import CONF_ADDRESS, CONF_PORT, Platform
15 from homeassistant.core import HomeAssistant, ServiceCall
16 from homeassistant.exceptions import PlatformNotReady
17 from homeassistant.helpers import config_validation as cv, device_registry as dr
18 from homeassistant.helpers.storage import STORAGE_DIR
19 
20 from .const import (
21  CONF_INTERFACE,
22  CONF_MEMO_TEXT,
23  DOMAIN,
24  SERVICE_CLEAR_CACHE,
25  SERVICE_SCAN,
26  SERVICE_SET_MEMO_TEXT,
27  SERVICE_SYNC,
28 )
29 
30 _LOGGER = logging.getLogger(__name__)
31 
32 PLATFORMS = [
33  Platform.BINARY_SENSOR,
34  Platform.BUTTON,
35  Platform.CLIMATE,
36  Platform.COVER,
37  Platform.LIGHT,
38  Platform.SELECT,
39  Platform.SENSOR,
40  Platform.SWITCH,
41 ]
42 
43 
45  controller: Velbus, hass: HomeAssistant, entry_id: str
46 ) -> None:
47  """Task to offload the long running connect."""
48  try:
49  await controller.connect()
50  except ConnectionError as ex:
51  raise PlatformNotReady(
52  f"Connection error while connecting to Velbus {entry_id}: {ex}"
53  ) from ex
54 
55 
56 def _migrate_device_identifiers(hass: HomeAssistant, entry_id: str) -> None:
57  """Migrate old device identifiers."""
58  dev_reg = dr.async_get(hass)
59  devices: list[dr.DeviceEntry] = dr.async_entries_for_config_entry(dev_reg, entry_id)
60  for device in devices:
61  old_identifier = list(next(iter(device.identifiers)))
62  if len(old_identifier) > 2:
63  new_identifier = {(old_identifier.pop(0), old_identifier.pop(0))}
64  _LOGGER.debug(
65  "migrate identifier '%s' to '%s'", device.identifiers, new_identifier
66  )
67  dev_reg.async_update_device(device.id, new_identifiers=new_identifier)
68 
69 
70 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
71  """Establish connection with velbus."""
72  hass.data.setdefault(DOMAIN, {})
73 
74  controller = Velbus(
75  entry.data[CONF_PORT],
76  cache_dir=hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}"),
77  )
78  hass.data[DOMAIN][entry.entry_id] = {}
79  hass.data[DOMAIN][entry.entry_id]["cntrl"] = controller
80  hass.data[DOMAIN][entry.entry_id]["tsk"] = hass.async_create_task(
81  velbus_connect_task(controller, hass, entry.entry_id)
82  )
83 
84  _migrate_device_identifiers(hass, entry.entry_id)
85 
86  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
87 
88  if hass.services.has_service(DOMAIN, SERVICE_SCAN):
89  return True
90 
91  def check_entry_id(interface: str) -> str:
92  for config_entry in hass.config_entries.async_entries(DOMAIN):
93  if "port" in config_entry.data and config_entry.data["port"] == interface:
94  return config_entry.entry_id
95  raise vol.Invalid(
96  "The interface provided is not defined as a port in a Velbus integration"
97  )
98 
99  async def scan(call: ServiceCall) -> None:
100  await hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"].scan()
101 
102  hass.services.async_register(
103  DOMAIN,
104  SERVICE_SCAN,
105  scan,
106  vol.Schema({vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id)}),
107  )
108 
109  async def syn_clock(call: ServiceCall) -> None:
110  await hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"].sync_clock()
111 
112  hass.services.async_register(
113  DOMAIN,
114  SERVICE_SYNC,
115  syn_clock,
116  vol.Schema({vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id)}),
117  )
118 
119  async def set_memo_text(call: ServiceCall) -> None:
120  """Handle Memo Text service call."""
121  memo_text = call.data[CONF_MEMO_TEXT]
122  await (
123  hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"]
124  .get_module(call.data[CONF_ADDRESS])
125  .set_memo_text(memo_text)
126  )
127 
128  hass.services.async_register(
129  DOMAIN,
130  SERVICE_SET_MEMO_TEXT,
131  set_memo_text,
132  vol.Schema(
133  {
134  vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id),
135  vol.Required(CONF_ADDRESS): vol.All(
136  vol.Coerce(int), vol.Range(min=0, max=255)
137  ),
138  vol.Optional(CONF_MEMO_TEXT, default=""): cv.string,
139  }
140  ),
141  )
142 
143  async def clear_cache(call: ServiceCall) -> None:
144  """Handle a clear cache service call."""
145  # clear the cache
146  with suppress(FileNotFoundError):
147  if call.data.get(CONF_ADDRESS):
148  await hass.async_add_executor_job(
149  os.unlink,
150  hass.config.path(
151  STORAGE_DIR,
152  f"velbuscache-{call.data[CONF_INTERFACE]}/{call.data[CONF_ADDRESS]}.p",
153  ),
154  )
155  else:
156  await hass.async_add_executor_job(
157  shutil.rmtree,
158  hass.config.path(
159  STORAGE_DIR, f"velbuscache-{call.data[CONF_INTERFACE]}/"
160  ),
161  )
162  # call a scan to repopulate
163  await scan(call)
164 
165  hass.services.async_register(
166  DOMAIN,
167  SERVICE_CLEAR_CACHE,
168  clear_cache,
169  vol.Schema(
170  {
171  vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id),
172  vol.Optional(CONF_ADDRESS): vol.All(
173  vol.Coerce(int), vol.Range(min=0, max=255)
174  ),
175  }
176  ),
177  )
178 
179  return True
180 
181 
182 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
183  """Unload (close) the velbus connection."""
184  unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
185  await hass.data[DOMAIN][entry.entry_id]["cntrl"].stop()
186  hass.data[DOMAIN].pop(entry.entry_id)
187  if not hass.data[DOMAIN]:
188  hass.data.pop(DOMAIN)
189  hass.services.async_remove(DOMAIN, SERVICE_SCAN)
190  hass.services.async_remove(DOMAIN, SERVICE_SYNC)
191  hass.services.async_remove(DOMAIN, SERVICE_SET_MEMO_TEXT)
192  hass.services.async_remove(DOMAIN, SERVICE_CLEAR_CACHE)
193  return unload_ok
194 
195 
196 async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
197  """Remove the velbus entry, so we also have to cleanup the cache dir."""
198  await hass.async_add_executor_job(
199  shutil.rmtree,
200  hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}"),
201  )
202 
203 
204 async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
205  """Migrate old entry."""
206  _LOGGER.debug("Migrating from version %s", config_entry.version)
207  cache_path = hass.config.path(STORAGE_DIR, f"velbuscache-{config_entry.entry_id}/")
208  if config_entry.version == 1:
209  # This is the config entry migration for adding the new program selection
210  # clean the velbusCache
211  if os.path.isdir(cache_path):
212  await hass.async_add_executor_job(shutil.rmtree, cache_path)
213  # set the new version
214  hass.config_entries.async_update_entry(config_entry, version=2)
215 
216  _LOGGER.debug("Migration to version %s successful", config_entry.version)
217  return True
bool async_migrate_entry(HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:204
None velbus_connect_task(Velbus controller, HomeAssistant hass, str entry_id)
Definition: __init__.py:46
None async_remove_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:196
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:182
None _migrate_device_identifiers(HomeAssistant hass, str entry_id)
Definition: __init__.py:56
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:70