Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Lutron Homeworks Series 4 and 8 systems."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Mapping
7 from dataclasses import dataclass
8 import logging
9 from typing import Any
10 
11 from pyhomeworks import exceptions as hw_exceptions
12 from pyhomeworks.pyhomeworks import (
13  HW_BUTTON_PRESSED,
14  HW_BUTTON_RELEASED,
15  HW_LOGIN_INCORRECT,
16  Homeworks,
17 )
18 import voluptuous as vol
19 
20 from homeassistant.config_entries import ConfigEntry
21 from homeassistant.const import (
22  CONF_HOST,
23  CONF_ID,
24  CONF_NAME,
25  CONF_PASSWORD,
26  CONF_PORT,
27  CONF_USERNAME,
28  EVENT_HOMEASSISTANT_STOP,
29  Platform,
30 )
31 from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
32 from homeassistant.exceptions import ConfigEntryNotReady, ServiceValidationError
34 from homeassistant.helpers.debounce import Debouncer
35 from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
36 from homeassistant.helpers.typing import ConfigType
37 from homeassistant.util import slugify
38 
39 from .const import CONF_ADDR, CONF_CONTROLLER_ID, CONF_KEYPADS, DOMAIN
40 
41 _LOGGER = logging.getLogger(__name__)
42 
43 PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
44 
45 CONF_COMMAND = "command"
46 
47 EVENT_BUTTON_PRESS = "homeworks_button_press"
48 EVENT_BUTTON_RELEASE = "homeworks_button_release"
49 
50 KEYPAD_LEDSTATE_POLL_COOLDOWN = 1.0
51 
52 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
53 
54 SERVICE_SEND_COMMAND_SCHEMA = vol.Schema(
55  {
56  vol.Required(CONF_CONTROLLER_ID): str,
57  vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [str]),
58  }
59 )
60 
61 
62 @dataclass
64  """Container for config entry data."""
65 
66  controller: Homeworks
67  controller_id: str
68  keypads: dict[str, HomeworksKeypad]
69 
70 
71 @callback
72 def async_setup_services(hass: HomeAssistant) -> None:
73  """Set up services for Lutron Homeworks Series 4 and 8 integration."""
74 
75  async def async_call_service(service_call: ServiceCall) -> None:
76  """Call the service."""
77  await async_send_command(hass, service_call.data)
78 
79  hass.services.async_register(
80  DOMAIN,
81  "send_command",
82  async_call_service,
83  schema=SERVICE_SEND_COMMAND_SCHEMA,
84  )
85 
86 
87 async def async_send_command(hass: HomeAssistant, data: Mapping[str, Any]) -> None:
88  """Send command to a controller."""
89 
90  def get_controller_ids() -> list[str]:
91  """Get homeworks data for the specified controller ID."""
92  return [data.controller_id for data in hass.data[DOMAIN].values()]
93 
94  def get_homeworks_data(controller_id: str) -> HomeworksData | None:
95  """Get homeworks data for the specified controller ID."""
96  data: HomeworksData
97  for data in hass.data[DOMAIN].values():
98  if data.controller_id == controller_id:
99  return data
100  return None
101 
102  homeworks_data = get_homeworks_data(data[CONF_CONTROLLER_ID])
103  if not homeworks_data:
105  translation_domain=DOMAIN,
106  translation_key="invalid_controller_id",
107  translation_placeholders={
108  "controller_id": data[CONF_CONTROLLER_ID],
109  "controller_ids": ",".join(get_controller_ids()),
110  },
111  )
112 
113  commands = data[CONF_COMMAND]
114  _LOGGER.debug("Send commands: %s", commands)
115  for command in commands:
116  if command.lower().startswith("delay"):
117  delay = int(command.partition(" ")[2])
118  _LOGGER.debug("Sleeping for %s ms", delay)
119  await asyncio.sleep(delay / 1000)
120  else:
121  _LOGGER.debug("Sending command '%s'", command)
122  await hass.async_add_executor_job(
123  homeworks_data.controller._send, # noqa: SLF001
124  command,
125  )
126 
127 
128 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
129  """Start Homeworks controller."""
131 
132  return True
133 
134 
135 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
136  """Set up Homeworks from a config entry."""
137 
138  hass.data.setdefault(DOMAIN, {})
139  controller_id = entry.options[CONF_CONTROLLER_ID]
140 
141  def hw_callback(msg_type: Any, values: Any) -> None:
142  """Dispatch state changes."""
143  _LOGGER.debug("callback: %s, %s", msg_type, values)
144  if msg_type == HW_LOGIN_INCORRECT:
145  _LOGGER.debug("login incorrect")
146  return
147  addr = values[0]
148  signal = f"homeworks_entity_{controller_id}_{addr}"
149  dispatcher_send(hass, signal, msg_type, values)
150 
151  config = entry.options
152  controller = Homeworks(
153  config[CONF_HOST],
154  config[CONF_PORT],
155  hw_callback,
156  entry.data.get(CONF_USERNAME),
157  entry.data.get(CONF_PASSWORD),
158  )
159  try:
160  await hass.async_add_executor_job(controller.connect)
161  except hw_exceptions.HomeworksException as err:
162  _LOGGER.debug("Failed to connect: %s", err, exc_info=True)
163  raise ConfigEntryNotReady from err
164  controller.start()
165 
166  def cleanup(event: Event) -> None:
167  controller.stop()
168 
169  entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup))
170 
171  keypads: dict[str, HomeworksKeypad] = {}
172  for key_config in config.get(CONF_KEYPADS, []):
173  addr = key_config[CONF_ADDR]
174  name = key_config[CONF_NAME]
175  keypads[addr] = HomeworksKeypad(hass, controller, controller_id, addr, name)
176 
177  hass.data[DOMAIN][entry.entry_id] = HomeworksData(
178  controller, controller_id, keypads
179  )
180 
181  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
182  entry.async_on_unload(entry.add_update_listener(update_listener))
183 
184  return True
185 
186 
187 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
188  """Unload a config entry."""
189  if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
190  data: HomeworksData = hass.data[DOMAIN].pop(entry.entry_id)
191  for keypad in data.keypads.values():
192  keypad.unsubscribe()
193 
194  await hass.async_add_executor_job(data.controller.stop)
195 
196  return unload_ok
197 
198 
199 async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
200  """Handle options update."""
201  await hass.config_entries.async_reload(entry.entry_id)
202 
203 
205  """When you want signals instead of entities.
206 
207  Stateless sensors such as keypads are expected to generate an event
208  instead of a sensor entity in hass.
209  """
210 
211  def __init__(
212  self,
213  hass: HomeAssistant,
214  controller: Homeworks,
215  controller_id: str,
216  addr: str,
217  name: str,
218  ) -> None:
219  """Register callback that will be used for signals."""
220  self._addr_addr = addr
221  self._controller_controller = controller
222  self._debouncer_debouncer = Debouncer(
223  hass,
224  _LOGGER,
225  cooldown=KEYPAD_LEDSTATE_POLL_COOLDOWN,
226  immediate=False,
227  function=self._request_keypad_led_states_request_keypad_led_states,
228  )
229  self._hass_hass = hass
230  self._name_name = name
231  self._id_id = slugify(self._name_name)
232  signal = f"homeworks_entity_{controller_id}_{self._addr}"
233  _LOGGER.debug("connecting %s", signal)
235  self._hass_hass, signal, self._update_callback_update_callback
236  )
237 
238  @callback
239  def _update_callback(self, msg_type: str, values: list[Any]) -> None:
240  """Fire events if button is pressed or released."""
241 
242  if msg_type == HW_BUTTON_PRESSED:
243  event = EVENT_BUTTON_PRESS
244  elif msg_type == HW_BUTTON_RELEASED:
245  event = EVENT_BUTTON_RELEASE
246  else:
247  return
248  data = {CONF_ID: self._id_id, CONF_NAME: self._name_name, "button": values[1]}
249  self._hass_hass.bus.async_fire(event, data)
250 
251  def _request_keypad_led_states(self) -> None:
252  """Query keypad led state."""
253  self._controller_controller._send(f"RKLS, {self._addr}") # noqa: SLF001
254 
255  async def request_keypad_led_states(self) -> None:
256  """Query keypad led state.
257 
258  Debounced to not storm the controller during setup.
259  """
260  await self._debouncer_debouncer.async_call()
None _update_callback(self, str msg_type, list[Any] values)
Definition: __init__.py:239
None __init__(self, HomeAssistant hass, Homeworks controller, str controller_id, str addr, str name)
Definition: __init__.py:218
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:128
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:187
None async_setup_services(HomeAssistant hass)
Definition: __init__.py:72
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:135
None async_send_command(HomeAssistant hass, Mapping[str, Any] data)
Definition: __init__.py:87
None update_listener(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:199
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
None dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:137
str slugify(str|None text, *str separator="_")
Definition: __init__.py:41