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