Skip to content

Commit

Permalink
Move KNX service registration to async_setup (home-assistant#106635)
Browse files Browse the repository at this point in the history
  • Loading branch information
farmio authored Jan 7, 2024
1 parent 426a151 commit f53109f
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 237 deletions.
248 changes: 11 additions & 237 deletions homeassistant/components/knx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,17 @@
import contextlib
import logging
from pathlib import Path
from typing import Final

import voluptuous as vol
from xknx import XKNX
from xknx.core import XknxConnectionState
from xknx.core.telegram_queue import TelegramQueue
from xknx.dpt import DPTArray, DPTBase, DPTBinary
from xknx.dpt import DPTBase
from xknx.exceptions import ConversionError, CouldNotParseTelegram, XKNXException
from xknx.io import ConnectionConfig, ConnectionType, SecureConfig
from xknx.telegram import AddressFilter, Telegram
from xknx.telegram.address import (
DeviceGroupAddress,
GroupAddress,
InternalGroupAddress,
parse_device_group_address,
)
from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite
from xknx.telegram.address import DeviceGroupAddress, GroupAddress, InternalGroupAddress
from xknx.telegram.apci import GroupValueResponse, GroupValueWrite

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
Expand All @@ -30,15 +24,13 @@
CONF_PORT,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD,
Platform,
)
from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType

Expand Down Expand Up @@ -95,24 +87,14 @@
TextSchema,
TimeSchema,
WeatherSchema,
ga_validator,
sensor_type_validator,
)
from .services import register_knx_services
from .telegrams import STORAGE_KEY as TELEGRAMS_STORAGE_KEY, Telegrams
from .websocket import register_panel

_LOGGER = logging.getLogger(__name__)


SERVICE_KNX_SEND: Final = "send"
SERVICE_KNX_ATTR_PAYLOAD: Final = "payload"
SERVICE_KNX_ATTR_TYPE: Final = "type"
SERVICE_KNX_ATTR_RESPONSE: Final = "response"
SERVICE_KNX_ATTR_REMOVE: Final = "remove"
SERVICE_KNX_EVENT_REGISTER: Final = "event_register"
SERVICE_KNX_EXPOSURE_REGISTER: Final = "exposure_register"
SERVICE_KNX_READ: Final = "read"

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
Expand Down Expand Up @@ -158,69 +140,6 @@
extra=vol.ALLOW_EXTRA,
)

SERVICE_KNX_SEND_SCHEMA = vol.Any(
vol.Schema(
{
vol.Required(KNX_ADDRESS): vol.All(
cv.ensure_list,
[ga_validator],
),
vol.Required(SERVICE_KNX_ATTR_PAYLOAD): cv.match_all,
vol.Required(SERVICE_KNX_ATTR_TYPE): sensor_type_validator,
vol.Optional(SERVICE_KNX_ATTR_RESPONSE, default=False): cv.boolean,
}
),
vol.Schema(
# without type given payload is treated as raw bytes
{
vol.Required(KNX_ADDRESS): vol.All(
cv.ensure_list,
[ga_validator],
),
vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(SERVICE_KNX_ATTR_RESPONSE, default=False): cv.boolean,
}
),
)

SERVICE_KNX_READ_SCHEMA = vol.Schema(
{
vol.Required(KNX_ADDRESS): vol.All(
cv.ensure_list,
[ga_validator],
)
}
)

SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema(
{
vol.Required(KNX_ADDRESS): vol.All(
cv.ensure_list,
[ga_validator],
),
vol.Optional(CONF_TYPE): sensor_type_validator,
vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean,
}
)

SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any(
ExposeSchema.EXPOSE_SENSOR_SCHEMA.extend(
{
vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean,
}
),
vol.Schema(
# for removing only `address` is required
{
vol.Required(KNX_ADDRESS): ga_validator,
vol.Required(SERVICE_KNX_ATTR_REMOVE): vol.All(cv.boolean, True),
},
extra=vol.ALLOW_EXTRA,
),
)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Start the KNX integration."""
Expand All @@ -235,6 +154,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
conf = dict(conf)
hass.data[DATA_KNX_CONFIG] = conf

register_knx_services(hass)

return True


Expand Down Expand Up @@ -287,43 +208,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
)

hass.services.async_register(
DOMAIN,
SERVICE_KNX_SEND,
knx_module.service_send_to_knx_bus,
schema=SERVICE_KNX_SEND_SCHEMA,
)

hass.services.async_register(
DOMAIN,
SERVICE_KNX_READ,
knx_module.service_read_to_knx_bus,
schema=SERVICE_KNX_READ_SCHEMA,
)

async_register_admin_service(
hass,
DOMAIN,
SERVICE_KNX_EVENT_REGISTER,
knx_module.service_event_register_modify,
schema=SERVICE_KNX_EVENT_REGISTER_SCHEMA,
)

async_register_admin_service(
hass,
DOMAIN,
SERVICE_KNX_EXPOSURE_REGISTER,
knx_module.service_exposure_register_modify,
schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA,
)

async def _reload_integration(call: ServiceCall) -> None:
"""Reload the integration."""
await hass.config_entries.async_reload(entry.entry_id)
hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context)

async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_integration)

await register_panel(hass)

return True
Expand Down Expand Up @@ -419,10 +303,8 @@ def __init__(
)

self._address_filter_transcoder: dict[AddressFilter, type[DPTBase]] = {}
self._group_address_transcoder: dict[DeviceGroupAddress, type[DPTBase]] = {}
self._knx_event_callback: TelegramQueue.Callback = (
self.register_event_callback()
)
self.group_address_transcoder: dict[DeviceGroupAddress, type[DPTBase]] = {}
self.knx_event_callback: TelegramQueue.Callback = self.register_event_callback()

self.entry.async_on_unload(
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
Expand Down Expand Up @@ -555,7 +437,7 @@ async def telegram_received_cb(self, telegram: Telegram) -> None:
):
data = telegram.payload.value.value
if transcoder := (
self._group_address_transcoder.get(telegram.destination_address)
self.group_address_transcoder.get(telegram.destination_address)
or next(
(
_transcoder
Expand Down Expand Up @@ -612,111 +494,3 @@ def register_event_callback(self) -> TelegramQueue.Callback:
group_addresses=[],
match_for_outgoing=True,
)

async def service_event_register_modify(self, call: ServiceCall) -> None:
"""Service for adding or removing a GroupAddress to the knx_event filter."""
attr_address = call.data[KNX_ADDRESS]
group_addresses = list(map(parse_device_group_address, attr_address))

if call.data.get(SERVICE_KNX_ATTR_REMOVE):
for group_address in group_addresses:
try:
self._knx_event_callback.group_addresses.remove(group_address)
except ValueError:
_LOGGER.warning(
"Service event_register could not remove event for '%s'",
str(group_address),
)
if group_address in self._group_address_transcoder:
del self._group_address_transcoder[group_address]
return

if (dpt := call.data.get(CONF_TYPE)) and (
transcoder := DPTBase.parse_transcoder(dpt)
):
self._group_address_transcoder.update(
{
_address: transcoder # type: ignore[type-abstract]
for _address in group_addresses
}
)
for group_address in group_addresses:
if group_address in self._knx_event_callback.group_addresses:
continue
self._knx_event_callback.group_addresses.append(group_address)
_LOGGER.debug(
"Service event_register registered event for '%s'",
str(group_address),
)

async def service_exposure_register_modify(self, call: ServiceCall) -> None:
"""Service for adding or removing an exposure to KNX bus."""
group_address = call.data[KNX_ADDRESS]

if call.data.get(SERVICE_KNX_ATTR_REMOVE):
try:
removed_exposure = self.service_exposures.pop(group_address)
except KeyError as err:
raise HomeAssistantError(
f"Could not find exposure for '{group_address}' to remove."
) from err

removed_exposure.shutdown()
return

if group_address in self.service_exposures:
replaced_exposure = self.service_exposures.pop(group_address)
_LOGGER.warning(
(
"Service exposure_register replacing already registered exposure"
" for '%s' - %s"
),
group_address,
replaced_exposure.device.name,
)
replaced_exposure.shutdown()
exposure = create_knx_exposure(self.hass, self.xknx, call.data)
self.service_exposures[group_address] = exposure
_LOGGER.debug(
"Service exposure_register registered exposure for '%s' - %s",
group_address,
exposure.device.name,
)

async def service_send_to_knx_bus(self, call: ServiceCall) -> None:
"""Service for sending an arbitrary KNX message to the KNX bus."""
attr_address = call.data[KNX_ADDRESS]
attr_payload = call.data[SERVICE_KNX_ATTR_PAYLOAD]
attr_type = call.data.get(SERVICE_KNX_ATTR_TYPE)
attr_response = call.data[SERVICE_KNX_ATTR_RESPONSE]

payload: DPTBinary | DPTArray
if attr_type is not None:
transcoder = DPTBase.parse_transcoder(attr_type)
if transcoder is None:
raise ValueError(f"Invalid type for knx.send service: {attr_type}")
payload = transcoder.to_knx(attr_payload)
elif isinstance(attr_payload, int):
payload = DPTBinary(attr_payload)
else:
payload = DPTArray(attr_payload)

for address in attr_address:
telegram = Telegram(
destination_address=parse_device_group_address(address),
payload=GroupValueResponse(payload)
if attr_response
else GroupValueWrite(payload),
source_address=self.xknx.current_address,
)
await self.xknx.telegrams.put(telegram)

async def service_read_to_knx_bus(self, call: ServiceCall) -> None:
"""Service for sending a GroupValueRead telegram to the KNX bus."""
for address in call.data[KNX_ADDRESS]:
telegram = Telegram(
destination_address=parse_device_group_address(address),
payload=GroupValueRead(),
source_address=self.xknx.current_address,
)
await self.xknx.telegrams.put(telegram)
9 changes: 9 additions & 0 deletions homeassistant/components/knx/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@
AsyncMessageCallbackType = Callable[[Telegram], Awaitable[None]]
MessageCallbackType = Callable[[Telegram], None]

SERVICE_KNX_SEND: Final = "send"
SERVICE_KNX_ATTR_PAYLOAD: Final = "payload"
SERVICE_KNX_ATTR_TYPE: Final = "type"
SERVICE_KNX_ATTR_RESPONSE: Final = "response"
SERVICE_KNX_ATTR_REMOVE: Final = "remove"
SERVICE_KNX_EVENT_REGISTER: Final = "event_register"
SERVICE_KNX_EXPOSURE_REGISTER: Final = "exposure_register"
SERVICE_KNX_READ: Final = "read"


class KNXConfigEntryData(TypedDict, total=False):
"""Config entry for the KNX integration."""
Expand Down
Loading

0 comments on commit f53109f

Please sign in to comment.