From 7d5c1581f15f1c0abe4e7b5e600fc9808c0e2305 Mon Sep 17 00:00:00 2001 From: Julius Mittenzwei Date: Mon, 26 Feb 2018 08:44:09 +0100 Subject: [PATCH] KNX Component: Scene support and expose sensor values (#11978) * XKNX improvements: Added Scene support, added support for exposing sensors to KNX bus, added reset value option for binary switches * fixed import * Bumped version of KNX library (minor upgrade with two important bugfixes) * bumped version of xknx (now without python requirement *sigh*) * Issue #11978: fixed review comments * Issue #11978: hound suggestion fixed: * review comments * made async functions async * Addressed issues mentined by @MartinHjelmare * removed default=None from validation schema * ATTR_ENTITY_ID->CONF_ENTITY_ID * moved missing function to async syntax * pylint * Trigger notification * Trigger notification * fixed review comment --- homeassistant/components/binary_sensor/knx.py | 5 +- homeassistant/components/knx.py | 97 ++++++++++++++++++- homeassistant/components/scene/knx.py | 79 +++++++++++++++ 3 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/scene/knx.py diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py index 1802ae34454..834186b8b18 100644 --- a/homeassistant/components/binary_sensor/knx.py +++ b/homeassistant/components/binary_sensor/knx.py @@ -25,6 +25,7 @@ CONF_DEFAULT_HOOK = 'on' CONF_COUNTER = 'counter' CONF_DEFAULT_COUNTER = 1 CONF_ACTION = 'action' +CONF_RESET_AFTER = 'reset_after' CONF__ACTION = 'turn_off_action' @@ -48,6 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICE_CLASS): cv.string, vol.Optional(CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT): cv.positive_int, + vol.Optional(CONF_RESET_AFTER): cv.positive_int, vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA, }) @@ -81,7 +83,8 @@ def async_add_devices_config(hass, config, async_add_devices): name=name, group_address=config.get(CONF_ADDRESS), device_class=config.get(CONF_DEVICE_CLASS), - significant_bit=config.get(CONF_SIGNIFICANT_BIT)) + significant_bit=config.get(CONF_SIGNIFICANT_BIT), + reset_after=config.get(CONF_RESET_AFTER)) hass.data[DATA_KNX].xknx.devices.add(binary_sensor) entity = KNXBinarySensor(hass, binary_sensor) diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py index 63407fd6246..d7630fee7a5 100644 --- a/homeassistant/components/knx.py +++ b/homeassistant/components/knx.py @@ -9,9 +9,12 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +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 from homeassistant.helpers.script import Script REQUIREMENTS = ['xknx==0.8.3'] @@ -26,6 +29,9 @@ 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_EXPOSE = "expose" +CONF_KNX_EXPOSE_TYPE = "type" +CONF_KNX_EXPOSE_ADDRESS = "address" SERVICE_KNX_SEND = "send" SERVICE_KNX_ATTR_ADDRESS = "address" @@ -45,6 +51,12 @@ ROUTING_SCHEMA = vol.Schema({ vol.Required(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.Required(CONF_KNX_EXPOSE_ADDRESS): cv.string, +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_KNX_CONFIG): cv.string, @@ -56,6 +68,10 @@ CONFIG_SCHEMA = vol.Schema({ vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, 'fire_ev'): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean, + vol.Optional(CONF_KNX_EXPOSE): + vol.All( + cv.ensure_list, + [EXPOSE_SCHEMA]), }) }, extra=vol.ALLOW_EXTRA) @@ -71,6 +87,7 @@ async def async_setup(hass, config): from xknx.exceptions import XKNXException try: hass.data[DATA_KNX] = KNXModule(hass, config) + hass.data[DATA_KNX].async_create_exposures() await hass.data[DATA_KNX].start() except XKNXException as ex: @@ -87,6 +104,7 @@ async def async_setup(hass, config): ('light', 'Light'), ('sensor', 'Sensor'), ('binary_sensor', 'BinarySensor'), + ('scene', 'Scene'), ('notify', 'Notification')): found_devices = _get_devices(hass, discovery_type) hass.async_add_job( @@ -121,6 +139,7 @@ class KNXModule(object): self.connected = False self.init_xknx() self.register_callbacks() + self.exposures = [] def init_xknx(self): """Initialize of KNX object.""" @@ -199,6 +218,26 @@ class KNXModule(object): self.xknx.telegram_queue.register_telegram_received_cb( self.telegram_received_cb, address_filters) + @callback + def async_create_exposures(self): + """Create exposures.""" + 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) + entity_id = to_expose.get(CONF_ENTITY_ID) + address = to_expose.get(CONF_KNX_EXPOSE_ADDRESS) + if expose_type in ['time', 'date', 'datetime']: + exposure = KNXExposeTime( + self.xknx, expose_type, address) + exposure.async_register() + self.exposures.append(exposure) + else: + exposure = KNXExposeSensor( + self.hass, self.xknx, expose_type, entity_id, address) + exposure.async_register() + self.exposures.append(exposure) + async def telegram_received_cb(self, telegram): """Call invoked after a KNX telegram was received.""" self.hass.bus.fire('knx_event', { @@ -243,3 +282,59 @@ class KNXAutomation(): hass.data[DATA_KNX].xknx, self.script.async_run, hook=hook, counter=counter) device.actions.append(self.action) + + +class KNXExposeTime(object): + """Object to Expose Time/Date object to KNX bus.""" + + def __init__(self, xknx, expose_type, address): + """Initialize of Expose class.""" + self.xknx = xknx + self.type = expose_type + self.address = address + self.device = None + + @callback + def async_register(self): + """Register listener.""" + from xknx.devices import DateTime, DateTimeBroadcastType + broadcast_type_string = self.type.upper() + broadcast_type = DateTimeBroadcastType[broadcast_type_string] + self.device = DateTime( + self.xknx, + 'Time', + broadcast_type=broadcast_type, + group_address=self.address) + self.xknx.devices.add(self.device) + + +class KNXExposeSensor(object): + """Object to Expose HASS entity to KNX bus.""" + + def __init__(self, hass, xknx, expose_type, entity_id, address): + """Initialize of Expose class.""" + self.hass = hass + self.xknx = xknx + self.type = expose_type + self.entity_id = entity_id + self.address = address + self.device = None + + @callback + def async_register(self): + """Register listener.""" + from xknx.devices import ExposeSensor + self.device = ExposeSensor( + self.xknx, + name=self.entity_id, + 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) + + async def _async_entity_changed(self, entity_id, old_state, new_state): + """Callback after entity changed.""" + if new_state is None: + return + await self.device.set(float(new_state.state)) diff --git a/homeassistant/components/scene/knx.py b/homeassistant/components/scene/knx.py new file mode 100644 index 00000000000..1329b440e5f --- /dev/null +++ b/homeassistant/components/scene/knx.py @@ -0,0 +1,79 @@ +""" +Support for KNX scenes. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/scene.knx/ +""" +import asyncio + +import voluptuous as vol + +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX +from homeassistant.components.scene import CONF_PLATFORM, Scene +from homeassistant.const import CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +CONF_ADDRESS = 'address' +CONF_SCENE_NUMBER = 'scene_number' + +DEFAULT_NAME = 'KNX SCENE' +DEPENDENCIES = ['knx'] + +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, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the scenes for KNX platform.""" + if discovery_info is not None: + async_add_devices_discovery(hass, discovery_info, async_add_devices) + else: + async_add_devices_config(hass, config, async_add_devices) + + +@callback +def async_add_devices_discovery(hass, discovery_info, async_add_devices): + """Set up scenes for KNX platform configured via xknx.yaml.""" + entities = [] + for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: + device = hass.data[DATA_KNX].xknx.devices[device_name] + entities.append(KNXScene(device)) + async_add_devices(entities) + + +@callback +def async_add_devices_config(hass, config, async_add_devices): + """Set up scene for KNX platform configured within platform.""" + import xknx + scene = xknx.devices.Scene( + hass.data[DATA_KNX].xknx, + name=config.get(CONF_NAME), + group_address=config.get(CONF_ADDRESS), + scene_number=config.get(CONF_SCENE_NUMBER)) + hass.data[DATA_KNX].xknx.devices.add(scene) + async_add_devices([KNXScene(scene)]) + + +class KNXScene(Scene): + """Representation of a KNX scene.""" + + def __init__(self, scene): + """Init KNX scene.""" + self.scene = scene + + @property + def name(self): + """Return the name of the scene.""" + return self.scene.name + + @asyncio.coroutine + def async_activate(self): + """Activate the scene.""" + yield from self.scene.run()