1 """Support KNX devices."""
3 from __future__
import annotations
7 from pathlib
import Path
8 from typing
import Final
10 import voluptuous
as vol
12 from xknx.core
import XknxConnectionState
13 from xknx.core.telegram_queue
import TelegramQueue
14 from xknx.dpt
import DPTBase
15 from xknx.exceptions
import ConversionError, CouldNotParseTelegram, XKNXException
16 from xknx.io
import ConnectionConfig, ConnectionType, SecureConfig
17 from xknx.telegram
import AddressFilter, Telegram
18 from xknx.telegram.address
import DeviceGroupAddress, GroupAddress, InternalGroupAddress
19 from xknx.telegram.apci
import GroupValueResponse, GroupValueWrite
27 EVENT_HOMEASSISTANT_STOP,
38 CONF_KNX_CONNECTION_TYPE,
40 CONF_KNX_INDIVIDUAL_ADDRESS,
41 CONF_KNX_KNXKEY_FILENAME,
42 CONF_KNX_KNXKEY_PASSWORD,
49 CONF_KNX_ROUTING_BACKBONE_KEY,
50 CONF_KNX_ROUTING_SECURE,
51 CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE,
52 CONF_KNX_SECURE_DEVICE_AUTHENTICATION,
53 CONF_KNX_SECURE_USER_ID,
54 CONF_KNX_SECURE_USER_PASSWORD,
55 CONF_KNX_STATE_UPDATER,
56 CONF_KNX_TELEGRAM_LOG_SIZE,
57 CONF_KNX_TUNNEL_ENDPOINT_IA,
59 CONF_KNX_TUNNELING_TCP,
60 CONF_KNX_TUNNELING_TCP_SECURE,
65 SUPPORTED_PLATFORMS_UI,
66 SUPPORTED_PLATFORMS_YAML,
69 from .device
import KNXInterfaceDevice
70 from .expose
import KNXExposeSensor, KNXExposeTime, create_knx_exposure
71 from .project
import STORAGE_KEY
as PROJECT_STORAGE_KEY, KNXProject
93 from .services
import register_knx_services
94 from .storage.config_store
import KNXConfigStore
95 from .telegrams
import STORAGE_KEY
as TELEGRAMS_STORAGE_KEY, Telegrams
96 from .websocket
import register_panel
98 _LOGGER = logging.getLogger(__name__)
100 _KNX_YAML_CONFIG: Final =
"knx_yaml_config"
102 CONFIG_SCHEMA = vol.Schema(
107 **EventSchema.SCHEMA,
108 **ExposeSchema.platform_node(),
109 **BinarySensorSchema.platform_node(),
110 **ButtonSchema.platform_node(),
111 **ClimateSchema.platform_node(),
112 **CoverSchema.platform_node(),
113 **DateSchema.platform_node(),
114 **DateTimeSchema.platform_node(),
115 **FanSchema.platform_node(),
116 **LightSchema.platform_node(),
117 **NotifySchema.platform_node(),
118 **NumberSchema.platform_node(),
119 **SceneSchema.platform_node(),
120 **SelectSchema.platform_node(),
121 **SensorSchema.platform_node(),
122 **SwitchSchema.platform_node(),
123 **TextSchema.platform_node(),
124 **TimeSchema.platform_node(),
125 **WeatherSchema.platform_node(),
130 extra=vol.ALLOW_EXTRA,
134 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
135 """Start the KNX integration."""
136 hass.data[DATA_HASS_CONFIG] = config
137 if (conf := config.get(DOMAIN))
is not None:
138 hass.data[_KNX_YAML_CONFIG] =
dict(conf)
145 """Load a config entry."""
148 config = hass.data.pop(_KNX_YAML_CONFIG,
None)
151 if not _conf
or DOMAIN
not in _conf:
155 config = _conf[DOMAIN]
157 knx_module =
KNXModule(hass, config, entry)
158 await knx_module.start()
159 except XKNXException
as ex:
160 raise ConfigEntryNotReady
from ex
162 hass.data[KNX_MODULE_KEY] = knx_module
164 if CONF_KNX_EXPOSE
in config:
165 for expose_config
in config[CONF_KNX_EXPOSE]:
166 knx_module.exposures.append(
169 configured_platforms_yaml = {
170 platform
for platform
in SUPPORTED_PLATFORMS_YAML
if platform
in config
172 await hass.config_entries.async_forward_entry_setups(
176 *SUPPORTED_PLATFORMS_UI,
177 *configured_platforms_yaml,
187 """Unloading the KNX platforms."""
188 knx_module = hass.data.get(KNX_MODULE_KEY)
193 for exposure
in knx_module.exposures:
194 exposure.async_remove()
196 configured_platforms_yaml = {
198 for platform
in SUPPORTED_PLATFORMS_YAML
199 if platform
in knx_module.config_yaml
201 unload_ok = await hass.config_entries.async_unload_platforms(
205 *SUPPORTED_PLATFORMS_UI,
206 *configured_platforms_yaml,
210 await knx_module.stop()
211 hass.data.pop(DOMAIN)
217 """Update a given config entry."""
218 await hass.config_entries.async_reload(entry.entry_id)
222 """Remove a config entry."""
224 def remove_files(storage_dir: Path, knxkeys_filename: str |
None) ->
None:
225 """Remove KNX files."""
226 if knxkeys_filename
is not None:
227 with contextlib.suppress(FileNotFoundError):
228 (storage_dir / knxkeys_filename).unlink()
229 with contextlib.suppress(FileNotFoundError):
230 (storage_dir / PROJECT_STORAGE_KEY).unlink()
231 with contextlib.suppress(FileNotFoundError):
232 (storage_dir / TELEGRAMS_STORAGE_KEY).unlink()
233 with contextlib.suppress(FileNotFoundError, OSError):
234 (storage_dir / DOMAIN).rmdir()
236 storage_dir = Path(hass.config.path(STORAGE_DIR))
237 knxkeys_filename = entry.data.get(CONF_KNX_KNXKEY_FILENAME)
238 await hass.async_add_executor_job(remove_files, storage_dir, knxkeys_filename)
242 hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
244 """Remove a config entry from a device."""
245 knx_module = hass.data[KNX_MODULE_KEY]
246 if not device_entry.identifiers.isdisjoint(
247 knx_module.interface_device.device_info[
"identifiers"]
251 for entity
in knx_module.config_store.get_entity_entries():
252 if entity.device_id == device_entry.id:
253 await knx_module.config_store.delete_entity(entity.entity_id)
258 """Representation of KNX Object."""
261 self, hass: HomeAssistant, config: ConfigType, entry: ConfigEntry
263 """Initialize KNX module."""
267 self.exposures: list[KNXExposeSensor | KNXExposeTime] = []
268 self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {}
271 self.
projectproject = KNXProject(hass=hass, entry=entry)
275 address_format=self.
projectproject.get_address_format(),
277 rate_limit=self.
entryentry.data[CONF_KNX_RATE_LIMIT],
278 state_updater=self.
entryentry.data[CONF_KNX_STATE_UPDATER],
280 self.
xknxxknx.connection_manager.register_connection_state_changed_cb(
287 log_size=entry.data.get(CONF_KNX_TELEGRAM_LOG_SIZE, TELEGRAM_LOG_DEFAULT),
290 hass=hass, entry=entry, xknx=self.
xknxxknx
293 self._address_filter_transcoder: dict[AddressFilter, type[DPTBase]] = {}
294 self.group_address_transcoder: dict[DeviceGroupAddress, type[DPTBase]] = {}
297 self.
entryentry.async_on_unload(
298 self.
hasshass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.
stopstop)
300 self.
entryentry.async_on_unload(self.
entryentry.add_update_listener(async_update_entry))
303 """Start XKNX object. Connect to tunneling or Routing device."""
304 await self.
projectproject.load_project(self.
xknxxknx)
306 await self.
telegramstelegrams.load_history()
309 async
def stop(self, event: Event |
None =
None) ->
None:
310 """Stop XKNX object. Disconnect from tunneling or Routing device."""
312 await self.
telegramstelegrams.save_history()
315 """Return the connection_config."""
316 _conn_type: str = self.
entryentry.data[CONF_KNX_CONNECTION_TYPE]
317 _knxkeys_file: str |
None = (
318 self.
hasshass.config.path(
320 self.
entryentry.data[CONF_KNX_KNXKEY_FILENAME],
322 if self.
entryentry.data.get(CONF_KNX_KNXKEY_FILENAME)
is not None
325 if _conn_type == CONF_KNX_ROUTING:
326 return ConnectionConfig(
327 connection_type=ConnectionType.ROUTING,
328 individual_address=self.
entryentry.data[CONF_KNX_INDIVIDUAL_ADDRESS],
329 multicast_group=self.
entryentry.data[CONF_KNX_MCAST_GRP],
330 multicast_port=self.
entryentry.data[CONF_KNX_MCAST_PORT],
331 local_ip=self.
entryentry.data.get(CONF_KNX_LOCAL_IP),
333 secure_config=SecureConfig(
334 knxkeys_password=self.
entryentry.data.get(CONF_KNX_KNXKEY_PASSWORD),
335 knxkeys_file_path=_knxkeys_file,
339 if _conn_type == CONF_KNX_TUNNELING:
340 return ConnectionConfig(
341 connection_type=ConnectionType.TUNNELING,
342 gateway_ip=self.
entryentry.data[CONF_HOST],
343 gateway_port=self.
entryentry.data[CONF_PORT],
344 local_ip=self.
entryentry.data.get(CONF_KNX_LOCAL_IP),
345 route_back=self.
entryentry.data.get(CONF_KNX_ROUTE_BACK,
False),
347 secure_config=SecureConfig(
348 knxkeys_password=self.
entryentry.data.get(CONF_KNX_KNXKEY_PASSWORD),
349 knxkeys_file_path=_knxkeys_file,
353 if _conn_type == CONF_KNX_TUNNELING_TCP:
354 return ConnectionConfig(
355 connection_type=ConnectionType.TUNNELING_TCP,
356 individual_address=self.
entryentry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
357 gateway_ip=self.
entryentry.data[CONF_HOST],
358 gateway_port=self.
entryentry.data[CONF_PORT],
360 secure_config=SecureConfig(
361 knxkeys_password=self.
entryentry.data.get(CONF_KNX_KNXKEY_PASSWORD),
362 knxkeys_file_path=_knxkeys_file,
366 if _conn_type == CONF_KNX_TUNNELING_TCP_SECURE:
367 return ConnectionConfig(
368 connection_type=ConnectionType.TUNNELING_TCP_SECURE,
369 individual_address=self.
entryentry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
370 gateway_ip=self.
entryentry.data[CONF_HOST],
371 gateway_port=self.
entryentry.data[CONF_PORT],
372 secure_config=SecureConfig(
373 user_id=self.
entryentry.data.get(CONF_KNX_SECURE_USER_ID),
374 user_password=self.
entryentry.data.get(CONF_KNX_SECURE_USER_PASSWORD),
375 device_authentication_password=self.
entryentry.data.get(
376 CONF_KNX_SECURE_DEVICE_AUTHENTICATION
378 knxkeys_password=self.
entryentry.data.get(CONF_KNX_KNXKEY_PASSWORD),
379 knxkeys_file_path=_knxkeys_file,
384 if _conn_type == CONF_KNX_ROUTING_SECURE:
385 return ConnectionConfig(
386 connection_type=ConnectionType.ROUTING_SECURE,
387 individual_address=self.
entryentry.data[CONF_KNX_INDIVIDUAL_ADDRESS],
388 multicast_group=self.
entryentry.data[CONF_KNX_MCAST_GRP],
389 multicast_port=self.
entryentry.data[CONF_KNX_MCAST_PORT],
390 local_ip=self.
entryentry.data.get(CONF_KNX_LOCAL_IP),
391 secure_config=SecureConfig(
392 backbone_key=self.
entryentry.data.get(CONF_KNX_ROUTING_BACKBONE_KEY),
393 latency_ms=self.
entryentry.data.get(
394 CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE
396 knxkeys_password=self.
entryentry.data.get(CONF_KNX_KNXKEY_PASSWORD),
397 knxkeys_file_path=_knxkeys_file,
402 return ConnectionConfig(
404 secure_config=SecureConfig(
405 knxkeys_password=self.
entryentry.data.get(CONF_KNX_KNXKEY_PASSWORD),
406 knxkeys_file_path=_knxkeys_file,
412 """Call invoked after a KNX connection state change was received."""
413 self.
connectedconnected = state == XknxConnectionState.CONNECTED
414 for device
in self.
xknxxknx.devices:
415 device.after_update()
418 """Call invoked after a KNX telegram was received."""
420 data: int | tuple[int, ...] |
None =
None
423 isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse))
424 and telegram.payload.value
is not None
426 telegram.destination_address, (GroupAddress, InternalGroupAddress)
429 data = telegram.payload.value.value
431 self.group_address_transcoder.
get(telegram.destination_address)
435 for _filter, _transcoder
in self._address_filter_transcoder.items()
436 if _filter.match(telegram.destination_address)
442 value = transcoder.from_knx(telegram.payload.value)
443 except (ConversionError, CouldNotParseTelegram)
as err:
446 "Error in `knx_event` at decoding type '%s' from"
454 self.
hasshass.bus.async_fire(
458 "destination":
str(telegram.destination_address),
459 "direction": telegram.direction.value,
461 "source":
str(telegram.source_address),
462 "telegramtype": telegram.payload.__class__.__name__,
467 """Register callback for knx_event within XKNX TelegramQueue."""
469 for filter_set
in self.
config_yamlconfig_yaml[CONF_EVENT]:
470 _filters =
list(map(AddressFilter, filter_set[KNX_ADDRESS]))
471 address_filters.extend(_filters)
472 if (dpt := filter_set.get(CONF_TYPE))
and (
473 transcoder := DPTBase.parse_transcoder(dpt)
475 self._address_filter_transcoder.
update(
476 {_filter: transcoder
for _filter
in _filters}
479 return self.
xknxxknx.telegram_queue.register_telegram_received_cb(
481 address_filters=address_filters,
483 match_for_outgoing=
True,
None stop(self, Event|None event=None)
TelegramQueue.Callback register_event_callback(self)
None connection_state_changed_cb(self, XknxConnectionState state)
None __init__(self, HomeAssistant hass, ConfigType config, ConfigEntry entry)
None telegram_received_cb(self, Telegram telegram)
ConnectionConfig connection_config(self)
web.Response get(self, web.Request request, str config_key)
IssData update(pyiss.ISS iss)
KNXExposeSensor|KNXExposeTime create_knx_exposure(HomeAssistant hass, XKNX xknx, ConfigType config)
None register_knx_services(HomeAssistant hass)
None register_panel(HomeAssistant hass)
None async_update_entry(HomeAssistant hass, ConfigEntry entry)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
None async_remove_entry(HomeAssistant hass, ConfigEntry entry)
bool async_remove_config_entry_device(HomeAssistant hass, ConfigEntry config_entry, DeviceEntry device_entry)
bool async_setup(HomeAssistant hass, ConfigType config)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
def load_data(hass, url=None, filepath=None, username=None, password=None, authentication=None, num_retries=5, verify_ssl=None)
ConfigType|None async_integration_yaml_config(HomeAssistant hass, str integration_name)