diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index c302188ff20..bc1aa6c1301 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -4,15 +4,20 @@ 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.exceptions import XKNXException from xknx.io import DEFAULT_MCAST_PORT, ConnectionConfig, ConnectionType -from xknx.knx import AddressFilter, DPTArray, DPTBinary, GroupAddress, Telegram +from xknx.telegram import AddressFilter, GroupAddress, Telegram from homeassistant.const import ( CONF_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback from homeassistant.helpers import discovery @@ -35,6 +40,8 @@ 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" SERVICE_KNX_SEND = "send" @@ -57,6 +64,8 @@ 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, } ) @@ -244,6 +253,8 @@ class KNXModule: for to_expose in self.config[DOMAIN][CONF_KNX_EXPOSE]: expose_type = to_expose.get(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) if expose_type in ["time", "date", "datetime"]: exposure = KNXExposeTime(self.xknx, expose_type, address) @@ -251,7 +262,13 @@ class KNXModule: self.exposures.append(exposure) else: exposure = KNXExposeSensor( - self.hass, self.xknx, expose_type, entity_id, address + self.hass, + self.xknx, + expose_type, + entity_id, + attribute, + default, + address, ) exposure.async_register() self.exposures.append(exposure) @@ -325,23 +342,26 @@ class KNXExposeTime: class KNXExposeSensor: """Object to Expose Home Assistant entity to KNX bus.""" - def __init__(self, hass, xknx, expose_type, entity_id, address): + def __init__(self, hass, xknx, expose_type, entity_id, attribute, default, address): """Initialize of Expose class.""" self.hass = hass self.xknx = xknx self.type = expose_type self.entity_id = entity_id + self.expose_attribute = attribute + self.expose_default = default self.address = address self.device = None @callback def async_register(self): """Register listener.""" + if self.expose_attribute is not None: + _name = self.entity_id + "__" + self.expose_attribute + else: + _name = self.entity_id self.device = ExposeSensor( - self.xknx, - name=self.entity_id, - group_address=self.address, - value_type=self.type, + self.xknx, name=_name, group_address=self.address, value_type=self.type, ) self.xknx.devices.add(self.device) async_track_state_change(self.hass, self.entity_id, self._async_entity_changed) @@ -350,13 +370,31 @@ class KNXExposeSensor: """Handle entity change.""" if new_state is None: return - if new_state.state == "unknown": + if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): return - if self.type == "binary": - if new_state.state == "on": - await self.device.set(True) - elif new_state.state == "off": - await self.device.set(False) + if self.expose_attribute is not None: + new_attribute = new_state.attributes.get(self.expose_attribute) + if old_state is not None: + old_attribute = old_state.attributes.get(self.expose_attribute) + if old_attribute == new_attribute: + # don't send same value sequentially + return + await self._async_set_knx_value(new_attribute) else: - await self.device.set(new_state.state) + await self._async_set_knx_value(new_state.state) + + async def _async_set_knx_value(self, value): + """Set new value on xknx ExposeSensor.""" + if value is None: + if self.expose_default is None: + return + value = self.expose_default + + if self.type == "binary": + if value == STATE_ON: + value = True + elif value == STATE_OFF: + value = False + + await self.device.set(value) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 9f8c3cc8491..29effaa7ebf 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -124,6 +124,10 @@ class KNXBinarySensor(BinarySensorEntity): """Store register state change callback.""" self.async_register_callbacks() + async def async_update(self): + """Request a state update from KNX bus.""" + await self.device.sync() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index a3d5ac046a4..a58e5312c11 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -3,7 +3,7 @@ from typing import List, Optional import voluptuous as vol from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode -from xknx.knx import HVACOperationMode +from xknx.dpt import HVACOperationMode from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( @@ -215,6 +215,11 @@ class KNXClimate(ClimateEntity): self.device.register_device_updated_cb(after_update_callback) self.device.mode.register_device_updated_cb(after_update_callback) + async def async_update(self): + """Request a state update from KNX bus.""" + await self.device.sync() + await self.device.mode.sync() + @property def name(self) -> str: """Return the name of the KNX device.""" @@ -279,7 +284,8 @@ class KNXClimate(ClimateEntity): return OPERATION_MODES.get( self.device.mode.operation_mode.value, HVAC_MODE_HEAT ) - return None + # default to "heat" + return HVAC_MODE_HEAT @property def hvac_modes(self) -> Optional[List[str]]: @@ -293,7 +299,9 @@ class KNXClimate(ClimateEntity): _operations.append(HVAC_MODE_HEAT) _operations.append(HVAC_MODE_OFF) - return [op for op in _operations if op is not None] + _modes = list(filter(None, _operations)) + # default to ["heat"] + return _modes if _modes else [HVAC_MODE_HEAT] async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set operation mode.""" diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 92e7e599bcb..731105f6629 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -116,6 +116,10 @@ class KNXCover(CoverEntity): """Store register state change callback.""" self.async_register_callbacks() + async def async_update(self): + """Request a state update from KNX bus.""" + await self.device.sync() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index bb82e86cd25..7ea5dc52155 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -162,6 +162,10 @@ class KNXLight(LightEntity): """Store register state change callback.""" self.async_register_callbacks() + async def async_update(self): + """Request a state update from KNX bus.""" + await self.device.sync() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index ab26c4b6287..941a62d2d14 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.2"], + "requirements": ["xknx==0.11.3"], "codeowners": ["@Julius2342"] } diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 2679170b03d..2d278ec04b4 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -77,6 +77,10 @@ class KNXSensor(Entity): """Store register state change callback.""" self.async_register_callbacks() + async def async_update(self): + """Update the state from KNX.""" + await self.device.sync() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 3f06221cea5..00b98f0224b 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -73,6 +73,10 @@ class KNXSwitch(SwitchEntity): """Store register state change callback.""" self.async_register_callbacks() + async def async_update(self): + """Request a state update from KNX bus.""" + await self.device.sync() + @property def name(self): """Return the name of the KNX device.""" diff --git a/requirements_all.txt b/requirements_all.txt index 741d0150d94..b1326f2ca8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2207,7 +2207,7 @@ xboxapi==0.1.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.11.2 +xknx==0.11.3 # homeassistant.components.bluesound # homeassistant.components.rest