Add config and options flow to KNX integration (#59377)

This commit is contained in:
Marvin Wichmann 2021-11-20 11:30:41 +01:00 committed by GitHub
parent 40104de0bf
commit e5c33474e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1469 additions and 244 deletions

View file

@ -540,7 +540,18 @@ omit =
homeassistant/components/keyboard_remote/* homeassistant/components/keyboard_remote/*
homeassistant/components/kira/* homeassistant/components/kira/*
homeassistant/components/kiwi/lock.py homeassistant/components/kiwi/lock.py
homeassistant/components/knx/* homeassistant/components/knx/__init__.py
homeassistant/components/knx/climate.py
homeassistant/components/knx/const.py
homeassistant/components/knx/cover.py
homeassistant/components/knx/expose.py
homeassistant/components/knx/knx_entity.py
homeassistant/components/knx/light.py
homeassistant/components/knx/notify.py
homeassistant/components/knx/scene.py
homeassistant/components/knx/schema.py
homeassistant/components/knx/switch.py
homeassistant/components/knx/weather.py
homeassistant/components/kodi/__init__.py homeassistant/components/kodi/__init__.py
homeassistant/components/kodi/browse_media.py homeassistant/components/kodi/browse_media.py
homeassistant/components/kodi/const.py homeassistant/components/kodi/const.py

View file

@ -21,28 +21,30 @@ from xknx.telegram.address import (
) )
from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_EVENT, CONF_EVENT,
CONF_HOST, CONF_HOST,
CONF_PORT, CONF_PORT,
CONF_TYPE, CONF_TYPE,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD,
) )
from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
CONF_KNX_CONNECTION_TYPE,
CONF_KNX_EXPOSE, CONF_KNX_EXPOSE,
CONF_KNX_INDIVIDUAL_ADDRESS, CONF_KNX_INDIVIDUAL_ADDRESS,
CONF_KNX_ROUTING, CONF_KNX_ROUTING,
CONF_KNX_TUNNELING, CONF_KNX_TUNNELING,
DATA_KNX_CONFIG,
DOMAIN, DOMAIN,
KNX_ADDRESS, KNX_ADDRESS,
SupportedPlatforms, SupportedPlatforms,
@ -87,6 +89,13 @@ CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.All( DOMAIN: vol.All(
# deprecated since 2021.12 # deprecated since 2021.12
cv.deprecated(ConnectionSchema.CONF_KNX_STATE_UPDATER),
cv.deprecated(ConnectionSchema.CONF_KNX_RATE_LIMIT),
cv.deprecated(CONF_KNX_ROUTING),
cv.deprecated(CONF_KNX_TUNNELING),
cv.deprecated(CONF_KNX_INDIVIDUAL_ADDRESS),
cv.deprecated(ConnectionSchema.CONF_KNX_MCAST_GRP),
cv.deprecated(ConnectionSchema.CONF_KNX_MCAST_PORT),
cv.deprecated(CONF_KNX_EVENT_FILTER), cv.deprecated(CONF_KNX_EVENT_FILTER),
# deprecated since 2021.4 # deprecated since 2021.4
cv.deprecated("config_file"), cv.deprecated("config_file"),
@ -185,35 +194,73 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the KNX integration.""" """Start the KNX integration."""
try: conf: ConfigType | None = config.get(DOMAIN)
knx_module = KNXModule(hass, config)
hass.data[DOMAIN] = knx_module if conf is None:
await knx_module.start() # If we have a config entry, setup is done by that config entry.
except XKNXException as ex: # If there is no config entry, this should fail.
_LOGGER.warning("Could not connect to KNX interface: %s", ex) return bool(hass.config_entries.async_entries(DOMAIN))
hass.components.persistent_notification.async_create(
f"Could not connect to KNX interface: <br><b>{ex}</b>", title="KNX" conf = dict(conf)
hass.data[DATA_KNX_CONFIG] = conf
# Only import if we haven't before.
if not hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf
)
) )
if CONF_KNX_EXPOSE in config[DOMAIN]: return True
for expose_config in config[DOMAIN][CONF_KNX_EXPOSE]:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Load a config entry."""
conf = hass.data.get(DATA_KNX_CONFIG)
# When reloading
if conf is None:
conf = await async_integration_yaml_config(hass, DOMAIN)
if not conf or DOMAIN not in conf:
return False
conf = conf[DOMAIN]
# If user didn't have configuration.yaml config, generate defaults
if conf is None:
conf = CONFIG_SCHEMA({DOMAIN: dict(entry.data)})[DOMAIN]
config = {**conf, **entry.data}
try:
knx_module = KNXModule(hass, config, entry)
await knx_module.start()
except XKNXException as ex:
raise ConfigEntryNotReady from ex
hass.data[DATA_KNX_CONFIG] = conf
hass.data[DOMAIN] = knx_module
if CONF_KNX_EXPOSE in config:
for expose_config in config[CONF_KNX_EXPOSE]:
knx_module.exposures.append( knx_module.exposures.append(
create_knx_exposure(hass, knx_module.xknx, expose_config) create_knx_exposure(hass, knx_module.xknx, expose_config)
) )
for platform in SupportedPlatforms: hass.config_entries.async_setup_platforms(
if platform.value not in config[DOMAIN]: entry,
continue [platform.value for platform in SupportedPlatforms if platform.value in config],
)
# set up notify platform, no entry support for notify component yet,
# have to use discovery to load platform.
if NotifySchema.PLATFORM_NAME in conf:
hass.async_create_task( hass.async_create_task(
discovery.async_load_platform( discovery.async_load_platform(
hass, hass, "notify", DOMAIN, conf[NotifySchema.PLATFORM_NAME], config
platform.value,
DOMAIN,
{
"platform_config": config[DOMAIN][platform.value],
},
config,
) )
) )
@ -247,39 +294,53 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA, schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA,
) )
async def reload_service_handler(service_call: ServiceCall) -> None:
"""Remove all KNX components and load new ones from config."""
# First check for config file. If for some reason it is no longer there
# or knx is no longer mentioned, stop the reload.
config = await async_integration_yaml_config(hass, DOMAIN)
if not config or DOMAIN not in config:
return
await asyncio.gather(
*(platform.async_reset() for platform in async_get_platforms(hass, DOMAIN))
)
await knx_module.xknx.stop()
await async_setup(hass, config)
async_register_admin_service(
hass, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({})
)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unloading the KNX platforms."""
# if not loaded directly return
if not hass.data.get(DOMAIN):
return True
knx_module: KNXModule = hass.data[DOMAIN]
for exposure in knx_module.exposures:
exposure.shutdown()
unload_ok = await hass.config_entries.async_unload_platforms(
entry,
[
platform.value
for platform in SupportedPlatforms
if platform.value in hass.data[DATA_KNX_CONFIG]
],
)
if unload_ok:
await knx_module.stop()
hass.data.pop(DOMAIN)
hass.data.pop(DATA_KNX_CONFIG)
return unload_ok
async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Update a given config entry."""
return await hass.config_entries.async_reload(entry.entry_id)
class KNXModule: class KNXModule:
"""Representation of KNX Object.""" """Representation of KNX Object."""
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: def __init__(
self, hass: HomeAssistant, config: ConfigType, entry: ConfigEntry
) -> None:
"""Initialize KNX module.""" """Initialize KNX module."""
self.hass = hass self.hass = hass
self.config = config self.config = config
self.connected = False self.connected = False
self.exposures: list[KNXExposeSensor | KNXExposeTime] = [] self.exposures: list[KNXExposeSensor | KNXExposeTime] = []
self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {} self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {}
self.entry = entry
self.init_xknx() self.init_xknx()
self.xknx.connection_manager.register_connection_state_changed_cb( self.xknx.connection_manager.register_connection_state_changed_cb(
@ -292,64 +353,49 @@ class KNXModule:
self.register_event_callback() self.register_event_callback()
) )
self.entry.async_on_unload(
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
)
self.entry.async_on_unload(self.entry.add_update_listener(async_update_entry))
def init_xknx(self) -> None: def init_xknx(self) -> None:
"""Initialize XKNX object.""" """Initialize XKNX object."""
self.xknx = XKNX( self.xknx = XKNX(
own_address=self.config[DOMAIN][CONF_KNX_INDIVIDUAL_ADDRESS], own_address=self.config[CONF_KNX_INDIVIDUAL_ADDRESS],
rate_limit=self.config[DOMAIN][ConnectionSchema.CONF_KNX_RATE_LIMIT], rate_limit=self.config[ConnectionSchema.CONF_KNX_RATE_LIMIT],
multicast_group=self.config[DOMAIN][ConnectionSchema.CONF_KNX_MCAST_GRP], multicast_group=self.config[ConnectionSchema.CONF_KNX_MCAST_GRP],
multicast_port=self.config[DOMAIN][ConnectionSchema.CONF_KNX_MCAST_PORT], multicast_port=self.config[ConnectionSchema.CONF_KNX_MCAST_PORT],
connection_config=self.connection_config(), connection_config=self.connection_config(),
state_updater=self.config[DOMAIN][ConnectionSchema.CONF_KNX_STATE_UPDATER], state_updater=self.config[ConnectionSchema.CONF_KNX_STATE_UPDATER],
) )
async def start(self) -> None: async def start(self) -> None:
"""Start XKNX object. Connect to tunneling or Routing device.""" """Start XKNX object. Connect to tunneling or Routing device."""
await self.xknx.start() await self.xknx.start()
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
async def stop(self, event: Event) -> None: async def stop(self, event: Event | None = None) -> None:
"""Stop XKNX object. Disconnect from tunneling or Routing device.""" """Stop XKNX object. Disconnect from tunneling or Routing device."""
await self.xknx.stop() await self.xknx.stop()
def connection_config(self) -> ConnectionConfig: def connection_config(self) -> ConnectionConfig:
"""Return the connection_config.""" """Return the connection_config."""
if CONF_KNX_TUNNELING in self.config[DOMAIN]: _conn_type: str = self.config[CONF_KNX_CONNECTION_TYPE]
return self.connection_config_tunneling() if _conn_type == CONF_KNX_ROUTING:
if CONF_KNX_ROUTING in self.config[DOMAIN]: return ConnectionConfig(
return self.connection_config_routing() connection_type=ConnectionType.ROUTING,
return ConnectionConfig(auto_reconnect=True) auto_reconnect=True,
)
def connection_config_routing(self) -> ConnectionConfig: if _conn_type == CONF_KNX_TUNNELING:
"""Return the connection_config if routing is configured.""" return ConnectionConfig(
local_ip = None connection_type=ConnectionType.TUNNELING,
# all configuration values are optional gateway_ip=self.config[CONF_HOST],
if self.config[DOMAIN][CONF_KNX_ROUTING] is not None: gateway_port=self.config[CONF_PORT],
local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get( route_back=self.config.get(ConnectionSchema.CONF_KNX_ROUTE_BACK, False),
ConnectionSchema.CONF_KNX_LOCAL_IP auto_reconnect=True,
) )
return ConnectionConfig(
connection_type=ConnectionType.ROUTING, local_ip=local_ip
)
def connection_config_tunneling(self) -> ConnectionConfig: return ConnectionConfig(auto_reconnect=True)
"""Return the connection_config if tunneling is configured."""
gateway_ip = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_HOST]
gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_PORT]
local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(
ConnectionSchema.CONF_KNX_LOCAL_IP
)
route_back = self.config[DOMAIN][CONF_KNX_TUNNELING][
ConnectionSchema.CONF_KNX_ROUTE_BACK
]
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING,
gateway_ip=gateway_ip,
gateway_port=gateway_port,
local_ip=local_ip,
route_back=route_back,
auto_reconnect=True,
)
async def connection_state_changed_cb(self, state: XknxConnectionState) -> None: async def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
"""Call invoked after a KNX connection state change was received.""" """Call invoked after a KNX connection state change was received."""
@ -409,10 +455,8 @@ class KNXModule:
"""Register callback for knx_event within XKNX TelegramQueue.""" """Register callback for knx_event within XKNX TelegramQueue."""
# backwards compatibility for deprecated CONF_KNX_EVENT_FILTER # backwards compatibility for deprecated CONF_KNX_EVENT_FILTER
# use `address_filters = []` when this is not needed anymore # use `address_filters = []` when this is not needed anymore
address_filters = list( address_filters = list(map(AddressFilter, self.config[CONF_KNX_EVENT_FILTER]))
map(AddressFilter, self.config[DOMAIN][CONF_KNX_EVENT_FILTER]) for filter_set in self.config[CONF_EVENT]:
)
for filter_set in self.config[DOMAIN][CONF_EVENT]:
_filters = list(map(AddressFilter, filter_set[KNX_ADDRESS])) _filters = list(map(AddressFilter, filter_set[KNX_ADDRESS]))
address_filters.extend(_filters) address_filters.extend(_filters)
if (dpt := filter_set.get(CONF_TYPE)) and ( if (dpt := filter_set.get(CONF_TYPE)) and (

View file

@ -6,6 +6,7 @@ from typing import Any
from xknx import XKNX from xknx import XKNX
from xknx.devices import BinarySensor as XknxBinarySensor from xknx.devices import BinarySensor as XknxBinarySensor
from homeassistant import config_entries
from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
@ -18,28 +19,31 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from .const import ATTR_COUNTER, ATTR_SOURCE, DOMAIN from .const import (
ATTR_COUNTER,
ATTR_SOURCE,
DATA_KNX_CONFIG,
DOMAIN,
SupportedPlatforms,
)
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
from .schema import BinarySensorSchema from .schema import BinarySensorSchema
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up binary sensor(s) for KNX platform.""" """Set up the KNX binary sensor platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: ConfigType = hass.data[DATA_KNX_CONFIG]
async_add_entities( async_add_entities(
KNXBinarySensor(xknx, entity_config) for entity_config in platform_config KNXBinarySensor(xknx, entity_config)
for entity_config in config[SupportedPlatforms.BINARY_SENSOR.value]
) )

View file

@ -4,30 +4,36 @@ from __future__ import annotations
from xknx import XKNX from xknx import XKNX
from xknx.devices import RawValue as XknxRawValue from xknx.devices import RawValue as XknxRawValue
from homeassistant import config_entries
from homeassistant.components.button import ButtonEntity from homeassistant.components.button import ButtonEntity
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from .const import CONF_PAYLOAD, CONF_PAYLOAD_LENGTH, DOMAIN, KNX_ADDRESS from .const import (
CONF_PAYLOAD,
CONF_PAYLOAD_LENGTH,
DATA_KNX_CONFIG,
DOMAIN,
KNX_ADDRESS,
SupportedPlatforms,
)
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up buttons for KNX platform.""" """Set up the KNX binary sensor platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: ConfigType = hass.data[DATA_KNX_CONFIG]
async_add_entities( async_add_entities(
KNXButton(xknx, entity_config) for entity_config in platform_config KNXButton(xknx, entity_config)
for entity_config in config[SupportedPlatforms.BUTTON.value]
) )

View file

@ -8,6 +8,7 @@ from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode
from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
from xknx.telegram.address import parse_device_group_address from xknx.telegram.address import parse_device_group_address
from homeassistant import config_entries
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
CURRENT_HVAC_IDLE, CURRENT_HVAC_IDLE,
@ -26,9 +27,16 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from .const import CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, DOMAIN, PRESET_MODES from .const import (
CONTROLLER_MODES,
CURRENT_HVAC_ACTIONS,
DATA_KNX_CONFIG,
DOMAIN,
PRESET_MODES,
SupportedPlatforms,
)
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
from .schema import ClimateSchema from .schema import ClimateSchema
@ -37,23 +45,19 @@ CONTROLLER_MODES_INV = {value: key for key, value in CONTROLLER_MODES.items()}
PRESET_MODES_INV = {value: key for key, value in PRESET_MODES.items()} PRESET_MODES_INV = {value: key for key, value in PRESET_MODES.items()}
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up climate(s) for KNX platform.""" """Set up climate(s) for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][
SupportedPlatforms.CLIMATE.value
]
_async_migrate_unique_id(hass, platform_config) _async_migrate_unique_id(hass, config)
async_add_entities( async_add_entities(KNXClimate(xknx, entity_config) for entity_config in config)
KNXClimate(xknx, entity_config) for entity_config in platform_config
)
@callback @callback

View file

@ -0,0 +1,409 @@
"""Config flow for KNX."""
from __future__ import annotations
from typing import Any, Final
import voluptuous as vol
from xknx import XKNX
from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
from xknx.io.gateway_scanner import GatewayDescriptor, GatewayScanner
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry, OptionsFlow
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from .const import (
CONF_KNX_AUTOMATIC,
CONF_KNX_CONNECTION_TYPE,
CONF_KNX_INDIVIDUAL_ADDRESS,
CONF_KNX_INITIAL_CONNECTION_TYPES,
CONF_KNX_ROUTING,
CONF_KNX_TUNNELING,
DOMAIN,
)
from .schema import ConnectionSchema
CONF_KNX_GATEWAY: Final = "gateway"
CONF_MAX_RATE_LIMIT: Final = 60
DEFAULT_ENTRY_DATA: Final = {
ConnectionSchema.CONF_KNX_STATE_UPDATER: ConnectionSchema.CONF_KNX_DEFAULT_STATE_UPDATER,
ConnectionSchema.CONF_KNX_RATE_LIMIT: ConnectionSchema.CONF_KNX_DEFAULT_RATE_LIMIT,
CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT,
}
class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a KNX config flow."""
VERSION = 1
_tunnels: list
_gateway_ip: str = ""
_gateway_port: int = DEFAULT_MCAST_PORT
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> KNXOptionsFlowHandler:
"""Get the options flow for this handler."""
return KNXOptionsFlowHandler(config_entry)
async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
"""Handle a flow initialized by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
self._tunnels = []
return await self.async_step_type()
async def async_step_type(self, user_input: dict | None = None) -> FlowResult:
"""Handle connection type configuration."""
errors: dict = {}
supported_connection_types = CONF_KNX_INITIAL_CONNECTION_TYPES.copy()
fields = {}
if user_input is None:
gateways = await scan_for_gateways()
if gateways:
supported_connection_types.insert(0, CONF_KNX_AUTOMATIC)
self._tunnels = [
gateway for gateway in gateways if gateway.supports_tunnelling
]
fields = {
vol.Required(CONF_KNX_CONNECTION_TYPE): vol.In(
supported_connection_types
)
}
if user_input is not None:
connection_type = user_input[CONF_KNX_CONNECTION_TYPE]
if connection_type == CONF_KNX_AUTOMATIC:
return self.async_create_entry(
title=CONF_KNX_AUTOMATIC.capitalize(),
data={**DEFAULT_ENTRY_DATA, **user_input},
)
if connection_type == CONF_KNX_ROUTING:
return await self.async_step_routing()
if connection_type == CONF_KNX_TUNNELING and self._tunnels:
return await self.async_step_tunnel()
return await self.async_step_manual_tunnel()
return self.async_show_form(
step_id="type", data_schema=vol.Schema(fields), errors=errors
)
async def async_step_manual_tunnel(
self, user_input: dict | None = None
) -> FlowResult:
"""General setup."""
errors: dict = {}
if user_input is not None:
return self.async_create_entry(
title=f"{CONF_KNX_TUNNELING.capitalize()} @ {user_input[CONF_HOST]}",
data={
**DEFAULT_ENTRY_DATA,
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
CONF_KNX_INDIVIDUAL_ADDRESS: user_input[
CONF_KNX_INDIVIDUAL_ADDRESS
],
ConnectionSchema.CONF_KNX_ROUTE_BACK: user_input[
ConnectionSchema.CONF_KNX_ROUTE_BACK
],
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
},
)
fields = {
vol.Required(CONF_HOST, default=self._gateway_ip): str,
vol.Required(CONF_PORT, default=self._gateway_port): vol.Coerce(int),
vol.Required(
CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS
): str,
vol.Required(
ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False
): vol.Coerce(bool),
}
return self.async_show_form(
step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors
)
async def async_step_tunnel(self, user_input: dict | None = None) -> FlowResult:
"""Select a tunnel from a list. Will be skipped if the gateway scan was unsuccessful or if only one gateway was found."""
errors: dict = {}
if user_input is not None:
gateway: GatewayDescriptor = next(
gateway
for gateway in self._tunnels
if user_input[CONF_KNX_GATEWAY] == str(gateway)
)
self._gateway_ip = gateway.ip_addr
self._gateway_port = gateway.port
return await self.async_step_manual_tunnel()
tunnel_repr = {
str(tunnel) for tunnel in self._tunnels if tunnel.supports_tunnelling
}
# skip this step if the user has only one unique gateway.
if len(tunnel_repr) == 1:
_gateway: GatewayDescriptor = self._tunnels[0]
self._gateway_ip = _gateway.ip_addr
self._gateway_port = _gateway.port
return await self.async_step_manual_tunnel()
fields = {vol.Required(CONF_KNX_GATEWAY): vol.In(tunnel_repr)}
return self.async_show_form(
step_id="tunnel", data_schema=vol.Schema(fields), errors=errors
)
async def async_step_routing(self, user_input: dict | None = None) -> FlowResult:
"""Routing setup."""
errors: dict = {}
if user_input is not None:
return self.async_create_entry(
title=CONF_KNX_ROUTING.capitalize(),
data={
**DEFAULT_ENTRY_DATA,
ConnectionSchema.CONF_KNX_MCAST_GRP: user_input[
ConnectionSchema.CONF_KNX_MCAST_GRP
],
ConnectionSchema.CONF_KNX_MCAST_PORT: user_input[
ConnectionSchema.CONF_KNX_MCAST_PORT
],
CONF_KNX_INDIVIDUAL_ADDRESS: user_input[
CONF_KNX_INDIVIDUAL_ADDRESS
],
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
},
)
fields = {
vol.Required(
CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS
): str,
vol.Required(
ConnectionSchema.CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP
): str,
vol.Required(
ConnectionSchema.CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT
): cv.port,
}
return self.async_show_form(
step_id="routing", data_schema=vol.Schema(fields), errors=errors
)
async def async_step_import(self, config: dict | None = None) -> FlowResult:
"""Import a config entry.
Performs a one time import of the YAML configuration and creates a config entry based on it
if not already done before.
"""
if self._async_current_entries() or not config:
return self.async_abort(reason="single_instance_allowed")
data = {
ConnectionSchema.CONF_KNX_RATE_LIMIT: min(
config[ConnectionSchema.CONF_KNX_RATE_LIMIT], CONF_MAX_RATE_LIMIT
),
ConnectionSchema.CONF_KNX_STATE_UPDATER: config[
ConnectionSchema.CONF_KNX_STATE_UPDATER
],
ConnectionSchema.CONF_KNX_MCAST_GRP: config[
ConnectionSchema.CONF_KNX_MCAST_GRP
],
ConnectionSchema.CONF_KNX_MCAST_PORT: config[
ConnectionSchema.CONF_KNX_MCAST_PORT
],
CONF_KNX_INDIVIDUAL_ADDRESS: config[CONF_KNX_INDIVIDUAL_ADDRESS],
}
if CONF_KNX_TUNNELING in config:
return self.async_create_entry(
title=f"{CONF_KNX_TUNNELING.capitalize()} @ {config[CONF_KNX_TUNNELING][CONF_HOST]}",
data={
**DEFAULT_ENTRY_DATA,
CONF_HOST: config[CONF_KNX_TUNNELING][CONF_HOST],
CONF_PORT: config[CONF_KNX_TUNNELING][CONF_PORT],
ConnectionSchema.CONF_KNX_ROUTE_BACK: config[CONF_KNX_TUNNELING][
ConnectionSchema.CONF_KNX_ROUTE_BACK
],
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
**data,
},
)
if CONF_KNX_ROUTING in config:
return self.async_create_entry(
title=CONF_KNX_ROUTING.capitalize(),
data={
**DEFAULT_ENTRY_DATA,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
**data,
},
)
return self.async_create_entry(
title=CONF_KNX_AUTOMATIC.capitalize(),
data={
**DEFAULT_ENTRY_DATA,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
**data,
},
)
class KNXOptionsFlowHandler(OptionsFlow):
"""Handle KNX options."""
general_settings: dict
current_config: dict
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize KNX options flow."""
self.config_entry = config_entry
async def async_step_tunnel(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage KNX tunneling options."""
if (
self.general_settings.get(CONF_KNX_CONNECTION_TYPE) == CONF_KNX_TUNNELING
and user_input is None
):
return self.async_show_form(
step_id="tunnel",
data_schema=vol.Schema(
{
vol.Required(
CONF_HOST, default=self.current_config.get(CONF_HOST)
): str,
vol.Required(
CONF_PORT, default=self.current_config.get(CONF_PORT, 3671)
): cv.port,
vol.Required(
ConnectionSchema.CONF_KNX_ROUTE_BACK,
default=self.current_config.get(
ConnectionSchema.CONF_KNX_ROUTE_BACK, False
),
): vol.Coerce(bool),
}
),
last_step=True,
)
entry_data = {
**DEFAULT_ENTRY_DATA,
**self.general_settings,
CONF_HOST: self.current_config.get(CONF_HOST, ""),
}
if user_input is not None:
entry_data = {
**entry_data,
**user_input,
}
entry_title = entry_data[CONF_KNX_CONNECTION_TYPE].capitalize()
if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING:
entry_title = f"{CONF_KNX_TUNNELING.capitalize()} @ {entry_data[CONF_HOST]}"
self.hass.config_entries.async_update_entry(
self.config_entry,
data=entry_data,
title=entry_title,
)
return self.async_create_entry(title="", data={})
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage KNX options."""
if user_input is not None:
self.general_settings = user_input
return await self.async_step_tunnel()
supported_connection_types = [
CONF_KNX_AUTOMATIC,
CONF_KNX_TUNNELING,
CONF_KNX_ROUTING,
]
self.current_config = self.config_entry.data # type: ignore
data_schema = {
vol.Required(
CONF_KNX_CONNECTION_TYPE,
default=self.current_config.get(CONF_KNX_CONNECTION_TYPE),
): vol.In(supported_connection_types),
vol.Required(
CONF_KNX_INDIVIDUAL_ADDRESS,
default=self.current_config[CONF_KNX_INDIVIDUAL_ADDRESS],
): str,
vol.Required(
ConnectionSchema.CONF_KNX_MCAST_GRP,
default=self.current_config.get(
ConnectionSchema.CONF_KNX_MCAST_GRP, DEFAULT_MCAST_GRP
),
): str,
vol.Required(
ConnectionSchema.CONF_KNX_MCAST_PORT,
default=self.current_config.get(
ConnectionSchema.CONF_KNX_MCAST_PORT, DEFAULT_MCAST_PORT
),
): cv.port,
}
if self.show_advanced_options:
data_schema[
vol.Required(
ConnectionSchema.CONF_KNX_STATE_UPDATER,
default=self.current_config.get(
ConnectionSchema.CONF_KNX_STATE_UPDATER,
ConnectionSchema.CONF_KNX_DEFAULT_STATE_UPDATER,
),
)
] = bool
data_schema[
vol.Required(
ConnectionSchema.CONF_KNX_RATE_LIMIT,
default=self.current_config.get(
ConnectionSchema.CONF_KNX_RATE_LIMIT,
ConnectionSchema.CONF_KNX_DEFAULT_RATE_LIMIT,
),
)
] = vol.All(vol.Coerce(int), vol.Range(min=1, max=CONF_MAX_RATE_LIMIT))
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(data_schema),
last_step=self.current_config.get(CONF_KNX_CONNECTION_TYPE)
!= CONF_KNX_TUNNELING,
)
async def scan_for_gateways(stop_on_found: int = 0) -> list:
"""Scan for gateways within the network."""
xknx = XKNX()
gatewayscanner = GatewayScanner(
xknx, stop_on_found=stop_on_found, timeout_in_seconds=2
)
return await gatewayscanner.scan()

View file

@ -29,6 +29,8 @@ KNX_ADDRESS: Final = "address"
CONF_INVERT: Final = "invert" CONF_INVERT: Final = "invert"
CONF_KNX_EXPOSE: Final = "expose" CONF_KNX_EXPOSE: Final = "expose"
CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address" CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address"
CONF_KNX_CONNECTION_TYPE: Final = "connection_type"
CONF_KNX_AUTOMATIC: Final = "automatic"
CONF_KNX_ROUTING: Final = "routing" CONF_KNX_ROUTING: Final = "routing"
CONF_KNX_TUNNELING: Final = "tunneling" CONF_KNX_TUNNELING: Final = "tunneling"
CONF_PAYLOAD: Final = "payload" CONF_PAYLOAD: Final = "payload"
@ -37,6 +39,9 @@ CONF_RESET_AFTER: Final = "reset_after"
CONF_RESPOND_TO_READ: Final = "respond_to_read" CONF_RESPOND_TO_READ: Final = "respond_to_read"
CONF_STATE_ADDRESS: Final = "state_address" CONF_STATE_ADDRESS: Final = "state_address"
CONF_SYNC_STATE: Final = "sync_state" CONF_SYNC_STATE: Final = "sync_state"
CONF_KNX_INITIAL_CONNECTION_TYPES: Final = [CONF_KNX_TUNNELING, CONF_KNX_ROUTING]
DATA_KNX_CONFIG: Final = "knx_config"
ATTR_COUNTER: Final = "counter" ATTR_COUNTER: Final = "counter"
ATTR_SOURCE: Final = "source" ATTR_SOURCE: Final = "source"

View file

@ -9,6 +9,7 @@ from xknx import XKNX
from xknx.devices import Cover as XknxCover, Device as XknxDevice from xknx.devices import Cover as XknxCover, Device as XknxDevice
from xknx.telegram.address import parse_device_group_address from xknx.telegram.address import parse_device_group_address
from homeassistant import config_entries
from homeassistant.components.cover import ( from homeassistant.components.cover import (
ATTR_POSITION, ATTR_POSITION,
ATTR_TILT_POSITION, ATTR_TILT_POSITION,
@ -28,29 +29,26 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.helpers.event import async_track_utc_time_change
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN from .const import DATA_KNX_CONFIG, DOMAIN, SupportedPlatforms
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
from .schema import CoverSchema from .schema import CoverSchema
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up cover(s) for KNX platform.""" """Set up cover(s) for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][
SupportedPlatforms.COVER.value
]
_async_migrate_unique_id(hass, platform_config) _async_migrate_unique_id(hass, config)
async_add_entities( async_add_entities(KNXCover(xknx, entity_config) for entity_config in config)
KNXCover(xknx, entity_config) for entity_config in platform_config
)
@callback @callback

View file

@ -7,37 +7,35 @@ from typing import Any, Final
from xknx import XKNX from xknx import XKNX
from xknx.devices import Fan as XknxFan from xknx.devices import Fan as XknxFan
from homeassistant import config_entries
from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
int_states_in_range, int_states_in_range,
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
) )
from .const import DOMAIN, KNX_ADDRESS from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, SupportedPlatforms
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
from .schema import FanSchema from .schema import FanSchema
DEFAULT_PERCENTAGE: Final = 50 DEFAULT_PERCENTAGE: Final = 50
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up fans for KNX platform.""" """Set up fan(s) for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][SupportedPlatforms.FAN.value]
async_add_entities(KNXFan(xknx, entity_config) for entity_config in platform_config) async_add_entities(KNXFan(xknx, entity_config) for entity_config in config)
class KNXFan(KnxEntity, FanEntity): class KNXFan(KnxEntity, FanEntity):

View file

@ -7,6 +7,7 @@ from xknx import XKNX
from xknx.devices.light import Light as XknxLight, XYYColor from xknx.devices.light import Light as XknxLight, XYYColor
from xknx.telegram.address import parse_device_group_address from xknx.telegram.address import parse_device_group_address
from homeassistant import config_entries
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP, ATTR_COLOR_TEMP,
@ -27,30 +28,33 @@ from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from .const import DOMAIN, KNX_ADDRESS, ColorTempModes from .const import (
DATA_KNX_CONFIG,
DOMAIN,
KNX_ADDRESS,
ColorTempModes,
SupportedPlatforms,
)
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
from .schema import LightSchema from .schema import LightSchema
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up lights for KNX platform.""" """Set up light(s) for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][
SupportedPlatforms.LIGHT.value
]
_async_migrate_unique_id(hass, platform_config) _async_migrate_unique_id(hass, config)
async_add_entities( async_add_entities(KNXLight(xknx, entity_config) for entity_config in config)
KNXLight(xknx, entity_config) for entity_config in platform_config
)
@callback @callback

View file

@ -1,9 +1,16 @@
{ {
"domain": "knx", "domain": "knx",
"name": "KNX", "name": "KNX",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/knx", "documentation": "https://www.home-assistant.io/integrations/knx",
"requirements": ["xknx==0.18.13"], "requirements": [
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "xknx==0.18.13"
],
"codeowners": [
"@Julius2342",
"@farmio",
"@marvin-w"
],
"quality_scale": "silver", "quality_scale": "silver",
"iot_class": "local_push" "iot_class": "local_push"
} }

View file

@ -20,10 +20,10 @@ async def async_get_service(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> KNXNotificationService | None: ) -> KNXNotificationService | None:
"""Get the KNX notification service.""" """Get the KNX notification service."""
if not discovery_info or not discovery_info["platform_config"]: if not discovery_info:
return None return None
platform_config = discovery_info["platform_config"] platform_config: dict = discovery_info
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
notification_devices = [] notification_devices = []

View file

@ -6,6 +6,7 @@ from typing import cast
from xknx import XKNX from xknx import XKNX
from xknx.devices import NumericValue from xknx.devices import NumericValue
from homeassistant import config_entries
from homeassistant.components.number import NumberEntity from homeassistant.components.number import NumberEntity
from homeassistant.const import ( from homeassistant.const import (
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
@ -18,28 +19,32 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from .const import CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, DOMAIN, KNX_ADDRESS from .const import (
CONF_RESPOND_TO_READ,
CONF_STATE_ADDRESS,
DATA_KNX_CONFIG,
DOMAIN,
KNX_ADDRESS,
SupportedPlatforms,
)
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
from .schema import NumberSchema from .schema import NumberSchema
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up number entities for KNX platform.""" """Set up number(s) for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][
SupportedPlatforms.NUMBER.value
]
async_add_entities( async_add_entities(KNXNumber(xknx, entity_config) for entity_config in config)
KNXNumber(xknx, entity_config) for entity_config in platform_config
)
def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue: def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue:

View file

@ -6,32 +6,30 @@ from typing import Any
from xknx import XKNX from xknx import XKNX
from xknx.devices import Scene as XknxScene from xknx.devices import Scene as XknxScene
from homeassistant import config_entries
from homeassistant.components.scene import Scene from homeassistant.components.scene import Scene
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, KNX_ADDRESS from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, SupportedPlatforms
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
from .schema import SceneSchema from .schema import SceneSchema
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the scenes for KNX platform.""" """Set up scene(s) for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][
SupportedPlatforms.SCENE.value
]
async_add_entities( async_add_entities(KNXScene(xknx, entity_config) for entity_config in config)
KNXScene(xknx, entity_config) for entity_config in platform_config
)
class KNXScene(KnxEntity, Scene): class KNXScene(KnxEntity, Scene):

View file

@ -201,7 +201,11 @@ sync_state_validator = vol.Any(
class ConnectionSchema: class ConnectionSchema:
"""Voluptuous schema for KNX connection.""" """
Voluptuous schema for KNX connection.
DEPRECATED: Migrated to config and options flow. Will be removed in a future version of Home Assistant.
"""
CONF_KNX_LOCAL_IP = "local_ip" CONF_KNX_LOCAL_IP = "local_ip"
CONF_KNX_MCAST_GRP = "multicast_group" CONF_KNX_MCAST_GRP = "multicast_group"
@ -210,6 +214,9 @@ class ConnectionSchema:
CONF_KNX_ROUTE_BACK = "route_back" CONF_KNX_ROUTE_BACK = "route_back"
CONF_KNX_STATE_UPDATER = "state_updater" CONF_KNX_STATE_UPDATER = "state_updater"
CONF_KNX_DEFAULT_STATE_UPDATER = True
CONF_KNX_DEFAULT_RATE_LIMIT = 20
TUNNELING_SCHEMA = vol.Schema( TUNNELING_SCHEMA = vol.Schema(
{ {
vol.Optional(CONF_PORT, default=DEFAULT_MCAST_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_MCAST_PORT): cv.port,
@ -229,8 +236,10 @@ class ConnectionSchema:
): ia_validator, ): ia_validator,
vol.Optional(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): cv.string, vol.Optional(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): cv.string,
vol.Optional(CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT): cv.port, vol.Optional(CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT): cv.port,
vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean, vol.Optional(
vol.Optional(CONF_KNX_RATE_LIMIT, default=20): vol.All( CONF_KNX_STATE_UPDATER, default=CONF_KNX_DEFAULT_STATE_UPDATER
): cv.boolean,
vol.Optional(CONF_KNX_RATE_LIMIT, default=CONF_KNX_DEFAULT_RATE_LIMIT): vol.All(
vol.Coerce(int), vol.Range(min=1, max=100) vol.Coerce(int), vol.Range(min=1, max=100)
), ),
} }

View file

@ -4,6 +4,7 @@ from __future__ import annotations
from xknx import XKNX from xknx import XKNX
from xknx.devices import Device as XknxDevice, RawValue from xknx.devices import Device as XknxDevice, RawValue
from homeassistant import config_entries
from homeassistant.components.select import SelectEntity from homeassistant.components.select import SelectEntity
from homeassistant.const import ( from homeassistant.const import (
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
@ -14,7 +15,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
CONF_PAYLOAD, CONF_PAYLOAD,
@ -22,28 +23,27 @@ from .const import (
CONF_RESPOND_TO_READ, CONF_RESPOND_TO_READ,
CONF_STATE_ADDRESS, CONF_STATE_ADDRESS,
CONF_SYNC_STATE, CONF_SYNC_STATE,
DATA_KNX_CONFIG,
DOMAIN, DOMAIN,
KNX_ADDRESS, KNX_ADDRESS,
SupportedPlatforms,
) )
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
from .schema import SelectSchema from .schema import SelectSchema
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up select entities for KNX platform.""" """Set up select(s) for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][
SupportedPlatforms.SELECT.value
]
async_add_entities( async_add_entities(KNXSelect(xknx, entity_config) for entity_config in config)
KNXSelect(xknx, entity_config) for entity_config in platform_config
)
def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue: def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue:

View file

@ -6,6 +6,7 @@ from typing import Any
from xknx import XKNX from xknx import XKNX
from xknx.devices import Sensor as XknxSensor from xknx.devices import Sensor as XknxSensor
from homeassistant import config_entries
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
CONF_STATE_CLASS, CONF_STATE_CLASS,
DEVICE_CLASSES, DEVICE_CLASSES,
@ -14,28 +15,25 @@ from homeassistant.components.sensor import (
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.helpers.typing import ConfigType, StateType
from .const import ATTR_SOURCE, DOMAIN from .const import ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN, SupportedPlatforms
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
from .schema import SensorSchema from .schema import SensorSchema
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up sensor(s) for KNX platform.""" """Set up sensor(s) for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][
SupportedPlatforms.SENSOR.value
]
async_add_entities( async_add_entities(KNXSensor(xknx, entity_config) for entity_config in config)
KNXSensor(xknx, entity_config) for entity_config in platform_config
)
def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor: def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor:

View file

@ -100,6 +100,3 @@ exposure_register:
default: false default: false
selector: selector:
boolean: boolean:
reload:
name: "Reload KNX configuration"
description: "Reload the KNX configuration from YAML."

View file

@ -0,0 +1,63 @@
{
"config": {
"step": {
"type": {
"description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.",
"data": {
"connection_type": "KNX Connection Type"
}
},
"tunnel": {
"description": "Please select a gateway from the list.",
"data": {
"gateway": "KNX Tunnel Connection"
}
},
"manual_tunnel": {
"description": "Please enter the connection information of your tunneling device.",
"data": {
"port": "[%key:common::config_flow::data::port%]",
"host": "[%key:common::config_flow::data::host%]",
"individual_address": "Individual address for the connection",
"route_back": "Route Back / NAT Mode"
}
},
"routing": {
"description": "Please configure the routing options.",
"data": {
"individual_address": "Individual address for the routing connection",
"multicast_group": "The multicast group used for routing",
"multicast_port": "The multicast port used for routing"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"options": {
"step": {
"init": {
"data": {
"connection_type": "KNX Connection Type",
"individual_address": "Default individual address",
"multicast_group": "Multicast group used for routing and discovery",
"multicast_port": "Multicast port used for routing and discovery",
"state_updater": "Globally enable reading states from the KNX Bus",
"rate_limit": "Maximum outgoing telegrams per second"
}
},
"tunnel": {
"data": {
"port": "[%key:common::config_flow::data::port%]",
"host": "[%key:common::config_flow::data::host%]",
"route_back": "Route Back / NAT Mode"
}
}
}
}
}

View file

@ -6,6 +6,7 @@ from typing import Any
from xknx import XKNX from xknx import XKNX
from xknx.devices import Switch as XknxSwitch from xknx.devices import Switch as XknxSwitch
from homeassistant import config_entries
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.const import ( from homeassistant.const import (
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
@ -17,28 +18,31 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from .const import CONF_RESPOND_TO_READ, DOMAIN, KNX_ADDRESS from .const import (
CONF_RESPOND_TO_READ,
DATA_KNX_CONFIG,
DOMAIN,
KNX_ADDRESS,
SupportedPlatforms,
)
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
from .schema import SwitchSchema from .schema import SwitchSchema
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up switch(es) for KNX platform.""" """Set up switch(es) for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][
SupportedPlatforms.SWITCH.value
]
async_add_entities( async_add_entities(KNXSwitch(xknx, entity_config) for entity_config in config)
KNXSwitch(xknx, entity_config) for entity_config in platform_config
)
class KNXSwitch(KnxEntity, SwitchEntity, RestoreEntity): class KNXSwitch(KnxEntity, SwitchEntity, RestoreEntity):

View file

@ -0,0 +1,63 @@
{
"config": {
"step": {
"type": {
"description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.",
"data": {
"connection_type": "KNX Connection Type"
}
},
"tunnel": {
"description": "Please select a gateway from the list.",
"data": {
"gateway": "KNX Tunnel Connection"
}
},
"manual_tunnel": {
"description": "Please enter the connection information of your tunneling device.",
"data": {
"host": "IP Address of KNX gateway",
"port": "Port of KNX gateway",
"individual_address": "Individual address for the connection",
"route_back": "Route Back / NAT Mode"
}
},
"routing": {
"description": "Please configure the routing options.",
"data": {
"individual_address": "Individual address for the routing connection",
"multicast_group": "The multicast group used for routing",
"multicast_port": "The multicast port used for routing"
}
}
},
"abort": {
"already_configured": "Service is already configured",
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
"cannot_connect": "Failed to connect."
}
},
"options": {
"step": {
"init": {
"data": {
"connection_type": "KNX Connection Type",
"individual_address": "Default individual address",
"multicast_group": "Multicast group used for routing and discovery",
"multicast_port": "Multicast port used for routing and discovery",
"state_updater": "Globally enable reading states from the KNX Bus",
"rate_limit": "Maximum outgoing telegrams per second"
}
},
"tunnel": {
"data": {
"port": "Port of KNX gateway",
"host": "IP Address of KNX gateway",
"route_back": "Route Back / NAT Mode"
}
}
}
}
}

View file

@ -4,32 +4,30 @@ from __future__ import annotations
from xknx import XKNX from xknx import XKNX
from xknx.devices import Weather as XknxWeather from xknx.devices import Weather as XknxWeather
from homeassistant import config_entries
from homeassistant.components.weather import WeatherEntity from homeassistant.components.weather import WeatherEntity
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, TEMP_CELSIUS from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, TEMP_CELSIUS
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN from .const import DATA_KNX_CONFIG, DOMAIN, SupportedPlatforms
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
from .schema import WeatherSchema from .schema import WeatherSchema
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up weather entities for KNX platform.""" """Set up switch(es) for KNX platform."""
if not discovery_info or not discovery_info["platform_config"]:
return
platform_config = discovery_info["platform_config"]
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][
SupportedPlatforms.WEATHER.value
]
async_add_entities( async_add_entities(KNXWeather(xknx, entity_config) for entity_config in config)
KNXWeather(xknx, entity_config) for entity_config in platform_config
)
def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather: def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather:

View file

@ -152,6 +152,7 @@ FLOWS = [
"juicenet", "juicenet",
"keenetic_ndms2", "keenetic_ndms2",
"kmtronic", "kmtronic",
"knx",
"kodi", "kodi",
"konnected", "konnected",
"kostal_plenticore", "kostal_plenticore",

View file

@ -8,23 +8,33 @@ import pytest
from xknx import XKNX from xknx import XKNX
from xknx.core import XknxConnectionState from xknx.core import XknxConnectionState
from xknx.dpt import DPTArray, DPTBinary from xknx.dpt import DPTArray, DPTBinary
from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
from xknx.telegram import Telegram, TelegramDirection from xknx.telegram import Telegram, TelegramDirection
from xknx.telegram.address import GroupAddress, IndividualAddress from xknx.telegram.address import GroupAddress, IndividualAddress
from xknx.telegram.apci import APCI, GroupValueRead, GroupValueResponse, GroupValueWrite from xknx.telegram.apci import APCI, GroupValueRead, GroupValueResponse, GroupValueWrite
from homeassistant.components.knx.const import DOMAIN as KNX_DOMAIN from homeassistant.components.knx import ConnectionSchema
from homeassistant.components.knx.const import (
CONF_KNX_AUTOMATIC,
CONF_KNX_CONNECTION_TYPE,
CONF_KNX_INDIVIDUAL_ADDRESS,
DOMAIN as KNX_DOMAIN,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
class KNXTestKit: class KNXTestKit:
"""Test helper for the KNX integration.""" """Test helper for the KNX integration."""
INDIVIDUAL_ADDRESS = "1.2.3" INDIVIDUAL_ADDRESS = "1.2.3"
def __init__(self, hass: HomeAssistant): def __init__(self, hass: HomeAssistant, mock_config_entry: MockConfigEntry):
"""Init KNX test helper class.""" """Init KNX test helper class."""
self.hass: HomeAssistant = hass self.hass: HomeAssistant = hass
self.mock_config_entry: MockConfigEntry = mock_config_entry
self.xknx: XKNX self.xknx: XKNX
# outgoing telegrams will be put in the Queue instead of sent to the interface # outgoing telegrams will be put in the Queue instead of sent to the interface
# telegrams to an InternalGroupAddress won't be queued here # telegrams to an InternalGroupAddress won't be queued here
@ -60,6 +70,7 @@ class KNXTestKit:
return_value=knx_ip_interface_mock(), return_value=knx_ip_interface_mock(),
side_effect=fish_xknx, side_effect=fish_xknx,
): ):
self.mock_config_entry.add_to_hass(self.hass)
await async_setup_component(self.hass, KNX_DOMAIN, {KNX_DOMAIN: config}) await async_setup_component(self.hass, KNX_DOMAIN, {KNX_DOMAIN: config})
await self.xknx.connection_manager.connection_state_changed( await self.xknx.connection_manager.connection_state_changed(
XknxConnectionState.CONNECTED XknxConnectionState.CONNECTED
@ -191,8 +202,23 @@ class KNXTestKit:
@pytest.fixture @pytest.fixture
async def knx(request, hass): def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="KNX",
domain=KNX_DOMAIN,
data={
CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
},
)
@pytest.fixture
async def knx(request, hass, mock_config_entry: MockConfigEntry):
"""Create a KNX TestKit instance.""" """Create a KNX TestKit instance."""
knx_test_kit = KNXTestKit(hass) knx_test_kit = KNXTestKit(hass, mock_config_entry)
yield knx_test_kit yield knx_test_kit
await knx_test_kit.assert_no_telegram() await knx_test_kit.assert_no_telegram()

View file

@ -0,0 +1,573 @@
"""Test the KNX config flow."""
from unittest.mock import patch
from xknx import XKNX
from xknx.io import DEFAULT_MCAST_GRP
from xknx.io.gateway_scanner import GatewayDescriptor
from homeassistant import config_entries
from homeassistant.components.knx import ConnectionSchema
from homeassistant.components.knx.config_flow import (
CONF_KNX_GATEWAY,
DEFAULT_ENTRY_DATA,
)
from homeassistant.components.knx.const import (
CONF_KNX_AUTOMATIC,
CONF_KNX_CONNECTION_TYPE,
CONF_KNX_INDIVIDUAL_ADDRESS,
CONF_KNX_ROUTING,
CONF_KNX_TUNNELING,
DOMAIN,
)
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
from tests.common import MockConfigEntry
def _gateway_descriptor(ip: str, port: int) -> GatewayDescriptor:
"""Get mock gw descriptor."""
return GatewayDescriptor("Test", ip, port, "eth0", "127.0.0.1", True)
async def test_user_single_instance(hass):
"""Test we only allow a single config flow."""
MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "abort"
assert result["reason"] == "single_instance_allowed"
async def test_routing_setup(hass: HomeAssistant) -> None:
"""Test routing setup."""
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = []
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_FORM
assert result2["step_id"] == "routing"
assert not result2["errors"]
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
},
)
await hass.async_block_till_done()
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == CONF_KNX_ROUTING.capitalize()
assert result3["data"] == {
**DEFAULT_ENTRY_DATA,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_tunneling_setup(hass: HomeAssistant) -> None:
"""Test tunneling if only one gateway is found."""
gateway = _gateway_descriptor("192.168.0.1", 3675)
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = [gateway]
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_FORM
assert result2["step_id"] == "manual_tunnel"
assert not result2["errors"]
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_HOST: "192.168.0.1",
CONF_PORT: 3675,
},
)
await hass.async_block_till_done()
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == "Tunneling @ 192.168.0.1"
assert result3["data"] == {
**DEFAULT_ENTRY_DATA,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
CONF_HOST: "192.168.0.1",
CONF_PORT: 3675,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
ConnectionSchema.CONF_KNX_ROUTE_BACK: False,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) -> None:
"""Test tunneling if only one gateway is found."""
gateway = _gateway_descriptor("192.168.0.1", 3675)
gateway2 = _gateway_descriptor("192.168.1.100", 3675)
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = [gateway, gateway2]
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
tunnel_flow = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
},
)
await hass.async_block_till_done()
assert tunnel_flow["type"] == RESULT_TYPE_FORM
assert tunnel_flow["step_id"] == "tunnel"
assert not tunnel_flow["errors"]
manual_tunnel = await hass.config_entries.flow.async_configure(
tunnel_flow["flow_id"],
{CONF_KNX_GATEWAY: str(gateway)},
)
await hass.async_block_till_done()
assert manual_tunnel["type"] == RESULT_TYPE_FORM
assert manual_tunnel["step_id"] == "manual_tunnel"
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry:
manual_tunnel_flow = await hass.config_entries.flow.async_configure(
manual_tunnel["flow_id"],
{
CONF_HOST: "192.168.0.1",
CONF_PORT: 3675,
},
)
await hass.async_block_till_done()
assert manual_tunnel_flow["type"] == RESULT_TYPE_CREATE_ENTRY
assert manual_tunnel_flow["data"] == {
**DEFAULT_ENTRY_DATA,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
CONF_HOST: "192.168.0.1",
CONF_PORT: 3675,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
ConnectionSchema.CONF_KNX_ROUTE_BACK: False,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_manual_tunnel_step_when_no_gateway(hass: HomeAssistant) -> None:
"""Test manual tunnel if no gateway is found and tunneling is selected."""
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = []
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
tunnel_flow = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
},
)
await hass.async_block_till_done()
assert tunnel_flow["type"] == RESULT_TYPE_FORM
assert tunnel_flow["step_id"] == "manual_tunnel"
assert not tunnel_flow["errors"]
async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> None:
"""Test we get the form."""
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = [_gateway_descriptor("192.168.0.1", 3675)]
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize()
assert result2["data"] == {
**DEFAULT_ENTRY_DATA,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
}
assert len(mock_setup_entry.mock_calls) == 1
##
# Import Tests
##
async def test_import_config_tunneling(hass: HomeAssistant) -> None:
"""Test tunneling import from config.yaml."""
config = {
CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, # has a default in the original config
ConnectionSchema.CONF_KNX_STATE_UPDATER: True, # has a default in the original config
CONF_KNX_TUNNELING: {
CONF_HOST: "192.168.1.1",
CONF_PORT: 3675,
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
},
}
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Tunneling @ 192.168.1.1"
assert result["data"] == {
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
CONF_HOST: "192.168.1.1",
CONF_PORT: 3675,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
ConnectionSchema.CONF_KNX_STATE_UPDATER: True,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20,
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_import_config_routing(hass: HomeAssistant) -> None:
"""Test routing import from config.yaml."""
config = {
CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, # has a default in the original config
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, # has a default in the original config
ConnectionSchema.CONF_KNX_STATE_UPDATER: True, # has a default in the original config
CONF_KNX_ROUTING: {}, # is required when using routing
}
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == CONF_KNX_ROUTING.capitalize()
assert result["data"] == {
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_STATE_UPDATER: True,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_import_config_automatic(hass: HomeAssistant) -> None:
"""Test automatic import from config.yaml."""
config = {
CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, # has a default in the original config
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, # has a default in the original config
ConnectionSchema.CONF_KNX_STATE_UPDATER: True, # has a default in the original config
}
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == CONF_KNX_AUTOMATIC.capitalize()
assert result["data"] == {
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_STATE_UPDATER: True,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_import_rate_limit_out_of_range(hass: HomeAssistant) -> None:
"""Test automatic import from config.yaml."""
config = {
CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, # has a default in the original config
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config
ConnectionSchema.CONF_KNX_RATE_LIMIT: 80,
ConnectionSchema.CONF_KNX_STATE_UPDATER: True, # has a default in the original config
}
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == CONF_KNX_AUTOMATIC.capitalize()
assert result["data"] == {
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_STATE_UPDATER: True,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 60,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_import_options(hass: HomeAssistant) -> None:
"""Test import from config.yaml with options."""
config = {
CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, # has a default in the original config
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 30,
}
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == CONF_KNX_AUTOMATIC.capitalize()
assert result["data"] == {
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 30,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_abort_if_entry_exists_already(hass: HomeAssistant) -> None:
"""Test routing import from config.yaml."""
MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
)
assert result["type"] == "abort"
assert result["reason"] == "single_instance_allowed"
async def test_options_flow(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test options config flow."""
mock_config_entry.add_to_hass(hass)
gateway = _gateway_descriptor("192.168.0.1", 3675)
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = [gateway]
result = await hass.config_entries.options.async_init(
mock_config_entry.entry_id
)
assert result.get("type") == RESULT_TYPE_FORM
assert result.get("step_id") == "init"
assert "flow_id" in result
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
},
)
await hass.async_block_till_done()
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
assert not result2.get("data")
assert mock_config_entry.data == {
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255",
CONF_HOST: "",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20,
ConnectionSchema.CONF_KNX_STATE_UPDATER: True,
}
async def test_tunneling_options_flow(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test options flow for tunneling."""
mock_config_entry.add_to_hass(hass)
gateway = _gateway_descriptor("192.168.0.1", 3675)
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = [gateway]
result = await hass.config_entries.options.async_init(
mock_config_entry.entry_id
)
assert result.get("type") == RESULT_TYPE_FORM
assert result.get("step_id") == "init"
assert "flow_id" in result
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
},
)
assert result2.get("type") == RESULT_TYPE_FORM
assert not result2.get("data")
assert "flow_id" in result2
result3 = await hass.config_entries.options.async_configure(
result2["flow_id"],
user_input={
CONF_HOST: "192.168.1.1",
CONF_PORT: 3675,
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
},
)
await hass.async_block_till_done()
assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY
assert not result3.get("data")
assert mock_config_entry.data == {
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20,
ConnectionSchema.CONF_KNX_STATE_UPDATER: True,
CONF_HOST: "192.168.1.1",
CONF_PORT: 3675,
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
}
async def test_advanced_options(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test options config flow."""
mock_config_entry.add_to_hass(hass)
gateway = _gateway_descriptor("192.168.0.1", 3675)
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = [gateway]
result = await hass.config_entries.options.async_init(
mock_config_entry.entry_id, context={"show_advanced_options": True}
)
assert result.get("type") == RESULT_TYPE_FORM
assert result.get("step_id") == "init"
assert "flow_id" in result
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
},
)
await hass.async_block_till_done()
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
assert not result2.get("data")
assert mock_config_entry.data == {
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
CONF_HOST: "",
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
}