Home Assistant Unofficial Reference 2024.12.1
services.py
Go to the documentation of this file.
1 """KNX integration services."""
2 
3 from __future__ import annotations
4 
5 from functools import partial
6 import logging
7 from typing import TYPE_CHECKING
8 
9 import voluptuous as vol
10 from xknx.dpt import DPTArray, DPTBase, DPTBinary
11 from xknx.exceptions import ConversionError
12 from xknx.telegram import Telegram
13 from xknx.telegram.address import parse_device_group_address
14 from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite
15 
16 from homeassistant.const import CONF_TYPE, SERVICE_RELOAD
17 from homeassistant.core import HomeAssistant, ServiceCall, callback
18 from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
20 from homeassistant.helpers.service import async_register_admin_service
21 
22 from .const import (
23  DOMAIN,
24  KNX_ADDRESS,
25  KNX_MODULE_KEY,
26  SERVICE_KNX_ATTR_PAYLOAD,
27  SERVICE_KNX_ATTR_REMOVE,
28  SERVICE_KNX_ATTR_RESPONSE,
29  SERVICE_KNX_ATTR_TYPE,
30  SERVICE_KNX_EVENT_REGISTER,
31  SERVICE_KNX_EXPOSURE_REGISTER,
32  SERVICE_KNX_READ,
33  SERVICE_KNX_SEND,
34 )
35 from .expose import create_knx_exposure
36 from .schema import ExposeSchema, dpt_base_type_validator, ga_validator
37 
38 if TYPE_CHECKING:
39  from . import KNXModule
40 
41 _LOGGER = logging.getLogger(__name__)
42 
43 
44 @callback
45 def register_knx_services(hass: HomeAssistant) -> None:
46  """Register KNX integration services."""
47  hass.services.async_register(
48  DOMAIN,
49  SERVICE_KNX_SEND,
50  partial(service_send_to_knx_bus, hass),
51  schema=SERVICE_KNX_SEND_SCHEMA,
52  )
53 
54  hass.services.async_register(
55  DOMAIN,
56  SERVICE_KNX_READ,
57  partial(service_read_to_knx_bus, hass),
58  schema=SERVICE_KNX_READ_SCHEMA,
59  )
60 
62  hass,
63  DOMAIN,
64  SERVICE_KNX_EVENT_REGISTER,
65  partial(service_event_register_modify, hass),
66  schema=SERVICE_KNX_EVENT_REGISTER_SCHEMA,
67  )
68 
70  hass,
71  DOMAIN,
72  SERVICE_KNX_EXPOSURE_REGISTER,
73  partial(service_exposure_register_modify, hass),
74  schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA,
75  )
76 
78  hass,
79  DOMAIN,
80  SERVICE_RELOAD,
81  partial(service_reload_integration, hass),
82  )
83 
84 
85 @callback
86 def get_knx_module(hass: HomeAssistant) -> KNXModule:
87  """Return KNXModule instance."""
88  try:
89  return hass.data[KNX_MODULE_KEY]
90  except KeyError as err:
91  raise HomeAssistantError("KNX entry not loaded") from err
92 
93 
94 SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema(
95  {
96  vol.Required(KNX_ADDRESS): vol.All(
97  cv.ensure_list,
98  [ga_validator],
99  ),
100  vol.Optional(CONF_TYPE): dpt_base_type_validator,
101  vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean,
102  }
103 )
104 
105 
106 async def service_event_register_modify(hass: HomeAssistant, call: ServiceCall) -> None:
107  """Service for adding or removing a GroupAddress to the knx_event filter."""
108  knx_module = get_knx_module(hass)
109 
110  attr_address = call.data[KNX_ADDRESS]
111  group_addresses = list(map(parse_device_group_address, attr_address))
112 
113  if call.data.get(SERVICE_KNX_ATTR_REMOVE):
114  for group_address in group_addresses:
115  try:
116  knx_module.knx_event_callback.group_addresses.remove(group_address)
117  except ValueError:
118  _LOGGER.warning(
119  "Service event_register could not remove event for '%s'",
120  str(group_address),
121  )
122  if group_address in knx_module.group_address_transcoder:
123  del knx_module.group_address_transcoder[group_address]
124  return
125 
126  if (dpt := call.data.get(CONF_TYPE)) and (
127  transcoder := DPTBase.parse_transcoder(dpt)
128  ):
129  knx_module.group_address_transcoder.update(
130  {_address: transcoder for _address in group_addresses}
131  )
132  for group_address in group_addresses:
133  if group_address in knx_module.knx_event_callback.group_addresses:
134  continue
135  knx_module.knx_event_callback.group_addresses.append(group_address)
136  _LOGGER.debug(
137  "Service event_register registered event for '%s'",
138  str(group_address),
139  )
140 
141 
142 SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any(
143  ExposeSchema.EXPOSE_SENSOR_SCHEMA.extend(
144  {
145  vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean,
146  }
147  ),
148  vol.Schema(
149  # for removing only `address` is required
150  {
151  vol.Required(KNX_ADDRESS): ga_validator,
152  vol.Required(SERVICE_KNX_ATTR_REMOVE): vol.All(cv.boolean, True),
153  },
154  extra=vol.ALLOW_EXTRA,
155  ),
156 )
157 
158 
160  hass: HomeAssistant, call: ServiceCall
161 ) -> None:
162  """Service for adding or removing an exposure to KNX bus."""
163  knx_module = get_knx_module(hass)
164 
165  group_address = call.data[KNX_ADDRESS]
166 
167  if call.data.get(SERVICE_KNX_ATTR_REMOVE):
168  try:
169  removed_exposure = knx_module.service_exposures.pop(group_address)
170  except KeyError as err:
172  f"Could not find exposure for '{group_address}' to remove."
173  ) from err
174 
175  removed_exposure.async_remove()
176  return
177 
178  if group_address in knx_module.service_exposures:
179  replaced_exposure = knx_module.service_exposures.pop(group_address)
180  _LOGGER.warning(
181  (
182  "Service exposure_register replacing already registered exposure"
183  " for '%s' - %s"
184  ),
185  group_address,
186  replaced_exposure.device.name,
187  )
188  replaced_exposure.async_remove()
189  exposure = create_knx_exposure(knx_module.hass, knx_module.xknx, call.data)
190  knx_module.service_exposures[group_address] = exposure
191  _LOGGER.debug(
192  "Service exposure_register registered exposure for '%s' - %s",
193  group_address,
194  exposure.device.name,
195  )
196 
197 
198 SERVICE_KNX_SEND_SCHEMA = vol.Any(
199  vol.Schema(
200  {
201  vol.Required(KNX_ADDRESS): vol.All(
202  cv.ensure_list,
203  [ga_validator],
204  ),
205  vol.Required(SERVICE_KNX_ATTR_PAYLOAD): cv.match_all,
206  vol.Required(SERVICE_KNX_ATTR_TYPE): dpt_base_type_validator,
207  vol.Optional(SERVICE_KNX_ATTR_RESPONSE, default=False): cv.boolean,
208  }
209  ),
210  vol.Schema(
211  # without type given payload is treated as raw bytes
212  {
213  vol.Required(KNX_ADDRESS): vol.All(
214  cv.ensure_list,
215  [ga_validator],
216  ),
217  vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any(
218  cv.positive_int, [cv.positive_int]
219  ),
220  vol.Optional(SERVICE_KNX_ATTR_RESPONSE, default=False): cv.boolean,
221  }
222  ),
223 )
224 
225 
226 async def service_send_to_knx_bus(hass: HomeAssistant, call: ServiceCall) -> None:
227  """Service for sending an arbitrary KNX message to the KNX bus."""
228  knx_module = get_knx_module(hass)
229 
230  attr_address = call.data[KNX_ADDRESS]
231  attr_payload = call.data[SERVICE_KNX_ATTR_PAYLOAD]
232  attr_type = call.data.get(SERVICE_KNX_ATTR_TYPE)
233  attr_response = call.data[SERVICE_KNX_ATTR_RESPONSE]
234 
235  payload: DPTBinary | DPTArray
236  if attr_type is not None:
237  transcoder = DPTBase.parse_transcoder(attr_type)
238  if transcoder is None:
240  f"Invalid type for knx.send service: {attr_type}"
241  )
242  try:
243  payload = transcoder.to_knx(attr_payload)
244  except ConversionError as err:
246  f"Invalid payload for knx.send service: {err}"
247  ) from err
248  elif isinstance(attr_payload, int):
249  payload = DPTBinary(attr_payload)
250  else:
251  payload = DPTArray(attr_payload)
252 
253  for address in attr_address:
254  telegram = Telegram(
255  destination_address=parse_device_group_address(address),
256  payload=GroupValueResponse(payload)
257  if attr_response
258  else GroupValueWrite(payload),
259  source_address=knx_module.xknx.current_address,
260  )
261  await knx_module.xknx.telegrams.put(telegram)
262 
263 
264 SERVICE_KNX_READ_SCHEMA = vol.Schema(
265  {
266  vol.Required(KNX_ADDRESS): vol.All(
267  cv.ensure_list,
268  [ga_validator],
269  )
270  }
271 )
272 
273 
274 async def service_read_to_knx_bus(hass: HomeAssistant, call: ServiceCall) -> None:
275  """Service for sending a GroupValueRead telegram to the KNX bus."""
276  knx_module = get_knx_module(hass)
277 
278  for address in call.data[KNX_ADDRESS]:
279  telegram = Telegram(
280  destination_address=parse_device_group_address(address),
281  payload=GroupValueRead(),
282  source_address=knx_module.xknx.current_address,
283  )
284  await knx_module.xknx.telegrams.put(telegram)
285 
286 
287 async def service_reload_integration(hass: HomeAssistant, call: ServiceCall) -> None:
288  """Reload the integration."""
289  knx_module = get_knx_module(hass)
290  await hass.config_entries.async_reload(knx_module.entry.entry_id)
291  hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context)
KNXExposeSensor|KNXExposeTime create_knx_exposure(HomeAssistant hass, XKNX xknx, ConfigType config)
Definition: expose.py:43
None service_exposure_register_modify(HomeAssistant hass, ServiceCall call)
Definition: services.py:161
None service_reload_integration(HomeAssistant hass, ServiceCall call)
Definition: services.py:287
None service_event_register_modify(HomeAssistant hass, ServiceCall call)
Definition: services.py:106
KNXModule get_knx_module(HomeAssistant hass)
Definition: services.py:86
None service_send_to_knx_bus(HomeAssistant hass, ServiceCall call)
Definition: services.py:226
None register_knx_services(HomeAssistant hass)
Definition: services.py:45
None service_read_to_knx_bus(HomeAssistant hass, ServiceCall call)
Definition: services.py:274
None async_register_admin_service(HomeAssistant hass, str domain, str service, Callable[[ServiceCall], Awaitable[None]|None] service_func, VolSchemaType schema=vol.Schema({}, extra=vol.PREVENT_EXTRA))
Definition: service.py:1121