diff --git a/CODEOWNERS b/CODEOWNERS
index 84869fe7144..41914c61c67 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -223,7 +223,7 @@ homeassistant/components/keba/* @dannerph
homeassistant/components/keenetic_ndms2/* @foxel
homeassistant/components/kef/* @basnijholt
homeassistant/components/keyboard_remote/* @bendavid
-homeassistant/components/knx/* @Julius2342
+homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
homeassistant/components/kodi/* @armills @OnFreund
homeassistant/components/konnected/* @heythisisnate @kit-klein
homeassistant/components/lametric/* @robbiet480
diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py
index d5bfdcc0e57..4de801a19d1 100644
--- a/homeassistant/components/knx/__init__.py
+++ b/homeassistant/components/knx/__init__.py
@@ -3,8 +3,8 @@ import logging
import voluptuous as vol
from xknx import XKNX
-from xknx.devices import ActionCallback, DateTime, DateTimeBroadcastType, ExposeSensor
-from xknx.dpt import DPTArray, DPTBinary
+from xknx.devices import DateTime, ExposeSensor
+from xknx.dpt import DPTArray, DPTBase, DPTBinary
from xknx.exceptions import XKNXException
from xknx.io import DEFAULT_MCAST_PORT, ConnectionConfig, ConnectionType
from xknx.telegram import AddressFilter, GroupAddress, Telegram
@@ -23,60 +23,61 @@ from homeassistant.core import callback
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change_event
-from homeassistant.helpers.script import Script
+
+from .const import DATA_KNX, DOMAIN, DeviceTypes
+from .factory import create_knx_device
+from .schema import (
+ BinarySensorSchema,
+ ClimateSchema,
+ ConnectionSchema,
+ CoverSchema,
+ ExposeSchema,
+ LightSchema,
+ NotifySchema,
+ SceneSchema,
+ SensorSchema,
+ SwitchSchema,
+)
_LOGGER = logging.getLogger(__name__)
-DOMAIN = "knx"
-DATA_KNX = "data_knx"
CONF_KNX_CONFIG = "config_file"
CONF_KNX_ROUTING = "routing"
CONF_KNX_TUNNELING = "tunneling"
-CONF_KNX_LOCAL_IP = "local_ip"
CONF_KNX_FIRE_EVENT = "fire_event"
CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter"
CONF_KNX_STATE_UPDATER = "state_updater"
CONF_KNX_RATE_LIMIT = "rate_limit"
CONF_KNX_EXPOSE = "expose"
-CONF_KNX_EXPOSE_TYPE = "type"
-CONF_KNX_EXPOSE_ATTRIBUTE = "attribute"
-CONF_KNX_EXPOSE_DEFAULT = "default"
-CONF_KNX_EXPOSE_ADDRESS = "address"
+
+CONF_KNX_LIGHT = "light"
+CONF_KNX_COVER = "cover"
+CONF_KNX_BINARY_SENSOR = "binary_sensor"
+CONF_KNX_SCENE = "scene"
+CONF_KNX_SENSOR = "sensor"
+CONF_KNX_SWITCH = "switch"
+CONF_KNX_NOTIFY = "notify"
+CONF_KNX_CLIMATE = "climate"
SERVICE_KNX_SEND = "send"
SERVICE_KNX_ATTR_ADDRESS = "address"
SERVICE_KNX_ATTR_PAYLOAD = "payload"
+SERVICE_KNX_ATTR_TYPE = "type"
ATTR_DISCOVER_DEVICES = "devices"
-TUNNELING_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_KNX_LOCAL_IP): cv.string,
- vol.Optional(CONF_PORT): cv.port,
- }
-)
-
-ROUTING_SCHEMA = vol.Schema({vol.Optional(CONF_KNX_LOCAL_IP): cv.string})
-
-EXPOSE_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_KNX_EXPOSE_TYPE): cv.string,
- vol.Optional(CONF_ENTITY_ID): cv.entity_id,
- vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string,
- vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all,
- vol.Required(CONF_KNX_EXPOSE_ADDRESS): cv.string,
- }
-)
-
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(CONF_KNX_CONFIG): cv.string,
- vol.Exclusive(CONF_KNX_ROUTING, "connection_type"): ROUTING_SCHEMA,
- vol.Exclusive(CONF_KNX_TUNNELING, "connection_type"): TUNNELING_SCHEMA,
+ vol.Exclusive(
+ CONF_KNX_ROUTING, "connection_type"
+ ): ConnectionSchema.ROUTING_SCHEMA,
+ vol.Exclusive(
+ CONF_KNX_TUNNELING, "connection_type"
+ ): ConnectionSchema.TUNNELING_SCHEMA,
vol.Inclusive(CONF_KNX_FIRE_EVENT, "fire_ev"): cv.boolean,
vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, "fire_ev"): vol.All(
cv.ensure_list, [cv.string]
@@ -85,7 +86,33 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_KNX_RATE_LIMIT, default=20): vol.All(
vol.Coerce(int), vol.Range(min=1, max=100)
),
- vol.Optional(CONF_KNX_EXPOSE): vol.All(cv.ensure_list, [EXPOSE_SCHEMA]),
+ vol.Optional(CONF_KNX_EXPOSE): vol.All(
+ cv.ensure_list, [ExposeSchema.SCHEMA]
+ ),
+ vol.Optional(CONF_KNX_COVER): vol.All(
+ cv.ensure_list, [CoverSchema.SCHEMA]
+ ),
+ vol.Optional(CONF_KNX_BINARY_SENSOR): vol.All(
+ cv.ensure_list, [BinarySensorSchema.SCHEMA]
+ ),
+ vol.Optional(CONF_KNX_LIGHT): vol.All(
+ cv.ensure_list, [LightSchema.SCHEMA]
+ ),
+ vol.Optional(CONF_KNX_CLIMATE): vol.All(
+ cv.ensure_list, [ClimateSchema.SCHEMA]
+ ),
+ vol.Optional(CONF_KNX_NOTIFY): vol.All(
+ cv.ensure_list, [NotifySchema.SCHEMA]
+ ),
+ vol.Optional(CONF_KNX_SWITCH): vol.All(
+ cv.ensure_list, [SwitchSchema.SCHEMA]
+ ),
+ vol.Optional(CONF_KNX_SENSOR): vol.All(
+ cv.ensure_list, [SensorSchema.SCHEMA]
+ ),
+ vol.Optional(CONF_KNX_SCENE): vol.All(
+ cv.ensure_list, [SceneSchema.SCHEMA]
+ ),
}
)
},
@@ -98,9 +125,21 @@ SERVICE_KNX_SEND_SCHEMA = vol.Schema(
vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any(
cv.positive_int, [cv.positive_int]
),
+ vol.Optional(SERVICE_KNX_ATTR_TYPE): vol.Any(int, float, str),
}
)
+KNX_CONFIG_PLATFORM_MAPPING = {
+ CONF_KNX_COVER: DeviceTypes.cover,
+ CONF_KNX_SWITCH: DeviceTypes.switch,
+ CONF_KNX_LIGHT: DeviceTypes.light,
+ CONF_KNX_SENSOR: DeviceTypes.sensor,
+ CONF_KNX_NOTIFY: DeviceTypes.notify,
+ CONF_KNX_SCENE: DeviceTypes.scene,
+ CONF_KNX_BINARY_SENSOR: DeviceTypes.binary_sensor,
+ CONF_KNX_CLIMATE: DeviceTypes.climate,
+}
+
async def async_setup(hass, config):
"""Set up the KNX component."""
@@ -114,6 +153,15 @@ async def async_setup(hass, config):
f"Can't connect to KNX interface:
{ex}", title="KNX"
)
+ for platform_config, device_type in KNX_CONFIG_PLATFORM_MAPPING.items():
+ if platform_config in config[DOMAIN]:
+ for device_config in config[DOMAIN][platform_config]:
+ hass.data[DATA_KNX].xknx.devices.add(
+ create_knx_device(
+ hass, device_type, hass.data[DATA_KNX].xknx, device_config
+ )
+ )
+
for component, discovery_type in (
("switch", "Switch"),
("climate", "Climate"),
@@ -203,11 +251,15 @@ class KNXModule:
return self.connection_config_tunneling()
if CONF_KNX_ROUTING in self.config[DOMAIN]:
return self.connection_config_routing()
- return self.connection_config_auto()
+ # return None to let xknx use config from xknx.yaml connection block if given
+ # otherwise it will use default ConnectionConfig (Automatic)
+ return None
def connection_config_routing(self):
"""Return the connection_config if routing is configured."""
- local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get(CONF_KNX_LOCAL_IP)
+ local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get(
+ ConnectionSchema.CONF_KNX_LOCAL_IP
+ )
return ConnectionConfig(
connection_type=ConnectionType.ROUTING, local_ip=local_ip
)
@@ -216,7 +268,9 @@ class KNXModule:
"""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].get(CONF_PORT)
- local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_KNX_LOCAL_IP)
+ local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(
+ ConnectionSchema.CONF_KNX_LOCAL_IP
+ )
if gateway_port is None:
gateway_port = DEFAULT_MCAST_PORT
return ConnectionConfig(
@@ -227,11 +281,6 @@ class KNXModule:
auto_reconnect=True,
)
- def connection_config_auto(self):
- """Return the connection_config if auto is configured."""
- # pylint: disable=no-self-use
- return ConnectionConfig()
-
def register_callbacks(self):
"""Register callbacks within XKNX object."""
if (
@@ -251,11 +300,11 @@ class KNXModule:
if CONF_KNX_EXPOSE not in self.config[DOMAIN]:
return
for to_expose in self.config[DOMAIN][CONF_KNX_EXPOSE]:
- expose_type = to_expose.get(CONF_KNX_EXPOSE_TYPE)
+ expose_type = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_TYPE)
entity_id = to_expose.get(CONF_ENTITY_ID)
- attribute = to_expose.get(CONF_KNX_EXPOSE_ATTRIBUTE)
- default = to_expose.get(CONF_KNX_EXPOSE_DEFAULT)
- address = to_expose.get(CONF_KNX_EXPOSE_ADDRESS)
+ attribute = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE)
+ default = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT)
+ address = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_ADDRESS)
if expose_type in ["time", "date", "datetime"]:
exposure = KNXExposeTime(self.xknx, expose_type, address)
exposure.async_register()
@@ -286,9 +335,15 @@ class KNXModule:
"""Service for sending an arbitrary KNX message to the KNX bus."""
attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD)
attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS)
+ attr_type = call.data.get(SERVICE_KNX_ATTR_TYPE)
def calculate_payload(attr_payload):
"""Calculate payload depending on type of attribute."""
+ 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}")
+ return DPTArray(transcoder.to_knx(attr_payload))
if isinstance(attr_payload, int):
return DPTBinary(attr_payload)
return DPTArray(attr_payload)
@@ -302,22 +357,6 @@ class KNXModule:
await self.xknx.telegrams.put(telegram)
-class KNXAutomation:
- """Wrapper around xknx.devices.ActionCallback object.."""
-
- def __init__(self, hass, device, hook, action, counter=1):
- """Initialize Automation class."""
- self.hass = hass
- self.device = device
- script_name = f"{device.get_name()} turn ON script"
- self.script = Script(hass, action, script_name, DOMAIN)
-
- self.action = ActionCallback(
- hass.data[DATA_KNX].xknx, self.script.async_run, hook=hook, counter=counter
- )
- device.actions.append(self.action)
-
-
class KNXExposeTime:
"""Object to Expose Time/Date object to KNX bus."""
@@ -332,7 +371,7 @@ class KNXExposeTime:
def async_register(self):
"""Register listener."""
broadcast_type_string = self.type.upper()
- broadcast_type = DateTimeBroadcastType[broadcast_type_string]
+ broadcast_type = broadcast_type_string
self.device = DateTime(
self.xknx, "Time", broadcast_type=broadcast_type, group_address=self.address
)
diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py
index 29effaa7ebf..a18889e122a 100644
--- a/homeassistant/components/knx/binary_sensor.py
+++ b/homeassistant/components/knx/binary_sensor.py
@@ -1,60 +1,16 @@
"""Support for KNX/IP binary sensors."""
-import voluptuous as vol
-from xknx.devices import BinarySensor
+from xknx.devices import BinarySensor as XknxBinarySensor
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
-from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
+from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.core import callback
-import homeassistant.helpers.config_validation as cv
-from . import ATTR_DISCOVER_DEVICES, DATA_KNX, KNXAutomation
-
-CONF_STATE_ADDRESS = "state_address"
-CONF_SIGNIFICANT_BIT = "significant_bit"
-CONF_DEFAULT_SIGNIFICANT_BIT = 1
-CONF_SYNC_STATE = "sync_state"
-CONF_AUTOMATION = "automation"
-CONF_HOOK = "hook"
-CONF_DEFAULT_HOOK = "on"
-CONF_COUNTER = "counter"
-CONF_DEFAULT_COUNTER = 1
-CONF_ACTION = "action"
-CONF_RESET_AFTER = "reset_after"
-
-CONF__ACTION = "turn_off_action"
-
-DEFAULT_NAME = "KNX Binary Sensor"
-AUTOMATION_SCHEMA = vol.Schema(
- {
- vol.Optional(CONF_HOOK, default=CONF_DEFAULT_HOOK): cv.string,
- vol.Optional(CONF_COUNTER, default=CONF_DEFAULT_COUNTER): cv.port,
- vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
- }
-)
-
-AUTOMATIONS_SCHEMA = vol.All(cv.ensure_list, [AUTOMATION_SCHEMA])
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(
- CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT
- ): cv.positive_int,
- vol.Optional(CONF_SYNC_STATE, default=True): cv.boolean,
- vol.Required(CONF_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_DEVICE_CLASS): cv.string,
- vol.Optional(CONF_RESET_AFTER): cv.positive_int,
- vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA,
- }
-)
+from . import ATTR_DISCOVER_DEVICES, DATA_KNX
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up binary sensor(s) for KNX platform."""
if discovery_info is not None:
async_add_entities_discovery(hass, discovery_info, async_add_entities)
- else:
- async_add_entities_config(hass, config, async_add_entities)
@callback
@@ -67,48 +23,12 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
async_add_entities(entities)
-@callback
-def async_add_entities_config(hass, config, async_add_entities):
- """Set up binary senor for KNX platform configured within platform."""
- name = config[CONF_NAME]
-
- binary_sensor = BinarySensor(
- hass.data[DATA_KNX].xknx,
- name=name,
- group_address_state=config[CONF_STATE_ADDRESS],
- sync_state=config[CONF_SYNC_STATE],
- device_class=config.get(CONF_DEVICE_CLASS),
- significant_bit=config[CONF_SIGNIFICANT_BIT],
- reset_after=config.get(CONF_RESET_AFTER),
- )
- hass.data[DATA_KNX].xknx.devices.add(binary_sensor)
-
- entity = KNXBinarySensor(binary_sensor)
- automations = config.get(CONF_AUTOMATION)
- if automations is not None:
- for automation in automations:
- counter = automation[CONF_COUNTER]
- hook = automation[CONF_HOOK]
- action = automation[CONF_ACTION]
- entity.automations.append(
- KNXAutomation(
- hass=hass,
- device=binary_sensor,
- hook=hook,
- action=action,
- counter=counter,
- )
- )
- async_add_entities([entity])
-
-
class KNXBinarySensor(BinarySensorEntity):
"""Representation of a KNX binary sensor."""
- def __init__(self, device):
+ def __init__(self, device: XknxBinarySensor):
"""Initialize of KNX binary sensor."""
self.device = device
- self.automations = []
@callback
def async_register_callbacks(self):
diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py
index a58e5312c11..db0559e5158 100644
--- a/homeassistant/components/knx/climate.py
+++ b/homeassistant/components/knx/climate.py
@@ -1,127 +1,31 @@
"""Support for KNX/IP climate devices."""
from typing import List, Optional
-import voluptuous as vol
-from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode
+from xknx.devices import Climate as XknxClimate
from xknx.dpt import HVACOperationMode
-from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity
+from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
- HVAC_MODE_AUTO,
- HVAC_MODE_COOL,
- HVAC_MODE_DRY,
- HVAC_MODE_FAN_ONLY,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
PRESET_AWAY,
- PRESET_COMFORT,
- PRESET_ECO,
- PRESET_SLEEP,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
)
-from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS
+from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.core import callback
-import homeassistant.helpers.config_validation as cv
from . import ATTR_DISCOVER_DEVICES, DATA_KNX
-
-CONF_SETPOINT_SHIFT_ADDRESS = "setpoint_shift_address"
-CONF_SETPOINT_SHIFT_STATE_ADDRESS = "setpoint_shift_state_address"
-CONF_SETPOINT_SHIFT_STEP = "setpoint_shift_step"
-CONF_SETPOINT_SHIFT_MAX = "setpoint_shift_max"
-CONF_SETPOINT_SHIFT_MIN = "setpoint_shift_min"
-CONF_TEMPERATURE_ADDRESS = "temperature_address"
-CONF_TARGET_TEMPERATURE_ADDRESS = "target_temperature_address"
-CONF_TARGET_TEMPERATURE_STATE_ADDRESS = "target_temperature_state_address"
-CONF_OPERATION_MODE_ADDRESS = "operation_mode_address"
-CONF_OPERATION_MODE_STATE_ADDRESS = "operation_mode_state_address"
-CONF_CONTROLLER_STATUS_ADDRESS = "controller_status_address"
-CONF_CONTROLLER_STATUS_STATE_ADDRESS = "controller_status_state_address"
-CONF_CONTROLLER_MODE_ADDRESS = "controller_mode_address"
-CONF_CONTROLLER_MODE_STATE_ADDRESS = "controller_mode_state_address"
-CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = "operation_mode_frost_protection_address"
-CONF_OPERATION_MODE_NIGHT_ADDRESS = "operation_mode_night_address"
-CONF_OPERATION_MODE_COMFORT_ADDRESS = "operation_mode_comfort_address"
-CONF_OPERATION_MODES = "operation_modes"
-CONF_ON_OFF_ADDRESS = "on_off_address"
-CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address"
-CONF_ON_OFF_INVERT = "on_off_invert"
-CONF_MIN_TEMP = "min_temp"
-CONF_MAX_TEMP = "max_temp"
-
-DEFAULT_NAME = "KNX Climate"
-DEFAULT_SETPOINT_SHIFT_STEP = 0.5
-DEFAULT_SETPOINT_SHIFT_MAX = 6
-DEFAULT_SETPOINT_SHIFT_MIN = -6
-DEFAULT_ON_OFF_INVERT = False
-# Map KNX operation modes to HA modes. This list might not be full.
-OPERATION_MODES = {
- # Map DPT 201.105 HVAC control modes
- "Auto": HVAC_MODE_AUTO,
- "Heat": HVAC_MODE_HEAT,
- "Cool": HVAC_MODE_COOL,
- "Off": HVAC_MODE_OFF,
- "Fan only": HVAC_MODE_FAN_ONLY,
- "Dry": HVAC_MODE_DRY,
-}
+from .const import OPERATION_MODES, PRESET_MODES
OPERATION_MODES_INV = dict(reversed(item) for item in OPERATION_MODES.items())
-
-PRESET_MODES = {
- # Map DPT 201.100 HVAC operating modes to HA presets
- "Frost Protection": PRESET_ECO,
- "Night": PRESET_SLEEP,
- "Standby": PRESET_AWAY,
- "Comfort": PRESET_COMFORT,
-}
-
PRESET_MODES_INV = dict(reversed(item) for item in PRESET_MODES.items())
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(
- CONF_SETPOINT_SHIFT_STEP, default=DEFAULT_SETPOINT_SHIFT_STEP
- ): vol.All(float, vol.Range(min=0, max=2)),
- vol.Optional(
- CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX
- ): vol.All(int, vol.Range(min=0, max=32)),
- vol.Optional(
- CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN
- ): vol.All(int, vol.Range(min=-32, max=0)),
- vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string,
- vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string,
- vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): cv.string,
- vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string,
- vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string,
- vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): cv.string,
- vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string,
- vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string,
- vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string,
- vol.Optional(CONF_ON_OFF_ADDRESS): cv.string,
- vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT): cv.boolean,
- vol.Optional(CONF_OPERATION_MODES): vol.All(
- cv.ensure_list, [vol.In({**OPERATION_MODES, **PRESET_MODES})]
- ),
- vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
- vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
- }
-)
-
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up climate(s) for KNX platform."""
if discovery_info is not None:
async_add_entities_discovery(hass, discovery_info, async_add_entities)
- else:
- async_add_entities_config(hass, config, async_add_entities)
@callback
@@ -134,68 +38,10 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
async_add_entities(entities)
-@callback
-def async_add_entities_config(hass, config, async_add_entities):
- """Set up climate for KNX platform configured within platform."""
- climate_mode = XknxClimateMode(
- hass.data[DATA_KNX].xknx,
- name=f"{config[CONF_NAME]} Mode",
- group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS),
- group_address_operation_mode_state=config.get(
- CONF_OPERATION_MODE_STATE_ADDRESS
- ),
- group_address_controller_status=config.get(CONF_CONTROLLER_STATUS_ADDRESS),
- group_address_controller_status_state=config.get(
- CONF_CONTROLLER_STATUS_STATE_ADDRESS
- ),
- group_address_controller_mode=config.get(CONF_CONTROLLER_MODE_ADDRESS),
- group_address_controller_mode_state=config.get(
- CONF_CONTROLLER_MODE_STATE_ADDRESS
- ),
- group_address_operation_mode_protection=config.get(
- CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS
- ),
- group_address_operation_mode_night=config.get(
- CONF_OPERATION_MODE_NIGHT_ADDRESS
- ),
- group_address_operation_mode_comfort=config.get(
- CONF_OPERATION_MODE_COMFORT_ADDRESS
- ),
- operation_modes=config.get(CONF_OPERATION_MODES),
- )
- hass.data[DATA_KNX].xknx.devices.add(climate_mode)
-
- climate = XknxClimate(
- hass.data[DATA_KNX].xknx,
- name=config[CONF_NAME],
- group_address_temperature=config[CONF_TEMPERATURE_ADDRESS],
- group_address_target_temperature=config.get(CONF_TARGET_TEMPERATURE_ADDRESS),
- group_address_target_temperature_state=config[
- CONF_TARGET_TEMPERATURE_STATE_ADDRESS
- ],
- group_address_setpoint_shift=config.get(CONF_SETPOINT_SHIFT_ADDRESS),
- group_address_setpoint_shift_state=config.get(
- CONF_SETPOINT_SHIFT_STATE_ADDRESS
- ),
- setpoint_shift_step=config[CONF_SETPOINT_SHIFT_STEP],
- setpoint_shift_max=config[CONF_SETPOINT_SHIFT_MAX],
- setpoint_shift_min=config[CONF_SETPOINT_SHIFT_MIN],
- group_address_on_off=config.get(CONF_ON_OFF_ADDRESS),
- group_address_on_off_state=config.get(CONF_ON_OFF_STATE_ADDRESS),
- min_temp=config.get(CONF_MIN_TEMP),
- max_temp=config.get(CONF_MAX_TEMP),
- mode=climate_mode,
- on_off_invert=config[CONF_ON_OFF_INVERT],
- )
- hass.data[DATA_KNX].xknx.devices.add(climate)
-
- async_add_entities([KNXClimate(climate)])
-
-
class KNXClimate(ClimateEntity):
"""Representation of a KNX climate device."""
- def __init__(self, device):
+ def __init__(self, device: XknxClimate):
"""Initialize of a KNX climate device."""
self.device = device
self._unit_of_measurement = TEMP_CELSIUS
@@ -278,8 +124,6 @@ class KNXClimate(ClimateEntity):
"""Return current operation ie. heat, cool, idle."""
if self.device.supports_on_off and not self.device.is_on:
return HVAC_MODE_OFF
- if self.device.supports_on_off and self.device.is_on:
- return HVAC_MODE_HEAT
if self.device.mode.supports_operation_mode:
return OPERATION_MODES.get(
self.device.mode.operation_mode.value, HVAC_MODE_HEAT
@@ -296,10 +140,11 @@ class KNXClimate(ClimateEntity):
]
if self.device.supports_on_off:
- _operations.append(HVAC_MODE_HEAT)
+ if not _operations:
+ _operations.append(HVAC_MODE_HEAT)
_operations.append(HVAC_MODE_OFF)
- _modes = list(filter(None, _operations))
+ _modes = list(set(filter(None, _operations)))
# default to ["heat"]
return _modes if _modes else [HVAC_MODE_HEAT]
@@ -307,12 +152,15 @@ class KNXClimate(ClimateEntity):
"""Set operation mode."""
if self.device.supports_on_off and hvac_mode == HVAC_MODE_OFF:
await self.device.turn_off()
- elif self.device.supports_on_off and hvac_mode == HVAC_MODE_HEAT:
- await self.device.turn_on()
- elif self.device.mode.supports_operation_mode:
- knx_operation_mode = HVACOperationMode(OPERATION_MODES_INV.get(hvac_mode))
- await self.device.mode.set_operation_mode(knx_operation_mode)
- self.async_write_ha_state()
+ else:
+ if self.device.supports_on_off and not self.device.is_on:
+ await self.device.turn_on()
+ if self.device.mode.supports_operation_mode:
+ knx_operation_mode = HVACOperationMode(
+ OPERATION_MODES_INV.get(hvac_mode)
+ )
+ await self.device.mode.set_operation_mode(knx_operation_mode)
+ self.async_write_ha_state()
@property
def preset_mode(self) -> Optional[str]:
diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py
new file mode 100644
index 00000000000..fefb0cd73c0
--- /dev/null
+++ b/homeassistant/components/knx/const.py
@@ -0,0 +1,61 @@
+"""Constants for the KNX integration."""
+from enum import Enum
+
+from homeassistant.components.climate.const import (
+ HVAC_MODE_AUTO,
+ HVAC_MODE_COOL,
+ HVAC_MODE_DRY,
+ HVAC_MODE_FAN_ONLY,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_OFF,
+ PRESET_AWAY,
+ PRESET_COMFORT,
+ PRESET_ECO,
+ PRESET_SLEEP,
+)
+
+DOMAIN = "knx"
+DATA_KNX = "data_knx"
+
+CONF_STATE_ADDRESS = "state_address"
+CONF_SYNC_STATE = "sync_state"
+
+
+class ColorTempModes(Enum):
+ """Color temperature modes for config validation."""
+
+ absolute = "DPT-7.600"
+ relative = "DPT-5.001"
+
+
+class DeviceTypes(Enum):
+ """KNX device types."""
+
+ cover = "cover"
+ light = "light"
+ binary_sensor = "binary_sensor"
+ climate = "climate"
+ switch = "switch"
+ notify = "notify"
+ scene = "scene"
+ sensor = "sensor"
+
+
+# Map KNX operation modes to HA modes. This list might not be complete.
+OPERATION_MODES = {
+ # Map DPT 20.105 HVAC control modes
+ "Auto": HVAC_MODE_AUTO,
+ "Heat": HVAC_MODE_HEAT,
+ "Cool": HVAC_MODE_COOL,
+ "Off": HVAC_MODE_OFF,
+ "Fan only": HVAC_MODE_FAN_ONLY,
+ "Dry": HVAC_MODE_DRY,
+}
+
+PRESET_MODES = {
+ # Map DPT 20.102 HVAC operating modes to HA presets
+ "Frost Protection": PRESET_ECO,
+ "Night": PRESET_SLEEP,
+ "Standby": PRESET_AWAY,
+ "Comfort": PRESET_COMFORT,
+}
diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py
index 731105f6629..583f41c48ca 100644
--- a/homeassistant/components/knx/cover.py
+++ b/homeassistant/components/knx/cover.py
@@ -1,11 +1,10 @@
"""Support for KNX/IP covers."""
-import voluptuous as vol
from xknx.devices import Cover as XknxCover
from homeassistant.components.cover import (
ATTR_POSITION,
ATTR_TILT_POSITION,
- PLATFORM_SCHEMA,
+ DEVICE_CLASS_BLIND,
SUPPORT_CLOSE,
SUPPORT_OPEN,
SUPPORT_SET_POSITION,
@@ -13,53 +12,16 @@ from homeassistant.components.cover import (
SUPPORT_STOP,
CoverEntity,
)
-from homeassistant.const import CONF_NAME
from homeassistant.core import callback
-import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_utc_time_change
from . import ATTR_DISCOVER_DEVICES, DATA_KNX
-CONF_MOVE_LONG_ADDRESS = "move_long_address"
-CONF_MOVE_SHORT_ADDRESS = "move_short_address"
-CONF_POSITION_ADDRESS = "position_address"
-CONF_POSITION_STATE_ADDRESS = "position_state_address"
-CONF_ANGLE_ADDRESS = "angle_address"
-CONF_ANGLE_STATE_ADDRESS = "angle_state_address"
-CONF_TRAVELLING_TIME_DOWN = "travelling_time_down"
-CONF_TRAVELLING_TIME_UP = "travelling_time_up"
-CONF_INVERT_POSITION = "invert_position"
-CONF_INVERT_ANGLE = "invert_angle"
-
-DEFAULT_TRAVEL_TIME = 25
-DEFAULT_NAME = "KNX Cover"
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_MOVE_LONG_ADDRESS): cv.string,
- vol.Optional(CONF_MOVE_SHORT_ADDRESS): cv.string,
- vol.Optional(CONF_POSITION_ADDRESS): cv.string,
- vol.Optional(CONF_POSITION_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_ANGLE_ADDRESS): cv.string,
- vol.Optional(CONF_ANGLE_STATE_ADDRESS): cv.string,
- vol.Optional(
- CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME
- ): cv.positive_int,
- vol.Optional(
- CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME
- ): cv.positive_int,
- vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
- vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
- }
-)
-
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up cover(s) for KNX platform."""
if discovery_info is not None:
async_add_entities_discovery(hass, discovery_info, async_add_entities)
- else:
- async_add_entities_config(hass, config, async_add_entities)
@callback
@@ -72,32 +34,10 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
async_add_entities(entities)
-@callback
-def async_add_entities_config(hass, config, async_add_entities):
- """Set up cover for KNX platform configured within platform."""
- cover = XknxCover(
- hass.data[DATA_KNX].xknx,
- name=config[CONF_NAME],
- group_address_long=config.get(CONF_MOVE_LONG_ADDRESS),
- group_address_short=config.get(CONF_MOVE_SHORT_ADDRESS),
- group_address_position_state=config.get(CONF_POSITION_STATE_ADDRESS),
- group_address_angle=config.get(CONF_ANGLE_ADDRESS),
- group_address_angle_state=config.get(CONF_ANGLE_STATE_ADDRESS),
- group_address_position=config.get(CONF_POSITION_ADDRESS),
- travel_time_down=config[CONF_TRAVELLING_TIME_DOWN],
- travel_time_up=config[CONF_TRAVELLING_TIME_UP],
- invert_position=config[CONF_INVERT_POSITION],
- invert_angle=config[CONF_INVERT_ANGLE],
- )
-
- hass.data[DATA_KNX].xknx.devices.add(cover)
- async_add_entities([KNXCover(cover)])
-
-
class KNXCover(CoverEntity):
"""Representation of a KNX cover."""
- def __init__(self, device):
+ def __init__(self, device: XknxCover):
"""Initialize the cover."""
self.device = device
self._unsubscribe_auto_updater = None
@@ -109,6 +49,8 @@ class KNXCover(CoverEntity):
async def after_update_callback(device):
"""Call after device was updated."""
self.async_write_ha_state()
+ if self.device.is_traveling():
+ self.start_auto_updater()
self.device.register_device_updated_cb(after_update_callback)
@@ -135,44 +77,62 @@ class KNXCover(CoverEntity):
"""No polling needed within KNX."""
return False
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ if self.device.supports_angle:
+ return DEVICE_CLASS_BLIND
+ return None
+
@property
def supported_features(self):
"""Flag supported features."""
- supported_features = (
- SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION | SUPPORT_STOP
- )
+ supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
+ if self.device.supports_stop:
+ supported_features |= SUPPORT_STOP
if self.device.supports_angle:
supported_features |= SUPPORT_SET_TILT_POSITION
return supported_features
@property
def current_cover_position(self):
- """Return the current position of the cover."""
- return self.device.current_position()
+ """Return the current position of the cover.
+
+ None is unknown, 0 is closed, 100 is fully open.
+ """
+ # In KNX 0 is open, 100 is closed.
+ try:
+ return 100 - self.device.current_position()
+ except TypeError:
+ return None
@property
def is_closed(self):
"""Return if the cover is closed."""
return self.device.is_closed()
+ @property
+ def is_opening(self):
+ """Return if the cover is opening or not."""
+ return self.device.is_opening()
+
+ @property
+ def is_closing(self):
+ """Return if the cover is closing or not."""
+ return self.device.is_closing()
+
async def async_close_cover(self, **kwargs):
"""Close the cover."""
- if not self.device.is_closed():
- await self.device.set_down()
- self.start_auto_updater()
+ await self.device.set_down()
async def async_open_cover(self, **kwargs):
"""Open the cover."""
- if not self.device.is_open():
- await self.device.set_up()
- self.start_auto_updater()
+ await self.device.set_up()
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
- if ATTR_POSITION in kwargs:
- position = kwargs[ATTR_POSITION]
- await self.device.set_position(position)
- self.start_auto_updater()
+ knx_position = 100 - kwargs[ATTR_POSITION]
+ await self.device.set_position(knx_position)
async def async_stop_cover(self, **kwargs):
"""Stop the cover."""
@@ -184,13 +144,15 @@ class KNXCover(CoverEntity):
"""Return current tilt position of cover."""
if not self.device.supports_angle:
return None
- return self.device.current_angle()
+ try:
+ return 100 - self.device.current_angle()
+ except TypeError:
+ return None
async def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
- if ATTR_TILT_POSITION in kwargs:
- tilt_position = kwargs[ATTR_TILT_POSITION]
- await self.device.set_angle(tilt_position)
+ knx_tilt_position = 100 - kwargs[ATTR_TILT_POSITION]
+ await self.device.set_angle(knx_tilt_position)
def start_auto_updater(self):
"""Start the autoupdater to update Home Assistant while cover is moving."""
diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py
new file mode 100644
index 00000000000..f53a7436122
--- /dev/null
+++ b/homeassistant/components/knx/factory.py
@@ -0,0 +1,263 @@
+"""Factory function to initialize KNX devices from config."""
+from xknx import XKNX
+from xknx.devices import (
+ ActionCallback as XknxActionCallback,
+ BinarySensor as XknxBinarySensor,
+ Climate as XknxClimate,
+ ClimateMode as XknxClimateMode,
+ Cover as XknxCover,
+ Device as XknxDevice,
+ Light as XknxLight,
+ Notification as XknxNotification,
+ Scene as XknxScene,
+ Sensor as XknxSensor,
+ Switch as XknxSwitch,
+)
+
+from homeassistant.const import CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME, CONF_TYPE
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.script import Script
+from homeassistant.helpers.typing import ConfigType
+
+from .const import DATA_KNX, DOMAIN, ColorTempModes, DeviceTypes
+from .schema import (
+ BinarySensorSchema,
+ ClimateSchema,
+ CoverSchema,
+ LightSchema,
+ SceneSchema,
+ SensorSchema,
+ SwitchSchema,
+)
+
+
+def create_knx_device(
+ hass: HomeAssistant, device_type: DeviceTypes, knx_module: XKNX, config: ConfigType
+) -> XknxDevice:
+ """Return the requested XKNX device."""
+ if device_type is DeviceTypes.light:
+ return _create_light(knx_module, config)
+
+ if device_type is DeviceTypes.cover:
+ return _create_cover(knx_module, config)
+
+ if device_type is DeviceTypes.climate:
+ return _create_climate(hass, knx_module, config)
+
+ if device_type is DeviceTypes.switch:
+ return _create_switch(knx_module, config)
+
+ if device_type is DeviceTypes.sensor:
+ return _create_sensor(knx_module, config)
+
+ if device_type is DeviceTypes.notify:
+ return _create_notify(knx_module, config)
+
+ if device_type is DeviceTypes.scene:
+ return _create_scene(knx_module, config)
+
+ if device_type is DeviceTypes.binary_sensor:
+ return _create_binary_sensor(hass, knx_module, config)
+
+
+def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover:
+ """Return a KNX Cover device to be used within XKNX."""
+ return XknxCover(
+ knx_module,
+ name=config[CONF_NAME],
+ group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS),
+ group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS),
+ group_address_stop=config.get(CoverSchema.CONF_STOP_ADDRESS),
+ group_address_position_state=config.get(
+ CoverSchema.CONF_POSITION_STATE_ADDRESS
+ ),
+ group_address_angle=config.get(CoverSchema.CONF_ANGLE_ADDRESS),
+ group_address_angle_state=config.get(CoverSchema.CONF_ANGLE_STATE_ADDRESS),
+ group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS),
+ travel_time_down=config[CoverSchema.CONF_TRAVELLING_TIME_DOWN],
+ travel_time_up=config[CoverSchema.CONF_TRAVELLING_TIME_UP],
+ invert_position=config[CoverSchema.CONF_INVERT_POSITION],
+ invert_angle=config[CoverSchema.CONF_INVERT_ANGLE],
+ )
+
+
+def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight:
+ """Return a KNX Light device to be used within XKNX."""
+ group_address_tunable_white = None
+ group_address_tunable_white_state = None
+ group_address_color_temp = None
+ group_address_color_temp_state = None
+ if config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.absolute:
+ group_address_color_temp = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS)
+ group_address_color_temp_state = config.get(
+ LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS
+ )
+ elif config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.relative:
+ group_address_tunable_white = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS)
+ group_address_tunable_white_state = config.get(
+ LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS
+ )
+
+ return XknxLight(
+ knx_module,
+ name=config[CONF_NAME],
+ group_address_switch=config[CONF_ADDRESS],
+ group_address_switch_state=config.get(LightSchema.CONF_STATE_ADDRESS),
+ group_address_brightness=config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS),
+ group_address_brightness_state=config.get(
+ LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS
+ ),
+ group_address_color=config.get(LightSchema.CONF_COLOR_ADDRESS),
+ group_address_color_state=config.get(LightSchema.CONF_COLOR_STATE_ADDRESS),
+ group_address_rgbw=config.get(LightSchema.CONF_RGBW_ADDRESS),
+ group_address_rgbw_state=config.get(LightSchema.CONF_RGBW_STATE_ADDRESS),
+ group_address_tunable_white=group_address_tunable_white,
+ group_address_tunable_white_state=group_address_tunable_white_state,
+ group_address_color_temperature=group_address_color_temp,
+ group_address_color_temperature_state=group_address_color_temp_state,
+ min_kelvin=config[LightSchema.CONF_MIN_KELVIN],
+ max_kelvin=config[LightSchema.CONF_MAX_KELVIN],
+ )
+
+
+def _create_climate(
+ hass: HomeAssistant, knx_module: XKNX, config: ConfigType
+) -> XknxClimate:
+ """Return a KNX Climate device to be used within XKNX."""
+ climate_mode = XknxClimateMode(
+ knx_module,
+ name=f"{config[CONF_NAME]} Mode",
+ group_address_operation_mode=config.get(
+ ClimateSchema.CONF_OPERATION_MODE_ADDRESS
+ ),
+ group_address_operation_mode_state=config.get(
+ ClimateSchema.CONF_OPERATION_MODE_STATE_ADDRESS
+ ),
+ group_address_controller_status=config.get(
+ ClimateSchema.CONF_CONTROLLER_STATUS_ADDRESS
+ ),
+ group_address_controller_status_state=config.get(
+ ClimateSchema.CONF_CONTROLLER_STATUS_STATE_ADDRESS
+ ),
+ group_address_controller_mode=config.get(
+ ClimateSchema.CONF_CONTROLLER_MODE_ADDRESS
+ ),
+ group_address_controller_mode_state=config.get(
+ ClimateSchema.CONF_CONTROLLER_MODE_STATE_ADDRESS
+ ),
+ group_address_operation_mode_protection=config.get(
+ ClimateSchema.CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS
+ ),
+ group_address_operation_mode_night=config.get(
+ ClimateSchema.CONF_OPERATION_MODE_NIGHT_ADDRESS
+ ),
+ group_address_operation_mode_comfort=config.get(
+ ClimateSchema.CONF_OPERATION_MODE_COMFORT_ADDRESS
+ ),
+ group_address_operation_mode_standby=config.get(
+ ClimateSchema.CONF_OPERATION_MODE_STANDBY_ADDRESS
+ ),
+ group_address_heat_cool=config.get(ClimateSchema.CONF_HEAT_COOL_ADDRESS),
+ group_address_heat_cool_state=config.get(
+ ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS
+ ),
+ operation_modes=config.get(ClimateSchema.CONF_OPERATION_MODES),
+ )
+ hass.data[DATA_KNX].xknx.devices.add(climate_mode)
+
+ return XknxClimate(
+ knx_module,
+ name=config[CONF_NAME],
+ group_address_temperature=config[ClimateSchema.CONF_TEMPERATURE_ADDRESS],
+ group_address_target_temperature=config.get(
+ ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS
+ ),
+ group_address_target_temperature_state=config[
+ ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS
+ ],
+ group_address_setpoint_shift=config.get(
+ ClimateSchema.CONF_SETPOINT_SHIFT_ADDRESS
+ ),
+ group_address_setpoint_shift_state=config.get(
+ ClimateSchema.CONF_SETPOINT_SHIFT_STATE_ADDRESS
+ ),
+ setpoint_shift_mode=config[ClimateSchema.CONF_SETPOINT_SHIFT_MODE],
+ setpoint_shift_max=config[ClimateSchema.CONF_SETPOINT_SHIFT_MAX],
+ setpoint_shift_min=config[ClimateSchema.CONF_SETPOINT_SHIFT_MIN],
+ temperature_step=config[ClimateSchema.CONF_TEMPERATURE_STEP],
+ group_address_on_off=config.get(ClimateSchema.CONF_ON_OFF_ADDRESS),
+ group_address_on_off_state=config.get(ClimateSchema.CONF_ON_OFF_STATE_ADDRESS),
+ min_temp=config.get(ClimateSchema.CONF_MIN_TEMP),
+ max_temp=config.get(ClimateSchema.CONF_MAX_TEMP),
+ mode=climate_mode,
+ on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT],
+ )
+
+
+def _create_switch(knx_module: XKNX, config: ConfigType) -> XknxSwitch:
+ """Return a KNX switch to be used within XKNX."""
+ return XknxSwitch(
+ knx_module,
+ name=config[CONF_NAME],
+ group_address=config[CONF_ADDRESS],
+ group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS),
+ )
+
+
+def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor:
+ """Return a KNX sensor to be used within XKNX."""
+ return XknxSensor(
+ knx_module,
+ name=config[CONF_NAME],
+ group_address_state=config[SensorSchema.CONF_STATE_ADDRESS],
+ sync_state=config[SensorSchema.CONF_SYNC_STATE],
+ value_type=config[CONF_TYPE],
+ )
+
+
+def _create_notify(knx_module: XKNX, config: ConfigType) -> XknxNotification:
+ """Return a KNX notification to be used within XKNX."""
+ return XknxNotification(
+ knx_module, name=config[CONF_NAME], group_address=config[CONF_ADDRESS],
+ )
+
+
+def _create_scene(knx_module: XKNX, config: ConfigType) -> XknxScene:
+ """Return a KNX scene to be used within XKNX."""
+ return XknxScene(
+ knx_module,
+ name=config[CONF_NAME],
+ group_address=config[CONF_ADDRESS],
+ scene_number=config[SceneSchema.CONF_SCENE_NUMBER],
+ )
+
+
+def _create_binary_sensor(
+ hass: HomeAssistant, knx_module: XKNX, config: ConfigType
+) -> XknxBinarySensor:
+ """Return a KNX binary sensor to be used within XKNX."""
+ device_name = config[CONF_NAME]
+ actions = []
+ automations = config.get(BinarySensorSchema.CONF_AUTOMATION)
+ if automations is not None:
+ for automation in automations:
+ counter = automation[BinarySensorSchema.CONF_COUNTER]
+ hook = automation[BinarySensorSchema.CONF_HOOK]
+ action = automation[BinarySensorSchema.CONF_ACTION]
+ script_name = f"{device_name} turn ON script"
+ script = Script(hass, action, script_name, DOMAIN)
+ action = XknxActionCallback(
+ knx_module, script.async_run, hook=hook, counter=counter
+ )
+ actions.append(action)
+
+ return XknxBinarySensor(
+ knx_module,
+ name=device_name,
+ group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS],
+ sync_state=config[BinarySensorSchema.CONF_SYNC_STATE],
+ device_class=config.get(CONF_DEVICE_CLASS),
+ ignore_internal_state=config[BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE],
+ reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER),
+ actions=actions,
+ )
diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py
index 7ea5dc52155..ac0bf2122a8 100644
--- a/homeassistant/components/knx/light.py
+++ b/homeassistant/components/knx/light.py
@@ -1,7 +1,4 @@
"""Support for KNX/IP lights."""
-from enum import Enum
-
-import voluptuous as vol
from xknx.devices import Light as XknxLight
from homeassistant.components.light import (
@@ -9,81 +6,26 @@ from homeassistant.components.light import (
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_WHITE_VALUE,
- PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
SUPPORT_WHITE_VALUE,
LightEntity,
)
-from homeassistant.const import CONF_ADDRESS, CONF_NAME
from homeassistant.core import callback
-import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
from . import ATTR_DISCOVER_DEVICES, DATA_KNX
-CONF_STATE_ADDRESS = "state_address"
-CONF_BRIGHTNESS_ADDRESS = "brightness_address"
-CONF_BRIGHTNESS_STATE_ADDRESS = "brightness_state_address"
-CONF_COLOR_ADDRESS = "color_address"
-CONF_COLOR_STATE_ADDRESS = "color_state_address"
-CONF_COLOR_TEMP_ADDRESS = "color_temperature_address"
-CONF_COLOR_TEMP_STATE_ADDRESS = "color_temperature_state_address"
-CONF_COLOR_TEMP_MODE = "color_temperature_mode"
-CONF_RGBW_ADDRESS = "rgbw_address"
-CONF_RGBW_STATE_ADDRESS = "rgbw_state_address"
-CONF_MIN_KELVIN = "min_kelvin"
-CONF_MAX_KELVIN = "max_kelvin"
-
-DEFAULT_NAME = "KNX Light"
DEFAULT_COLOR = (0.0, 0.0)
DEFAULT_BRIGHTNESS = 255
-DEFAULT_COLOR_TEMP_MODE = "absolute"
DEFAULT_WHITE_VALUE = 255
-DEFAULT_MIN_KELVIN = 2700 # 370 mireds
-DEFAULT_MAX_KELVIN = 6000 # 166 mireds
-
-
-class ColorTempModes(Enum):
- """Color temperature modes for config validation."""
-
- absolute = "DPT-7.600"
- relative = "DPT-5.001"
-
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_ADDRESS): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string,
- vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_COLOR_ADDRESS): cv.string,
- vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string,
- vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE): cv.enum(
- ColorTempModes
- ),
- vol.Optional(CONF_RGBW_ADDRESS): cv.string,
- vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All(
- vol.Coerce(int), vol.Range(min=1)
- ),
- vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
- vol.Coerce(int), vol.Range(min=1)
- ),
- }
-)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up lights for KNX platform."""
if discovery_info is not None:
async_add_entities_discovery(hass, discovery_info, async_add_entities)
- else:
- async_add_entities_config(hass, config, async_add_entities)
@callback
@@ -96,46 +38,10 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
async_add_entities(entities)
-@callback
-def async_add_entities_config(hass, config, async_add_entities):
- """Set up light for KNX platform configured within platform."""
- group_address_tunable_white = None
- group_address_tunable_white_state = None
- group_address_color_temp = None
- group_address_color_temp_state = None
- if config[CONF_COLOR_TEMP_MODE] == ColorTempModes.absolute:
- group_address_color_temp = config.get(CONF_COLOR_TEMP_ADDRESS)
- group_address_color_temp_state = config.get(CONF_COLOR_TEMP_STATE_ADDRESS)
- elif config[CONF_COLOR_TEMP_MODE] == ColorTempModes.relative:
- group_address_tunable_white = config.get(CONF_COLOR_TEMP_ADDRESS)
- group_address_tunable_white_state = config.get(CONF_COLOR_TEMP_STATE_ADDRESS)
-
- light = XknxLight(
- hass.data[DATA_KNX].xknx,
- name=config[CONF_NAME],
- group_address_switch=config[CONF_ADDRESS],
- group_address_switch_state=config.get(CONF_STATE_ADDRESS),
- group_address_brightness=config.get(CONF_BRIGHTNESS_ADDRESS),
- group_address_brightness_state=config.get(CONF_BRIGHTNESS_STATE_ADDRESS),
- group_address_color=config.get(CONF_COLOR_ADDRESS),
- group_address_color_state=config.get(CONF_COLOR_STATE_ADDRESS),
- group_address_rgbw=config.get(CONF_RGBW_ADDRESS),
- group_address_rgbw_state=config.get(CONF_RGBW_STATE_ADDRESS),
- group_address_tunable_white=group_address_tunable_white,
- group_address_tunable_white_state=group_address_tunable_white_state,
- group_address_color_temperature=group_address_color_temp,
- group_address_color_temperature_state=group_address_color_temp_state,
- min_kelvin=config[CONF_MIN_KELVIN],
- max_kelvin=config[CONF_MAX_KELVIN],
- )
- hass.data[DATA_KNX].xknx.devices.add(light)
- async_add_entities([KNXLight(light)])
-
-
class KNXLight(LightEntity):
"""Representation of a KNX light."""
- def __init__(self, device):
+ def __init__(self, device: XknxLight):
"""Initialize of KNX light."""
self.device = device
diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json
index 941a62d2d14..108f3a2062a 100644
--- a/homeassistant/components/knx/manifest.json
+++ b/homeassistant/components/knx/manifest.json
@@ -2,6 +2,6 @@
"domain": "knx",
"name": "KNX",
"documentation": "https://www.home-assistant.io/integrations/knx",
- "requirements": ["xknx==0.11.3"],
- "codeowners": ["@Julius2342"]
+ "requirements": ["xknx==0.12.0"],
+ "codeowners": ["@Julius2342", "@farmio", "@marvin-w"]
}
diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py
index 64d513b8624..fcb5bd352d5 100644
--- a/homeassistant/components/knx/notify.py
+++ b/homeassistant/components/knx/notify.py
@@ -1,31 +1,16 @@
"""Support for KNX/IP notification services."""
-import voluptuous as vol
from xknx.devices import Notification as XknxNotification
-from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService
-from homeassistant.const import CONF_ADDRESS, CONF_NAME
+from homeassistant.components.notify import BaseNotificationService
from homeassistant.core import callback
-import homeassistant.helpers.config_validation as cv
from . import ATTR_DISCOVER_DEVICES, DATA_KNX
-DEFAULT_NAME = "KNX Notify"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_ADDRESS): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- }
-)
-
async def async_get_service(hass, config, discovery_info=None):
"""Get the KNX notification service."""
- return (
+ if discovery_info is not None:
async_get_service_discovery(hass, discovery_info)
- if discovery_info is not None
- else async_get_service_config(hass, config)
- )
@callback
@@ -40,22 +25,10 @@ def async_get_service_discovery(hass, discovery_info):
)
-@callback
-def async_get_service_config(hass, config):
- """Set up notification for KNX platform configured within platform."""
- notification = XknxNotification(
- hass.data[DATA_KNX].xknx,
- name=config[CONF_NAME],
- group_address=config[CONF_ADDRESS],
- )
- hass.data[DATA_KNX].xknx.devices.add(notification)
- return KNXNotificationService([notification])
-
-
class KNXNotificationService(BaseNotificationService):
"""Implement demo notification service."""
- def __init__(self, devices):
+ def __init__(self, devices: XknxNotification):
"""Initialize the service."""
self.devices = devices
diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py
index 8f2c24c05b6..dfa667dcd4f 100644
--- a/homeassistant/components/knx/scene.py
+++ b/homeassistant/components/knx/scene.py
@@ -1,35 +1,18 @@
"""Support for KNX scenes."""
from typing import Any
-import voluptuous as vol
from xknx.devices import Scene as XknxScene
-from homeassistant.components.scene import CONF_PLATFORM, Scene
-from homeassistant.const import CONF_ADDRESS, CONF_NAME
+from homeassistant.components.scene import Scene
from homeassistant.core import callback
-import homeassistant.helpers.config_validation as cv
from . import ATTR_DISCOVER_DEVICES, DATA_KNX
-CONF_SCENE_NUMBER = "scene_number"
-
-DEFAULT_NAME = "KNX SCENE"
-PLATFORM_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_PLATFORM): "knx",
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Required(CONF_ADDRESS): cv.string,
- vol.Required(CONF_SCENE_NUMBER): cv.positive_int,
- }
-)
-
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the scenes for KNX platform."""
if discovery_info is not None:
async_add_entities_discovery(hass, discovery_info, async_add_entities)
- else:
- async_add_entities_config(hass, config, async_add_entities)
@callback
@@ -42,23 +25,10 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
async_add_entities(entities)
-@callback
-def async_add_entities_config(hass, config, async_add_entities):
- """Set up scene for KNX platform configured within platform."""
- scene = XknxScene(
- hass.data[DATA_KNX].xknx,
- name=config[CONF_NAME],
- group_address=config[CONF_ADDRESS],
- scene_number=config[CONF_SCENE_NUMBER],
- )
- hass.data[DATA_KNX].xknx.devices.add(scene)
- async_add_entities([KNXScene(scene)])
-
-
class KNXScene(Scene):
"""Representation of a KNX scene."""
- def __init__(self, scene):
+ def __init__(self, scene: XknxScene):
"""Init KNX scene."""
self.scene = scene
diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py
new file mode 100644
index 00000000000..3b9436a4eee
--- /dev/null
+++ b/homeassistant/components/knx/schema.py
@@ -0,0 +1,342 @@
+"""Voluptuous schemas for the KNX integration."""
+import voluptuous as vol
+from xknx.devices.climate import SetpointShiftMode
+
+from homeassistant.const import (
+ CONF_ADDRESS,
+ CONF_DEVICE_CLASS,
+ CONF_ENTITY_ID,
+ CONF_HOST,
+ CONF_NAME,
+ CONF_PORT,
+ CONF_TYPE,
+)
+import homeassistant.helpers.config_validation as cv
+
+from .const import (
+ CONF_STATE_ADDRESS,
+ CONF_SYNC_STATE,
+ OPERATION_MODES,
+ PRESET_MODES,
+ ColorTempModes,
+)
+
+
+class ConnectionSchema:
+ """Voluptuous schema for KNX connection."""
+
+ CONF_KNX_LOCAL_IP = "local_ip"
+
+ TUNNELING_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_KNX_LOCAL_IP): cv.string,
+ vol.Optional(CONF_PORT): cv.port,
+ }
+ )
+
+ ROUTING_SCHEMA = vol.Schema({vol.Optional(CONF_KNX_LOCAL_IP): cv.string})
+
+
+class CoverSchema:
+ """Voluptuous schema for KNX covers."""
+
+ CONF_MOVE_LONG_ADDRESS = "move_long_address"
+ CONF_MOVE_SHORT_ADDRESS = "move_short_address"
+ CONF_STOP_ADDRESS = "stop_address"
+ CONF_POSITION_ADDRESS = "position_address"
+ CONF_POSITION_STATE_ADDRESS = "position_state_address"
+ CONF_ANGLE_ADDRESS = "angle_address"
+ CONF_ANGLE_STATE_ADDRESS = "angle_state_address"
+ CONF_TRAVELLING_TIME_DOWN = "travelling_time_down"
+ CONF_TRAVELLING_TIME_UP = "travelling_time_up"
+ CONF_INVERT_POSITION = "invert_position"
+ CONF_INVERT_ANGLE = "invert_angle"
+
+ DEFAULT_TRAVEL_TIME = 25
+ DEFAULT_NAME = "KNX Cover"
+
+ SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_MOVE_LONG_ADDRESS): cv.string,
+ vol.Optional(CONF_MOVE_SHORT_ADDRESS): cv.string,
+ vol.Optional(CONF_STOP_ADDRESS): cv.string,
+ vol.Optional(CONF_POSITION_ADDRESS): cv.string,
+ vol.Optional(CONF_POSITION_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_ANGLE_ADDRESS): cv.string,
+ vol.Optional(CONF_ANGLE_STATE_ADDRESS): cv.string,
+ vol.Optional(
+ CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME
+ ): cv.positive_int,
+ vol.Optional(
+ CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME
+ ): cv.positive_int,
+ vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
+ vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
+ }
+ )
+
+
+class BinarySensorSchema:
+ """Voluptuous schema for KNX binary sensors."""
+
+ CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
+ CONF_SYNC_STATE = CONF_SYNC_STATE
+ CONF_IGNORE_INTERNAL_STATE = "ignore_internal_state"
+ CONF_AUTOMATION = "automation"
+ CONF_HOOK = "hook"
+ CONF_DEFAULT_HOOK = "on"
+ CONF_COUNTER = "counter"
+ CONF_DEFAULT_COUNTER = 1
+ CONF_ACTION = "action"
+ CONF_RESET_AFTER = "reset_after"
+
+ DEFAULT_NAME = "KNX Binary Sensor"
+ AUTOMATION_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_HOOK, default=CONF_DEFAULT_HOOK): cv.string,
+ vol.Optional(CONF_COUNTER, default=CONF_DEFAULT_COUNTER): cv.port,
+ vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
+ }
+ )
+
+ AUTOMATIONS_SCHEMA = vol.All(cv.ensure_list, [AUTOMATION_SCHEMA])
+
+ SCHEMA = vol.All(
+ cv.deprecated("significant_bit"),
+ vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_SYNC_STATE, default=True): vol.Any(
+ vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)),
+ cv.boolean,
+ cv.string,
+ ),
+ vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=False): cv.boolean,
+ vol.Required(CONF_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_DEVICE_CLASS): cv.string,
+ vol.Optional(CONF_RESET_AFTER): cv.positive_int,
+ vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA,
+ }
+ ),
+ )
+
+
+class LightSchema:
+ """Voluptuous schema for KNX lights."""
+
+ CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
+ CONF_BRIGHTNESS_ADDRESS = "brightness_address"
+ CONF_BRIGHTNESS_STATE_ADDRESS = "brightness_state_address"
+ CONF_COLOR_ADDRESS = "color_address"
+ CONF_COLOR_STATE_ADDRESS = "color_state_address"
+ CONF_COLOR_TEMP_ADDRESS = "color_temperature_address"
+ CONF_COLOR_TEMP_STATE_ADDRESS = "color_temperature_state_address"
+ CONF_COLOR_TEMP_MODE = "color_temperature_mode"
+ CONF_RGBW_ADDRESS = "rgbw_address"
+ CONF_RGBW_STATE_ADDRESS = "rgbw_state_address"
+ CONF_MIN_KELVIN = "min_kelvin"
+ CONF_MAX_KELVIN = "max_kelvin"
+
+ DEFAULT_NAME = "KNX Light"
+ DEFAULT_COLOR_TEMP_MODE = "absolute"
+ DEFAULT_MIN_KELVIN = 2700 # 370 mireds
+ DEFAULT_MAX_KELVIN = 6000 # 166 mireds
+
+ SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_ADDRESS): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string,
+ vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_COLOR_ADDRESS): cv.string,
+ vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string,
+ vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string,
+ vol.Optional(
+ CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE
+ ): cv.enum(ColorTempModes),
+ vol.Optional(CONF_RGBW_ADDRESS): cv.string,
+ vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All(
+ vol.Coerce(int), vol.Range(min=1)
+ ),
+ vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
+ vol.Coerce(int), vol.Range(min=1)
+ ),
+ }
+ )
+
+
+class ClimateSchema:
+ """Voluptuous schema for KNX climate devices."""
+
+ CONF_SETPOINT_SHIFT_ADDRESS = "setpoint_shift_address"
+ CONF_SETPOINT_SHIFT_STATE_ADDRESS = "setpoint_shift_state_address"
+ CONF_SETPOINT_SHIFT_MODE = "setpoint_shift_mode"
+ CONF_SETPOINT_SHIFT_MAX = "setpoint_shift_max"
+ CONF_SETPOINT_SHIFT_MIN = "setpoint_shift_min"
+ CONF_TEMPERATURE_ADDRESS = "temperature_address"
+ CONF_TEMPERATURE_STEP = "temperature_step"
+ CONF_TARGET_TEMPERATURE_ADDRESS = "target_temperature_address"
+ CONF_TARGET_TEMPERATURE_STATE_ADDRESS = "target_temperature_state_address"
+ CONF_OPERATION_MODE_ADDRESS = "operation_mode_address"
+ CONF_OPERATION_MODE_STATE_ADDRESS = "operation_mode_state_address"
+ CONF_CONTROLLER_STATUS_ADDRESS = "controller_status_address"
+ CONF_CONTROLLER_STATUS_STATE_ADDRESS = "controller_status_state_address"
+ CONF_CONTROLLER_MODE_ADDRESS = "controller_mode_address"
+ CONF_CONTROLLER_MODE_STATE_ADDRESS = "controller_mode_state_address"
+ CONF_HEAT_COOL_ADDRESS = "heat_cool_address"
+ CONF_HEAT_COOL_STATE_ADDRESS = "heat_cool_state_address"
+ CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = (
+ "operation_mode_frost_protection_address"
+ )
+ CONF_OPERATION_MODE_NIGHT_ADDRESS = "operation_mode_night_address"
+ CONF_OPERATION_MODE_COMFORT_ADDRESS = "operation_mode_comfort_address"
+ CONF_OPERATION_MODE_STANDBY_ADDRESS = "operation_mode_standby_address"
+ CONF_OPERATION_MODES = "operation_modes"
+ CONF_ON_OFF_ADDRESS = "on_off_address"
+ CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address"
+ CONF_ON_OFF_INVERT = "on_off_invert"
+ CONF_MIN_TEMP = "min_temp"
+ CONF_MAX_TEMP = "max_temp"
+
+ DEFAULT_NAME = "KNX Climate"
+ DEFAULT_SETPOINT_SHIFT_MODE = "DPT6010"
+ DEFAULT_SETPOINT_SHIFT_MAX = 6
+ DEFAULT_SETPOINT_SHIFT_MIN = -6
+ DEFAULT_TEMPERATURE_STEP = 0.1
+ DEFAULT_ON_OFF_INVERT = False
+
+ SCHEMA = vol.All(
+ cv.deprecated("setpoint_shift_step", replacement_key=CONF_TEMPERATURE_STEP),
+ vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(
+ CONF_SETPOINT_SHIFT_MODE, default=DEFAULT_SETPOINT_SHIFT_MODE
+ ): cv.enum(SetpointShiftMode),
+ vol.Optional(
+ CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX
+ ): vol.All(int, vol.Range(min=0, max=32)),
+ vol.Optional(
+ CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN
+ ): vol.All(int, vol.Range(min=-32, max=0)),
+ vol.Optional(
+ CONF_TEMPERATURE_STEP, default=DEFAULT_TEMPERATURE_STEP
+ ): vol.All(float, vol.Range(min=0, max=2)),
+ vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string,
+ vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string,
+ vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): cv.string,
+ vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string,
+ vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string,
+ vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): cv.string,
+ vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_HEAT_COOL_ADDRESS): cv.string,
+ vol.Optional(CONF_HEAT_COOL_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string,
+ vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string,
+ vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string,
+ vol.Optional(CONF_OPERATION_MODE_STANDBY_ADDRESS): cv.string,
+ vol.Optional(CONF_ON_OFF_ADDRESS): cv.string,
+ vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string,
+ vol.Optional(
+ CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT
+ ): cv.boolean,
+ vol.Optional(CONF_OPERATION_MODES): vol.All(
+ cv.ensure_list, [vol.In({**OPERATION_MODES, **PRESET_MODES})]
+ ),
+ vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
+ vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
+ }
+ ),
+ )
+
+
+class SwitchSchema:
+ """Voluptuous schema for KNX switches."""
+
+ CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
+
+ DEFAULT_NAME = "KNX Switch"
+ SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_ADDRESS): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_STATE_ADDRESS): cv.string,
+ }
+ )
+
+
+class ExposeSchema:
+ """Voluptuous schema for KNX exposures."""
+
+ CONF_KNX_EXPOSE_TYPE = CONF_TYPE
+ CONF_KNX_EXPOSE_ATTRIBUTE = "attribute"
+ CONF_KNX_EXPOSE_DEFAULT = "default"
+ CONF_KNX_EXPOSE_ADDRESS = CONF_ADDRESS
+
+ SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_KNX_EXPOSE_TYPE): vol.Any(int, float, str),
+ vol.Optional(CONF_ENTITY_ID): cv.entity_id,
+ vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string,
+ vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all,
+ vol.Required(CONF_KNX_EXPOSE_ADDRESS): cv.string,
+ }
+ )
+
+
+class NotifySchema:
+ """Voluptuous schema for KNX notifications."""
+
+ DEFAULT_NAME = "KNX Notify"
+
+ SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_ADDRESS): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ }
+ )
+
+
+class SensorSchema:
+ """Voluptuous schema for KNX sensors."""
+
+ CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
+ CONF_SYNC_STATE = CONF_SYNC_STATE
+ DEFAULT_NAME = "KNX Sensor"
+
+ SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_SYNC_STATE, default=True): vol.Any(
+ vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)),
+ cv.boolean,
+ cv.string,
+ ),
+ vol.Required(CONF_STATE_ADDRESS): cv.string,
+ vol.Required(CONF_TYPE): vol.Any(int, float, str),
+ }
+ )
+
+
+class SceneSchema:
+ """Voluptuous schema for KNX scenes."""
+
+ CONF_SCENE_NUMBER = "scene_number"
+
+ DEFAULT_NAME = "KNX SCENE"
+ SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Required(CONF_ADDRESS): cv.string,
+ vol.Required(CONF_SCENE_NUMBER): cv.positive_int,
+ }
+ )
diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py
index 2d278ec04b4..1fd8950a3fb 100644
--- a/homeassistant/components/knx/sensor.py
+++ b/homeassistant/components/knx/sensor.py
@@ -1,35 +1,16 @@
"""Support for KNX/IP sensors."""
-import voluptuous as vol
from xknx.devices import Sensor as XknxSensor
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, CONF_TYPE
from homeassistant.core import callback
-import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from . import ATTR_DISCOVER_DEVICES, DATA_KNX
-CONF_STATE_ADDRESS = "state_address"
-CONF_SYNC_STATE = "sync_state"
-DEFAULT_NAME = "KNX Sensor"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_SYNC_STATE, default=True): cv.boolean,
- vol.Required(CONF_STATE_ADDRESS): cv.string,
- vol.Required(CONF_TYPE): cv.string,
- }
-)
-
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up sensor(s) for KNX platform."""
if discovery_info is not None:
async_add_entities_discovery(hass, discovery_info, async_add_entities)
- else:
- async_add_entities_config(hass, config, async_add_entities)
@callback
@@ -42,24 +23,10 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
async_add_entities(entities)
-@callback
-def async_add_entities_config(hass, config, async_add_entities):
- """Set up sensor for KNX platform configured within platform."""
- sensor = XknxSensor(
- hass.data[DATA_KNX].xknx,
- name=config[CONF_NAME],
- group_address_state=config[CONF_STATE_ADDRESS],
- sync_state=config[CONF_SYNC_STATE],
- value_type=config[CONF_TYPE],
- )
- hass.data[DATA_KNX].xknx.devices.add(sensor)
- async_add_entities([KNXSensor(sensor)])
-
-
class KNXSensor(Entity):
"""Representation of a KNX sensor."""
- def __init__(self, device):
+ def __init__(self, device: XknxSensor):
"""Initialize of a KNX sensor."""
self.device = device
diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml
index 5faaf0678d1..03d4e69b32c 100644
--- a/homeassistant/components/knx/services.yaml
+++ b/homeassistant/components/knx/services.yaml
@@ -7,3 +7,6 @@ send:
payload:
description: "Payload to send to the bus. Integers are treated as DPT 1/2/3 payloads. For DPTs > 6 bits send a list. Each value represents 1 octet (0-255). Pad with 0 to DPT byte length."
example: "[0, 4]"
+ type:
+ description: "Optional. If set, the payload will not be sent as raw bytes, but encoded as given DPT. Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)."
+ example: "temperature"
diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py
index 00b98f0224b..a6e7e583b88 100644
--- a/homeassistant/components/knx/switch.py
+++ b/homeassistant/components/knx/switch.py
@@ -1,32 +1,16 @@
"""Support for KNX/IP switches."""
-import voluptuous as vol
from xknx.devices import Switch as XknxSwitch
-from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
-from homeassistant.const import CONF_ADDRESS, CONF_NAME
+from homeassistant.components.switch import SwitchEntity
from homeassistant.core import callback
-import homeassistant.helpers.config_validation as cv
from . import ATTR_DISCOVER_DEVICES, DATA_KNX
-CONF_STATE_ADDRESS = "state_address"
-
-DEFAULT_NAME = "KNX Switch"
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_ADDRESS): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_STATE_ADDRESS): cv.string,
- }
-)
-
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up switch(es) for KNX platform."""
if discovery_info is not None:
async_add_entities_discovery(hass, discovery_info, async_add_entities)
- else:
- async_add_entities_config(hass, config, async_add_entities)
@callback
@@ -39,23 +23,10 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
async_add_entities(entities)
-@callback
-def async_add_entities_config(hass, config, async_add_entities):
- """Set up switch for KNX platform configured within platform."""
- switch = XknxSwitch(
- hass.data[DATA_KNX].xknx,
- name=config[CONF_NAME],
- group_address=config[CONF_ADDRESS],
- group_address_state=config.get(CONF_STATE_ADDRESS),
- )
- hass.data[DATA_KNX].xknx.devices.add(switch)
- async_add_entities([KNXSwitch(switch)])
-
-
class KNXSwitch(SwitchEntity):
"""Representation of a KNX switch."""
- def __init__(self, device):
+ def __init__(self, device: XknxSwitch):
"""Initialize of KNX switch."""
self.device = device
diff --git a/requirements_all.txt b/requirements_all.txt
index c26e0322b17..39e6f82e6d4 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2252,7 +2252,7 @@ xboxapi==2.0.1
xfinity-gateway==0.0.4
# homeassistant.components.knx
-xknx==0.11.3
+xknx==0.12.0
# homeassistant.components.bluesound
# homeassistant.components.rest