From 5c35c1db0a88b73f00915a371eaf2fe68f915200 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Wed, 4 Apr 2018 17:14:41 -0400 Subject: [PATCH 1/7] Add Konnected component with support for discovery, binary sensor, and switch Co-authored-by: Eitan Mosenkis --- .coveragerc | 3 + .../components/binary_sensor/konnected.py | 73 +++++ homeassistant/components/discovery.py | 3 + homeassistant/components/konnected.py | 293 ++++++++++++++++++ homeassistant/components/switch/konnected.py | 83 +++++ requirements_all.txt | 3 + 6 files changed, 458 insertions(+) create mode 100644 homeassistant/components/binary_sensor/konnected.py create mode 100644 homeassistant/components/konnected.py create mode 100644 homeassistant/components/switch/konnected.py diff --git a/.coveragerc b/.coveragerc index eae6498cd0a09..d70f6a0788ad9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -151,6 +151,9 @@ omit = homeassistant/components/knx.py homeassistant/components/*/knx.py + homeassistant/components/konnected.py + homeassistant/components/*/konnected.py + homeassistant/components/lametric.py homeassistant/components/*/lametric.py diff --git a/homeassistant/components/binary_sensor/konnected.py b/homeassistant/components/binary_sensor/konnected.py new file mode 100644 index 0000000000000..c3841ad52d16b --- /dev/null +++ b/homeassistant/components/binary_sensor/konnected.py @@ -0,0 +1,73 @@ +""" +Support for wired binary sensors attached to a Konnected device. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.konnected/ +""" +import asyncio +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.konnected import (DOMAIN, PIN_TO_ZONE) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['konnected'] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up binary sensors attached to a Konnected device.""" + if discovery_info is None: + return + + data = hass.data[DOMAIN] + device_id = discovery_info['device_id'] + sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data) + for pin_num, pin_data in + data['devices'][device_id]['sensors'].items()] + async_add_devices(sensors, True) + + +class KonnectedBinarySensor(BinarySensorDevice): + """Representation of a Konnected binary sensor.""" + + def __init__(self, device_id, pin_num, data): + """Initialize the binary sensor.""" + self._data = data + self._device_id = device_id + self._pin_num = pin_num + self._state = self._data.get('state') + self._device_class = self._data.get('type', 'motion') + self._name = self._data.get('name', 'Konnected {} Zone {}'.format( + device_id, PIN_TO_ZONE[pin_num])) + self._data['entity'] = self + _LOGGER.info('Created new sensor: %s', self._name) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._state + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @asyncio.coroutine + def async_set_state(self, state): + """Update the sensor's state.""" + self._state = state + self._data['state'] = state + self.async_schedule_update_ha_state() + _LOGGER.info('Updating state: %s is %s', self.name, state) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index f0ebcba836677..d8c1fe7d938ba 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -37,6 +37,7 @@ SERVICE_XIAOMI_GW = 'xiaomi_gw' SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_HUE = 'philips_hue' +SERVICE_KONNECTED = 'konnected' SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' @@ -60,6 +61,7 @@ SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_DAIKIN: ('daikin', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), + SERVICE_KONNECTED: ('konnected', None), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), @@ -189,6 +191,7 @@ def _discover(netdisco): for disc in netdisco.discover(): for service in netdisco.get_info(disc): results.append((disc, service)) + finally: netdisco.stop() diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py new file mode 100644 index 0000000000000..34c8052bbd988 --- /dev/null +++ b/homeassistant/components/konnected.py @@ -0,0 +1,293 @@ +""" +Support for Konnected devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/konnected/ +""" +import asyncio +import logging +import voluptuous as vol + +from aiohttp.hdrs import AUTHORIZATION +from aiohttp.web import Request, Response # NOQA + +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.components.discovery import SERVICE_KONNECTED +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED) +from homeassistant.helpers import discovery + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['konnected==0.1.2'] + +DOMAIN = 'konnected' + +PIN_TO_ZONE = {1: 1, 2: 2, 5: 3, 6: 4, 7: 5, 8: 'out', 9: 6} +ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()} + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: { + 'auth_token': str, + vol.Optional('home_assistant_url'): str, + 'devices': [{ + vol.Required('id', default=''): vol.Coerce(str), + 'sensors': [{ + vol.Exclusive('pin', 's_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive('zone', 's_pin'): vol.Any(*ZONE_TO_PIN), + vol.Required('type', default='motion'): + DEVICE_CLASSES_SCHEMA, + vol.Optional('name'): str, + }], + 'actuators': [{ + vol.Exclusive('pin', 'a_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive('zone', 'a_pin'): vol.Any(*ZONE_TO_PIN), + vol.Optional('name'): str, + vol.Required('activation', default='high'): + vol.All(vol.Lower, vol.Any('high', 'low')) + }], + }], + }, + }, + extra=vol.ALLOW_EXTRA, +) + +DEPENDENCIES = ['http'] + +ENDPOINT_ROOT = '/api/konnected' +UPDATE_ENDPOINT = ( + ENDPOINT_ROOT + + r'/device/{device_id:[a-zA-Z0-9]+}/{pin_num:[0-9]}/{state:[01]}') + + +@asyncio.coroutine +def async_setup(hass, config): + """Set up the Konnected platform.""" + cfg = config.get(DOMAIN) + if cfg is None: + cfg = {} + + auth_token = cfg.get('auth_token') or 'supersecret' + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {'auth_token': auth_token} + + @asyncio.coroutine + def async_device_discovered(service, info): + """Call when a Konnected device has been discovered.""" + _LOGGER.info("Discovered a new Konnected device: %s", info) + host = info.get('host') + port = info.get('port') + + device = KonnectedDevice(hass, host, port, cfg) + device.setup() + + discovery.async_listen( + hass, + SERVICE_KONNECTED, + async_device_discovered) + + hass.http.register_view(KonnectedView(auth_token)) + + return True + + +class KonnectedDevice(object): + """A representation of a single Konnected device.""" + + def __init__(self, hass, host, port, config): + """Initialize the Konnected device.""" + self.hass = hass + self.host = host + self.port = port + self.user_config = config + + import konnected + self.client = konnected.Client(host, str(port)) + self.status = self.client.get_status() + _LOGGER.info('Initialized Konnected device %s', self.device_id) + + def setup(self): + """Set up a newly discovered Konnected device.""" + user_config = self.config() + if user_config: + _LOGGER.info('Configuring Konnected device %s', self.device_id) + self.save_data() + self.sync_device() + self.hass.async_add_job( + discovery.async_load_platform( + self.hass, 'binary_sensor', + DOMAIN, {'device_id': self.device_id})) + self.hass.async_add_job( + discovery.async_load_platform( + self.hass, 'switch', DOMAIN, + {'device_id': self.device_id})) + + @property + def device_id(self): + """Device id is the MAC address as string with punctuation removed.""" + return self.status['mac'].replace(':', '') + + def config(self): + """Return an object representing the user defined configuration.""" + device_id = self.device_id + valid_keys = [device_id, device_id.upper(), + device_id[6:], device_id.upper()[6:]] + configured_devices = self.user_config['devices'] + return next((device for device in + configured_devices if device['id'] in valid_keys), None) + + def save_data(self): + """Save the device configuration to `hass.data`. + + TODO: This can probably be refactored and tidied up. + """ + sensors = {} + for entity in self.config().get('sensors') or []: + if 'zone' in entity: + pin = ZONE_TO_PIN[entity['zone']] + else: + pin = entity['pin'] + + sensor_status = next((sensor for sensor in + self.status.get('sensors') if + sensor.get('pin') == pin), {}) + if sensor_status.get('state'): + initial_state = bool(int(sensor_status.get('state'))) + else: + initial_state = None + + sensors[pin] = { + 'type': entity['type'], + 'name': entity.get('name', 'Konnected {} Zone {}'.format( + self.device_id[6:], PIN_TO_ZONE[pin])), + 'state': initial_state + } + _LOGGER.info('Set up sensor %s (initial state: %s)', + sensors[pin].get('name'), sensors[pin].get('state')) + + actuators = {} + for entity in self.config().get('actuators') or []: + if 'zone' in entity: + pin = ZONE_TO_PIN[entity['zone']] + else: + pin = entity['pin'] + + actuator_status = next((actuator for actuator in + self.status.get('actuators') if + actuator.get('pin') == pin), {}) + if actuator_status.get('state'): + initial_state = bool(int(actuator_status.get('state'))) + else: + initial_state = None + + actuators[pin] = { + 'name': entity.get('name', 'Konnected {} Actuator {}'.format( + self.device_id[6:], PIN_TO_ZONE[pin])), + 'state': initial_state, + 'activation': entity['activation'], + } + _LOGGER.info('Set up actuator %s (initial state: %s)', + actuators[pin].get('name'), + actuators[pin].get('state')) + + device_data = { + 'client': self.client, + 'sensors': sensors, + 'actuators': actuators, + 'host': self.host, + 'port': self.port, + } + + if 'devices' not in self.hass.data[DOMAIN]: + self.hass.data[DOMAIN]['devices'] = {} + + _LOGGER.info('Storing data in hass.data[konnected]: %s', device_data) + self.hass.data[DOMAIN]['devices'][self.device_id] = device_data + + @property + def stored_configuration(self): + """Return the configuration stored in `hass.data` for this device.""" + return self.hass.data[DOMAIN]['devices'][self.device_id] + + def sensor_configuration(self): + """Return the configuration map for syncing sensors.""" + return [{'pin': p} for p in + self.stored_configuration['sensors'].keys()] + + def actuator_configuration(self): + """Return the configuration map for syncing actuators.""" + return [{'pin': p, + 'trigger': (0 if data.get('activation') in [0, 'low'] else 1)} + for p, data in + self.stored_configuration['actuators'].items()] + + def sync_device(self): + """Sync the new pin configuration to the Konnected device.""" + desired_sensor_configuration = self.sensor_configuration() + current_sensor_configuration = [ + {'pin': s['pin']} for s in self.status.get('sensors')] + _LOGGER.info('%s: desired sensor config: %s', self.device_id, + desired_sensor_configuration) + _LOGGER.info('%s: current sensor config: %s', self.device_id, + current_sensor_configuration) + + desired_actuator_config = self.actuator_configuration() + current_actuator_config = self.status.get('actuators') + _LOGGER.info('%s: desired actuator config: %s', self.device_id, + desired_actuator_config) + _LOGGER.info('%s: current actuator config: %s', self.device_id, + current_actuator_config) + + if (desired_sensor_configuration != current_sensor_configuration) or \ + (current_actuator_config != desired_actuator_config): + _LOGGER.info('pushing settings to device %s', self.device_id) + self.client.put_settings( + desired_sensor_configuration, + desired_actuator_config, + self.hass.data[DOMAIN].get('auth_token'), + self.hass.config.api.base_url + ENDPOINT_ROOT + ) + + +class KonnectedView(HomeAssistantView): + """View creates an endpoint to receive push updates from the device.""" + + url = UPDATE_ENDPOINT + name = 'api:konnected' + requires_auth = False # Uses access token from configuration + + def __init__(self, auth_token): + """Initialize the view.""" + self.auth_token = auth_token + + @asyncio.coroutine + def put(self, request: Request, device_id, pin_num, state) -> Response: + """Receive a sensor update via PUT request and async set state.""" + hass = request.app['hass'] + data = hass.data[DOMAIN] + + auth = request.headers.get(AUTHORIZATION, None) + if 'Bearer {}'.format(self.auth_token) != auth: + return self.json_message( + "unauthorized", status_code=HTTP_UNAUTHORIZED) + pin_num = int(pin_num) + state = bool(int(state)) + device = data['devices'].get(device_id) + if device is None: + return self.json_message('unregistered device', + status_code=HTTP_BAD_REQUEST) + pin_data = device['sensors'].get(pin_num) or \ + device['actuators'].get(pin_num) + + if pin_data is None: + return self.json_message('unregistered sensor/actuator', + status_code=HTTP_BAD_REQUEST) + entity = pin_data.get('entity') + if entity is None: + return self.json_message('uninitialized sensor/actuator', + status_code=HTTP_INTERNAL_SERVER_ERROR) + + yield from entity.async_set_state(state) + return self.json_message('ok') diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py new file mode 100644 index 0000000000000..b307f1244a25c --- /dev/null +++ b/homeassistant/components/switch/konnected.py @@ -0,0 +1,83 @@ +""" +Support for wired switches attached to a Konnected device. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.konnected/ +""" + +import asyncio +import logging + +from homeassistant.components.konnected import (DOMAIN, PIN_TO_ZONE) +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['konnected'] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set switches attached to a Konnected device.""" + if discovery_info is None: + return + + data = hass.data[DOMAIN] + device_id = discovery_info['device_id'] + client = data['devices'][device_id]['client'] + actuators = [KonnectedSwitch(device_id, pin_num, pin_data, client) + for pin_num, pin_data in + data['devices'][device_id]['actuators'].items()] + async_add_devices(actuators, True) + + +class KonnectedSwitch(ToggleEntity): + """Representation of a Konnected switch.""" + + def __init__(self, device_id, pin_num, data, client): + """Initialize the switch.""" + self._data = data + self._device_id = device_id + self._pin_num = pin_num + self._state = self._data.get('state') + self._name = self._data.get( + 'name', 'Konnected {} Actuator {}'.format( + device_id, PIN_TO_ZONE[pin_num])) + self._data['entity'] = self + self._client = client + _LOGGER.info('Created new switch: %s', self._name) + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + def turn_on(self, **kwargs): + """Send a command to turn on the switch.""" + self._client.put_device(self._pin_num, 1) + self._set_state(True) + + def turn_off(self, **kwargs): + """Send a command to turn off the switch.""" + self._client.put_device(self._pin_num, 0) + self._set_state(False) + + def _set_state(self, state): + self._state = state + self._data['state'] = state + self.schedule_update_ha_state() + _LOGGER.info('Setting status of %s actuator pin %s to %s', + self._device_id, self.name, state) + + @asyncio.coroutine + def async_set_state(self, state): + """Update the switch's state.""" + self._state = state + self._data['state'] = state + self.async_schedule_update_ha_state() + _LOGGER.info('Updating state: %s is %s', self.name, state) diff --git a/requirements_all.txt b/requirements_all.txt index d6f811ba68c7b..d34e789689595 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,6 +464,9 @@ keyring==12.0.0 # homeassistant.scripts.keyring keyrings.alt==3.0 +# homeassistant.components.konnected +konnected==0.1.2 + # homeassistant.components.eufy lakeside==0.5 From bc87472b6e97d95017c9a07002235748bf729f01 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Sat, 7 Apr 2018 11:51:22 -0400 Subject: [PATCH 2/7] Use more built-in constants from const.py --- .../components/binary_sensor/konnected.py | 12 +- homeassistant/components/konnected.py | 111 +++++++++--------- homeassistant/components/switch/konnected.py | 17 +-- 3 files changed, 74 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/binary_sensor/konnected.py b/homeassistant/components/binary_sensor/konnected.py index c3841ad52d16b..db26a7d8380aa 100644 --- a/homeassistant/components/binary_sensor/konnected.py +++ b/homeassistant/components/binary_sensor/konnected.py @@ -9,6 +9,8 @@ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.konnected import (DOMAIN, PIN_TO_ZONE) +from homeassistant.const import ( + CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_SENSORS, ATTR_STATE) _LOGGER = logging.getLogger(__name__) @@ -25,7 +27,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): device_id = discovery_info['device_id'] sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data) for pin_num, pin_data in - data['devices'][device_id]['sensors'].items()] + data[CONF_DEVICES][device_id][CONF_SENSORS].items()] async_add_devices(sensors, True) @@ -37,9 +39,9 @@ def __init__(self, device_id, pin_num, data): self._data = data self._device_id = device_id self._pin_num = pin_num - self._state = self._data.get('state') - self._device_class = self._data.get('type', 'motion') - self._name = self._data.get('name', 'Konnected {} Zone {}'.format( + self._state = self._data.get(ATTR_STATE) + self._device_class = self._data.get(CONF_TYPE, 'motion') + self._name = self._data.get(CONF_NAME, 'Konnected {} Zone {}'.format( device_id, PIN_TO_ZONE[pin_num])) self._data['entity'] = self _LOGGER.info('Created new sensor: %s', self._name) @@ -68,6 +70,6 @@ def device_class(self): def async_set_state(self, state): """Update the sensor's state.""" self._state = state - self._data['state'] = state + self._data[ATTR_STATE] = state self.async_schedule_update_ha_state() _LOGGER.info('Updating state: %s is %s', self.name, state) diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 34c8052bbd988..aa5658b4fab34 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -15,8 +15,11 @@ from homeassistant.components.discovery import SERVICE_KONNECTED from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED) + HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, + CONF_DEVICES, CONF_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT, + CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, ATTR_STATE) from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation _LOGGER = logging.getLogger(__name__) @@ -29,27 +32,26 @@ CONFIG_SCHEMA = vol.Schema( { - DOMAIN: { - 'auth_token': str, - vol.Optional('home_assistant_url'): str, - 'devices': [{ - vol.Required('id', default=''): vol.Coerce(str), - 'sensors': [{ - vol.Exclusive('pin', 's_pin'): vol.Any(*PIN_TO_ZONE), - vol.Exclusive('zone', 's_pin'): vol.Any(*ZONE_TO_PIN), - vol.Required('type', default='motion'): + DOMAIN: vol.Schema({ + vol.Required('auth_token'): config_validation.string, + vol.Required(CONF_DEVICES): [{ + vol.Required(CONF_ID, default=''): config_validation.string, + vol.Optional(CONF_SENSORS): [{ + vol.Exclusive(CONF_PIN, 's_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN), + vol.Required(CONF_TYPE, default='motion'): DEVICE_CLASSES_SCHEMA, - vol.Optional('name'): str, + vol.Optional(CONF_NAME): config_validation.string, }], - 'actuators': [{ - vol.Exclusive('pin', 'a_pin'): vol.Any(*PIN_TO_ZONE), - vol.Exclusive('zone', 'a_pin'): vol.Any(*ZONE_TO_PIN), - vol.Optional('name'): str, + vol.Optional(CONF_SWITCHES): [{ + vol.Exclusive(CONF_PIN, 'a_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 'a_pin'): vol.Any(*ZONE_TO_PIN), + vol.Optional(CONF_NAME): config_validation.string, vol.Required('activation', default='high'): vol.All(vol.Lower, vol.Any('high', 'low')) }], }], - }, + }), }, extra=vol.ALLOW_EXTRA, ) @@ -69,7 +71,7 @@ def async_setup(hass, config): if cfg is None: cfg = {} - auth_token = cfg.get('auth_token') or 'supersecret' + auth_token = cfg.get('auth_token') if DOMAIN not in hass.data: hass.data[DOMAIN] = {'auth_token': auth_token} @@ -77,8 +79,8 @@ def async_setup(hass, config): def async_device_discovered(service, info): """Call when a Konnected device has been discovered.""" _LOGGER.info("Discovered a new Konnected device: %s", info) - host = info.get('host') - port = info.get('port') + host = info.get(CONF_HOST) + port = info.get(CONF_PORT) device = KonnectedDevice(hass, host, port, cfg) device.setup() @@ -134,9 +136,10 @@ def config(self): device_id = self.device_id valid_keys = [device_id, device_id.upper(), device_id[6:], device_id.upper()[6:]] - configured_devices = self.user_config['devices'] + configured_devices = self.user_config[CONF_DEVICES] return next((device for device in - configured_devices if device['id'] in valid_keys), None) + configured_devices if device[CONF_ID] in valid_keys), + None) def save_data(self): """Save the device configuration to `hass.data`. @@ -144,31 +147,32 @@ def save_data(self): TODO: This can probably be refactored and tidied up. """ sensors = {} - for entity in self.config().get('sensors') or []: - if 'zone' in entity: - pin = ZONE_TO_PIN[entity['zone']] + for entity in self.config().get(CONF_SENSORS) or []: + if CONF_ZONE in entity: + pin = ZONE_TO_PIN[entity[CONF_ZONE]] else: - pin = entity['pin'] + pin = entity[CONF_PIN] sensor_status = next((sensor for sensor in self.status.get('sensors') if - sensor.get('pin') == pin), {}) - if sensor_status.get('state'): - initial_state = bool(int(sensor_status.get('state'))) + sensor.get(CONF_PIN) == pin), {}) + if sensor_status.get(ATTR_STATE): + initial_state = bool(int(sensor_status.get(ATTR_STATE))) else: initial_state = None sensors[pin] = { - 'type': entity['type'], - 'name': entity.get('name', 'Konnected {} Zone {}'.format( + CONF_TYPE: entity[CONF_TYPE], + CONF_NAME: entity.get(CONF_NAME, 'Konnected {} Zone {}'.format( self.device_id[6:], PIN_TO_ZONE[pin])), - 'state': initial_state + ATTR_STATE: initial_state } _LOGGER.info('Set up sensor %s (initial state: %s)', - sensors[pin].get('name'), sensors[pin].get('state')) + sensors[pin].get('name'), + sensors[pin].get(ATTR_STATE)) actuators = {} - for entity in self.config().get('actuators') or []: + for entity in self.config().get(CONF_SWITCHES) or []: if 'zone' in entity: pin = ZONE_TO_PIN[entity['zone']] else: @@ -177,57 +181,58 @@ def save_data(self): actuator_status = next((actuator for actuator in self.status.get('actuators') if actuator.get('pin') == pin), {}) - if actuator_status.get('state'): - initial_state = bool(int(actuator_status.get('state'))) + if actuator_status.get(ATTR_STATE): + initial_state = bool(int(actuator_status.get(ATTR_STATE))) else: initial_state = None actuators[pin] = { - 'name': entity.get('name', 'Konnected {} Actuator {}'.format( - self.device_id[6:], PIN_TO_ZONE[pin])), - 'state': initial_state, + CONF_NAME: entity.get( + CONF_NAME, 'Konnected {} Actuator {}'.format( + self.device_id[6:], PIN_TO_ZONE[pin])), + ATTR_STATE: initial_state, 'activation': entity['activation'], } _LOGGER.info('Set up actuator %s (initial state: %s)', - actuators[pin].get('name'), - actuators[pin].get('state')) + actuators[pin].get(CONF_NAME), + actuators[pin].get(ATTR_STATE)) device_data = { 'client': self.client, - 'sensors': sensors, - 'actuators': actuators, - 'host': self.host, - 'port': self.port, + CONF_SENSORS: sensors, + CONF_SWITCHES: actuators, + CONF_HOST: self.host, + CONF_PORT: self.port, } if 'devices' not in self.hass.data[DOMAIN]: - self.hass.data[DOMAIN]['devices'] = {} + self.hass.data[DOMAIN][CONF_DEVICES] = {} _LOGGER.info('Storing data in hass.data[konnected]: %s', device_data) - self.hass.data[DOMAIN]['devices'][self.device_id] = device_data + self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data @property def stored_configuration(self): """Return the configuration stored in `hass.data` for this device.""" - return self.hass.data[DOMAIN]['devices'][self.device_id] + return self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] def sensor_configuration(self): """Return the configuration map for syncing sensors.""" return [{'pin': p} for p in - self.stored_configuration['sensors'].keys()] + self.stored_configuration[CONF_SENSORS].keys()] def actuator_configuration(self): """Return the configuration map for syncing actuators.""" return [{'pin': p, 'trigger': (0 if data.get('activation') in [0, 'low'] else 1)} for p, data in - self.stored_configuration['actuators'].items()] + self.stored_configuration[CONF_SWITCHES].items()] def sync_device(self): """Sync the new pin configuration to the Konnected device.""" desired_sensor_configuration = self.sensor_configuration() current_sensor_configuration = [ - {'pin': s['pin']} for s in self.status.get('sensors')] + {'pin': s[CONF_PIN]} for s in self.status.get('sensors')] _LOGGER.info('%s: desired sensor config: %s', self.device_id, desired_sensor_configuration) _LOGGER.info('%s: current sensor config: %s', self.device_id, @@ -274,12 +279,12 @@ def put(self, request: Request, device_id, pin_num, state) -> Response: "unauthorized", status_code=HTTP_UNAUTHORIZED) pin_num = int(pin_num) state = bool(int(state)) - device = data['devices'].get(device_id) + device = data[CONF_DEVICES].get(device_id) if device is None: return self.json_message('unregistered device', status_code=HTTP_BAD_REQUEST) - pin_data = device['sensors'].get(pin_num) or \ - device['actuators'].get(pin_num) + pin_data = device[CONF_SENSORS].get(pin_num) or \ + device[CONF_SWITCHES].get(pin_num) if pin_data is None: return self.json_message('unregistered sensor/actuator', diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py index b307f1244a25c..8b1decf8bd506 100644 --- a/homeassistant/components/switch/konnected.py +++ b/homeassistant/components/switch/konnected.py @@ -10,6 +10,7 @@ from homeassistant.components.konnected import (DOMAIN, PIN_TO_ZONE) from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import (CONF_DEVICES, CONF_SWITCHES, ATTR_STATE) _LOGGER = logging.getLogger(__name__) @@ -24,11 +25,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): data = hass.data[DOMAIN] device_id = discovery_info['device_id'] - client = data['devices'][device_id]['client'] - actuators = [KonnectedSwitch(device_id, pin_num, pin_data, client) - for pin_num, pin_data in - data['devices'][device_id]['actuators'].items()] - async_add_devices(actuators, True) + client = data[CONF_DEVICES][device_id]['client'] + switches = [KonnectedSwitch(device_id, pin_num, pin_data, client) + for pin_num, pin_data in + data[CONF_DEVICES][device_id][CONF_SWITCHES].items()] + async_add_devices(switches, True) class KonnectedSwitch(ToggleEntity): @@ -39,7 +40,7 @@ def __init__(self, device_id, pin_num, data, client): self._data = data self._device_id = device_id self._pin_num = pin_num - self._state = self._data.get('state') + self._state = self._data.get(ATTR_STATE) self._name = self._data.get( 'name', 'Konnected {} Actuator {}'.format( device_id, PIN_TO_ZONE[pin_num])) @@ -69,7 +70,7 @@ def turn_off(self, **kwargs): def _set_state(self, state): self._state = state - self._data['state'] = state + self._data[ATTR_STATE] = state self.schedule_update_ha_state() _LOGGER.info('Setting status of %s actuator pin %s to %s', self._device_id, self.name, state) @@ -78,6 +79,6 @@ def _set_state(self, state): def async_set_state(self, state): """Update the switch's state.""" self._state = state - self._data['state'] = state + self._data[ATTR_STATE] = state self.async_schedule_update_ha_state() _LOGGER.info('Updating state: %s is %s', self.name, state) From ecc1175920b178cd1952c8cbb6644510ed9ea02e Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Sun, 15 Apr 2018 11:27:24 -0400 Subject: [PATCH 3/7] Fix switch actuation with low-level trigger --- homeassistant/components/switch/konnected.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py index 8b1decf8bd506..6c75b0969e21e 100644 --- a/homeassistant/components/switch/konnected.py +++ b/homeassistant/components/switch/konnected.py @@ -41,6 +41,7 @@ def __init__(self, device_id, pin_num, data, client): self._device_id = device_id self._pin_num = pin_num self._state = self._data.get(ATTR_STATE) + self._activation = self._data.get('activation', 'high') self._name = self._data.get( 'name', 'Konnected {} Actuator {}'.format( device_id, PIN_TO_ZONE[pin_num])) @@ -60,12 +61,12 @@ def is_on(self): def turn_on(self, **kwargs): """Send a command to turn on the switch.""" - self._client.put_device(self._pin_num, 1) + self._client.put_device(self._pin_num, int(self._activation == 'high')) self._set_state(True) def turn_off(self, **kwargs): """Send a command to turn off the switch.""" - self._client.put_device(self._pin_num, 0) + self._client.put_device(self._pin_num, int(self._activation == 'low')) self._set_state(False) def _set_state(self, state): From a0da4b229ad94d282b308edf57249276fc7e8bd1 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Sat, 21 Apr 2018 22:26:00 -0400 Subject: [PATCH 4/7] Quiet logging; Improve schema validation. --- .../components/binary_sensor/konnected.py | 7 +-- homeassistant/components/konnected.py | 62 +++++++++++-------- homeassistant/components/switch/konnected.py | 5 +- 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/binary_sensor/konnected.py b/homeassistant/components/binary_sensor/konnected.py index db26a7d8380aa..894ba891ab374 100644 --- a/homeassistant/components/binary_sensor/konnected.py +++ b/homeassistant/components/binary_sensor/konnected.py @@ -28,7 +28,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data) for pin_num, pin_data in data[CONF_DEVICES][device_id][CONF_SENSORS].items()] - async_add_devices(sensors, True) + async_add_devices(sensors) class KonnectedBinarySensor(BinarySensorDevice): @@ -40,11 +40,11 @@ def __init__(self, device_id, pin_num, data): self._device_id = device_id self._pin_num = pin_num self._state = self._data.get(ATTR_STATE) - self._device_class = self._data.get(CONF_TYPE, 'motion') + self._device_class = self._data.get(CONF_TYPE) self._name = self._data.get(CONF_NAME, 'Konnected {} Zone {}'.format( device_id, PIN_TO_ZONE[pin_num])) self._data['entity'] = self - _LOGGER.info('Created new sensor: %s', self._name) + _LOGGER.debug('Created new Konnected sensor: %s', self._name) @property def name(self): @@ -72,4 +72,3 @@ def async_set_state(self, state): self._state = state self._data[ATTR_STATE] = state self.async_schedule_update_ha_state() - _LOGGER.info('Updating state: %s is %s', self.name, state) diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index aa5658b4fab34..578aba3a8ebaa 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -6,6 +6,7 @@ """ import asyncio import logging +import hmac import voluptuous as vol from aiohttp.hdrs import AUTHORIZATION @@ -30,33 +31,40 @@ PIN_TO_ZONE = {1: 1, 2: 2, 5: 3, 6: 4, 7: 5, 8: 'out', 9: 6} ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()} +_SENSOR_SCHEMA = vol.All( + vol.Schema({ + vol.Exclusive(CONF_PIN, 's_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN), + vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_NAME): config_validation.string, + }), config_validation.has_at_least_one_key(CONF_PIN, CONF_ZONE) +) + +_SWITCH_SCHEMA = vol.All( + vol.Schema({ + vol.Exclusive(CONF_PIN, 'a_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 'a_pin'): vol.Any(*ZONE_TO_PIN), + vol.Optional(CONF_NAME): config_validation.string, + vol.Optional('activation', default='high'): + vol.All(vol.Lower, vol.Any('high', 'low')) + }), config_validation.has_at_least_one_key(CONF_PIN, CONF_ZONE) +) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema({ vol.Required('auth_token'): config_validation.string, vol.Required(CONF_DEVICES): [{ vol.Required(CONF_ID, default=''): config_validation.string, - vol.Optional(CONF_SENSORS): [{ - vol.Exclusive(CONF_PIN, 's_pin'): vol.Any(*PIN_TO_ZONE), - vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN), - vol.Required(CONF_TYPE, default='motion'): - DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_NAME): config_validation.string, - }], - vol.Optional(CONF_SWITCHES): [{ - vol.Exclusive(CONF_PIN, 'a_pin'): vol.Any(*PIN_TO_ZONE), - vol.Exclusive(CONF_ZONE, 'a_pin'): vol.Any(*ZONE_TO_PIN), - vol.Optional(CONF_NAME): config_validation.string, - vol.Required('activation', default='high'): - vol.All(vol.Lower, vol.Any('high', 'low')) - }], + vol.Optional(CONF_SENSORS): [_SENSOR_SCHEMA], + vol.Optional(CONF_SWITCHES): [_SWITCH_SCHEMA], }], }), }, extra=vol.ALLOW_EXTRA, ) -DEPENDENCIES = ['http'] +DEPENDENCIES = ['http', 'discovery'] ENDPOINT_ROOT = '/api/konnected' UPDATE_ENDPOINT = ( @@ -78,7 +86,7 @@ def async_setup(hass, config): @asyncio.coroutine def async_device_discovered(service, info): """Call when a Konnected device has been discovered.""" - _LOGGER.info("Discovered a new Konnected device: %s", info) + _LOGGER.debug("Discovered a new Konnected device: %s", info) host = info.get(CONF_HOST) port = info.get(CONF_PORT) @@ -114,7 +122,7 @@ def setup(self): """Set up a newly discovered Konnected device.""" user_config = self.config() if user_config: - _LOGGER.info('Configuring Konnected device %s', self.device_id) + _LOGGER.debug('Configuring Konnected device %s', self.device_id) self.save_data() self.sync_device() self.hass.async_add_job( @@ -167,7 +175,7 @@ def save_data(self): self.device_id[6:], PIN_TO_ZONE[pin])), ATTR_STATE: initial_state } - _LOGGER.info('Set up sensor %s (initial state: %s)', + _LOGGER.debug('Set up sensor %s (initial state: %s)', sensors[pin].get('name'), sensors[pin].get(ATTR_STATE)) @@ -193,7 +201,7 @@ def save_data(self): ATTR_STATE: initial_state, 'activation': entity['activation'], } - _LOGGER.info('Set up actuator %s (initial state: %s)', + _LOGGER.debug('Set up actuator %s (initial state: %s)', actuators[pin].get(CONF_NAME), actuators[pin].get(ATTR_STATE)) @@ -205,10 +213,10 @@ def save_data(self): CONF_PORT: self.port, } - if 'devices' not in self.hass.data[DOMAIN]: + if CONF_DEVICES not in self.hass.data[DOMAIN]: self.hass.data[DOMAIN][CONF_DEVICES] = {} - _LOGGER.info('Storing data in hass.data[konnected]: %s', device_data) + _LOGGER.debug('Storing data in hass.data[konnected]: %s', device_data) self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data @property @@ -233,21 +241,21 @@ def sync_device(self): desired_sensor_configuration = self.sensor_configuration() current_sensor_configuration = [ {'pin': s[CONF_PIN]} for s in self.status.get('sensors')] - _LOGGER.info('%s: desired sensor config: %s', self.device_id, + _LOGGER.debug('%s: desired sensor config: %s', self.device_id, desired_sensor_configuration) - _LOGGER.info('%s: current sensor config: %s', self.device_id, + _LOGGER.debug('%s: current sensor config: %s', self.device_id, current_sensor_configuration) desired_actuator_config = self.actuator_configuration() current_actuator_config = self.status.get('actuators') - _LOGGER.info('%s: desired actuator config: %s', self.device_id, + _LOGGER.debug('%s: desired actuator config: %s', self.device_id, desired_actuator_config) - _LOGGER.info('%s: current actuator config: %s', self.device_id, + _LOGGER.debug('%s: current actuator config: %s', self.device_id, current_actuator_config) if (desired_sensor_configuration != current_sensor_configuration) or \ (current_actuator_config != desired_actuator_config): - _LOGGER.info('pushing settings to device %s', self.device_id) + _LOGGER.debug('pushing settings to device %s', self.device_id) self.client.put_settings( desired_sensor_configuration, desired_actuator_config, @@ -274,7 +282,7 @@ def put(self, request: Request, device_id, pin_num, state) -> Response: data = hass.data[DOMAIN] auth = request.headers.get(AUTHORIZATION, None) - if 'Bearer {}'.format(self.auth_token) != auth: + if not hmac.compare_digest('Bearer {}'.format(self.auth_token), auth): return self.json_message( "unauthorized", status_code=HTTP_UNAUTHORIZED) pin_num = int(pin_num) diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py index 6c75b0969e21e..d069fe378592e 100644 --- a/homeassistant/components/switch/konnected.py +++ b/homeassistant/components/switch/konnected.py @@ -47,7 +47,7 @@ def __init__(self, device_id, pin_num, data, client): device_id, PIN_TO_ZONE[pin_num])) self._data['entity'] = self self._client = client - _LOGGER.info('Created new switch: %s', self._name) + _LOGGER.debug('Created new switch: %s', self._name) @property def name(self): @@ -73,7 +73,7 @@ def _set_state(self, state): self._state = state self._data[ATTR_STATE] = state self.schedule_update_ha_state() - _LOGGER.info('Setting status of %s actuator pin %s to %s', + _LOGGER.debug('Setting status of %s actuator pin %s to %s', self._device_id, self.name, state) @asyncio.coroutine @@ -82,4 +82,3 @@ def async_set_state(self, state): self._state = state self._data[ATTR_STATE] = state self.async_schedule_update_ha_state() - _LOGGER.info('Updating state: %s is %s', self.name, state) From df74c5ef12e0ec3c9432d6e62e338f0e6de1fee9 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Sun, 22 Apr 2018 00:37:17 -0400 Subject: [PATCH 5/7] Execute sync request outside of event loop --- homeassistant/components/binary_sensor/konnected.py | 3 +-- homeassistant/components/konnected.py | 8 +++----- homeassistant/components/switch/konnected.py | 5 ++--- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/binary_sensor/konnected.py b/homeassistant/components/binary_sensor/konnected.py index 894ba891ab374..ae290821287a0 100644 --- a/homeassistant/components/binary_sensor/konnected.py +++ b/homeassistant/components/binary_sensor/konnected.py @@ -17,8 +17,7 @@ DEPENDENCIES = ['konnected'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up binary sensors attached to a Konnected device.""" if discovery_info is None: return diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 578aba3a8ebaa..9fb57a42dc8cf 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -72,8 +72,7 @@ r'/device/{device_id:[a-zA-Z0-9]+}/{pin_num:[0-9]}/{state:[01]}') -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Konnected platform.""" cfg = config.get(DOMAIN) if cfg is None: @@ -83,8 +82,7 @@ def async_setup(hass, config): if DOMAIN not in hass.data: hass.data[DOMAIN] = {'auth_token': auth_token} - @asyncio.coroutine - def async_device_discovered(service, info): + async def async_device_discovered(service, info): """Call when a Konnected device has been discovered.""" _LOGGER.debug("Discovered a new Konnected device: %s", info) host = info.get(CONF_HOST) @@ -124,7 +122,7 @@ def setup(self): if user_config: _LOGGER.debug('Configuring Konnected device %s', self.device_id) self.save_data() - self.sync_device() + self.hass.async_add_job(self.sync_device) self.hass.async_add_job( discovery.async_load_platform( self.hass, 'binary_sensor', diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py index d069fe378592e..b2906f6f47d34 100644 --- a/homeassistant/components/switch/konnected.py +++ b/homeassistant/components/switch/konnected.py @@ -17,8 +17,7 @@ DEPENDENCIES = ['konnected'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set switches attached to a Konnected device.""" if discovery_info is None: return @@ -29,7 +28,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): switches = [KonnectedSwitch(device_id, pin_num, pin_data, client) for pin_num, pin_data in data[CONF_DEVICES][device_id][CONF_SWITCHES].items()] - async_add_devices(switches, True) + async_add_devices(switches) class KonnectedSwitch(ToggleEntity): From 6c4e3998b2d6e6d358ed6106b3ad5ab20529da3a Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Sun, 22 Apr 2018 00:42:52 -0400 Subject: [PATCH 6/7] Whitespace cleanup --- .../components/binary_sensor/konnected.py | 3 ++- homeassistant/components/konnected.py | 16 ++++++++-------- homeassistant/components/switch/konnected.py | 5 +++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/binary_sensor/konnected.py b/homeassistant/components/binary_sensor/konnected.py index ae290821287a0..8648c1921d320 100644 --- a/homeassistant/components/binary_sensor/konnected.py +++ b/homeassistant/components/binary_sensor/konnected.py @@ -17,7 +17,8 @@ DEPENDENCIES = ['konnected'] -async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up binary sensors attached to a Konnected device.""" if discovery_info is None: return diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 9fb57a42dc8cf..28db38e8e5c77 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -174,8 +174,8 @@ def save_data(self): ATTR_STATE: initial_state } _LOGGER.debug('Set up sensor %s (initial state: %s)', - sensors[pin].get('name'), - sensors[pin].get(ATTR_STATE)) + sensors[pin].get('name'), + sensors[pin].get(ATTR_STATE)) actuators = {} for entity in self.config().get(CONF_SWITCHES) or []: @@ -200,8 +200,8 @@ def save_data(self): 'activation': entity['activation'], } _LOGGER.debug('Set up actuator %s (initial state: %s)', - actuators[pin].get(CONF_NAME), - actuators[pin].get(ATTR_STATE)) + actuators[pin].get(CONF_NAME), + actuators[pin].get(ATTR_STATE)) device_data = { 'client': self.client, @@ -240,16 +240,16 @@ def sync_device(self): current_sensor_configuration = [ {'pin': s[CONF_PIN]} for s in self.status.get('sensors')] _LOGGER.debug('%s: desired sensor config: %s', self.device_id, - desired_sensor_configuration) + desired_sensor_configuration) _LOGGER.debug('%s: current sensor config: %s', self.device_id, - current_sensor_configuration) + current_sensor_configuration) desired_actuator_config = self.actuator_configuration() current_actuator_config = self.status.get('actuators') _LOGGER.debug('%s: desired actuator config: %s', self.device_id, - desired_actuator_config) + desired_actuator_config) _LOGGER.debug('%s: current actuator config: %s', self.device_id, - current_actuator_config) + current_actuator_config) if (desired_sensor_configuration != current_sensor_configuration) or \ (current_actuator_config != desired_actuator_config): diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py index b2906f6f47d34..57688a8bd8925 100644 --- a/homeassistant/components/switch/konnected.py +++ b/homeassistant/components/switch/konnected.py @@ -17,7 +17,8 @@ DEPENDENCIES = ['konnected'] -async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set switches attached to a Konnected device.""" if discovery_info is None: return @@ -73,7 +74,7 @@ def _set_state(self, state): self._data[ATTR_STATE] = state self.schedule_update_ha_state() _LOGGER.debug('Setting status of %s actuator pin %s to %s', - self._device_id, self.name, state) + self._device_id, self.name, state) @asyncio.coroutine def async_set_state(self, state): From 71cb33f0e39e4c693796a2dfd541dee86cae1fcf Mon Sep 17 00:00:00 2001 From: William Sarra Date: Sat, 28 Apr 2018 22:55:12 -0700 Subject: [PATCH 7/7] Added services to beep switches --- homeassistant/components/konnected.py | 194 ++++++++++++++++++------- homeassistant/components/services.yaml | 49 +++++-- 2 files changed, 181 insertions(+), 62 deletions(-) mode change 100644 => 100755 homeassistant/components/konnected.py mode change 100644 => 100755 homeassistant/components/services.yaml diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py old mode 100644 new mode 100755 index 28db38e8e5c77..22be3b00ad7b4 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -6,7 +6,6 @@ """ import asyncio import logging -import hmac import voluptuous as vol from aiohttp.hdrs import AUTHORIZATION @@ -18,9 +17,15 @@ from homeassistant.const import ( HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, CONF_DEVICES, CONF_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT, - CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, ATTR_STATE) -from homeassistant.helpers import discovery -from homeassistant.helpers import config_validation + CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, ATTR_STATE, ATTR_ENTITY_ID) +from homeassistant.helpers import discovery, config_validation + +''' Entity based lookup for services''' +from homeassistant.helpers.entity_component import EntityComponent +from datetime import timedelta +SCAN_INTERVAL = timedelta(seconds=30) +GROUP_NAME_ALL_SWITCHES = 'all switches' +ENTITY_ID_FORMAT = 'switch.{}' _LOGGER = logging.getLogger(__name__) @@ -31,40 +36,52 @@ PIN_TO_ZONE = {1: 1, 2: 2, 5: 3, 6: 4, 7: 5, 8: 'out', 9: 6} ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()} -_SENSOR_SCHEMA = vol.All( - vol.Schema({ - vol.Exclusive(CONF_PIN, 's_pin'): vol.Any(*PIN_TO_ZONE), - vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN), - vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_NAME): config_validation.string, - }), config_validation.has_at_least_one_key(CONF_PIN, CONF_ZONE) -) - -_SWITCH_SCHEMA = vol.All( - vol.Schema({ - vol.Exclusive(CONF_PIN, 'a_pin'): vol.Any(*PIN_TO_ZONE), - vol.Exclusive(CONF_ZONE, 'a_pin'): vol.Any(*ZONE_TO_PIN), - vol.Optional(CONF_NAME): config_validation.string, - vol.Optional('activation', default='high'): - vol.All(vol.Lower, vol.Any('high', 'low')) - }), config_validation.has_at_least_one_key(CONF_PIN, CONF_ZONE) -) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema({ vol.Required('auth_token'): config_validation.string, vol.Required(CONF_DEVICES): [{ vol.Required(CONF_ID, default=''): config_validation.string, - vol.Optional(CONF_SENSORS): [_SENSOR_SCHEMA], - vol.Optional(CONF_SWITCHES): [_SWITCH_SCHEMA], + vol.Optional(CONF_SENSORS): [{ + vol.Exclusive(CONF_PIN, 's_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN), + vol.Required(CONF_TYPE, default='motion'): + DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_NAME): config_validation.string, + }], + vol.Optional(CONF_SWITCHES): [{ + vol.Exclusive(CONF_PIN, 'a_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 'a_pin'): vol.Any(*ZONE_TO_PIN), + vol.Optional(CONF_NAME): config_validation.string, + vol.Required('activation', default='high'): + vol.All(vol.Lower, vol.Any('high', 'low')) + }], }], }), }, extra=vol.ALLOW_EXTRA, ) -DEPENDENCIES = ['http', 'discovery'] +SERVICE_BEEP_DEVICE_SCHEMA = vol.Schema( + { + vol.Required('device_id'): config_validation.string, + vol.Required(CONF_PIN): vol.Any(*PIN_TO_ZONE), + vol.Optional('momentary'): vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional('times'): vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional('pause'): vol.All(vol.Coerce(int), vol.Range(min=0)), + } +) + +SERVICE_BEEP_DEVICE_BY_ENTITY_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): config_validation.entity_ids, + vol.Optional('momentary'): vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional('times'): vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional('pause'): vol.All(vol.Coerce(int), vol.Range(min=0)), + } +) + +DEPENDENCIES = ['http'] ENDPOINT_ROOT = '/api/konnected' UPDATE_ENDPOINT = ( @@ -72,7 +89,8 @@ r'/device/{device_id:[a-zA-Z0-9]+}/{pin_num:[0-9]}/{state:[01]}') -async def async_setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Set up the Konnected platform.""" cfg = config.get(DOMAIN) if cfg is None: @@ -81,10 +99,11 @@ async def async_setup(hass, config): auth_token = cfg.get('auth_token') if DOMAIN not in hass.data: hass.data[DOMAIN] = {'auth_token': auth_token} - - async def async_device_discovered(service, info): + + @asyncio.coroutine + def async_device_discovered(service, info): """Call when a Konnected device has been discovered.""" - _LOGGER.debug("Discovered a new Konnected device: %s", info) + _LOGGER.info("Discovered a new Konnected device: %s", info) host = info.get(CONF_HOST) port = info.get(CONF_PORT) @@ -98,6 +117,81 @@ async def async_device_discovered(service, info): hass.http.register_view(KonnectedView(auth_token)) + """Basic beep device service + {"device_id":"ecfabc07e14b", "pin":7, "momentary":100, "times":3, "pause":100} + """ + @asyncio.coroutine + def beep_device(call): + data = hass.data[DOMAIN] + + #find device with matching entity name + device_id = call.data.get('device_id') + device = data[CONF_DEVICES].get(device_id) + if device is None: + _LOGGER.error("Invalid device id specified: %s", device_id) + return False + client = data[CONF_DEVICES][device_id]['client'] + + device_pin = call.data.get(CONF_PIN) + #check if pin is valid switch + if device_pin not in data[CONF_DEVICES][device_id][CONF_SWITCHES]: + _LOGGER.error("Specified pin not configured: %s", device_pin) + return False + + flashcount = call.data.get('momentary', None) + times = call.data.get('times', None) + pause = call.data.get('pause', None) + + #def put_device(self, pin, state, momentary=None, times=None, pause=None): + #state 1 == ON + client.put_device(device_pin, 1, flashcount, times, pause) + + """Register Basic beep device service """ + hass.services.async_register(DOMAIN, 'beepDevice', beep_device, schema=SERVICE_BEEP_DEVICE_SCHEMA) + + """Entity based beep device service + {"entity_id":"switch.Piezo", "momentary":100, "times":3, "pause":100} + """ + @asyncio.coroutine + def beep_device_by_entity(call): + #FIXME - how to get drop down? + component = EntityComponent(_LOGGER, 'switch', hass, SCAN_INTERVAL, GROUP_NAME_ALL_SWITCHES) + target_switches = component.async_extract_from_service(call) + for switch in target_switches: + _LOGGER.info("Extracted switch name: %s", switch) + + data = hass.data[DOMAIN] + + #get entity name from call - find hosting device + for entityId in call.data.get('entity_id'): + #_LOGGER.info("***Entity id: %s", entityId) + for device in data[CONF_DEVICES]: + for switch in data[CONF_DEVICES][device][CONF_SWITCHES]: + kswitch = data[CONF_DEVICES][device][CONF_SWITCHES][switch] + #_LOGGER.info("Device: %s; Configured switch name: %s", device, kswitch['name']) + targetName = "switch." + kswitch['name'] + #_LOGGER.info("***Target name: %s", targetName) + if ( targetName.lower() == entityId.lower() ): + _LOGGER.info("Found switch %s on device: %s, at pin: %s", entityId, device, switch) + + #TODO + client = data[CONF_DEVICES][device]['client'] + flashcount = call.data.get('momentary', None) + times = call.data.get('times', None) + pause = call.data.get('pause', None) + + #def put_device(self, pin, state, momentary=None, times=None, pause=None): + #state 1 == ON + client.put_device(switch, 1, flashcount, times, pause) + + return True + + _LOGGER.error("Specified entity is not found as a Konnected device: %s", entityId) + return False + + """Register Entity based beep device service """ + hass.services.async_register(DOMAIN, 'beepDevice_ByEntityId', beep_device_by_entity, schema=SERVICE_BEEP_DEVICE_BY_ENTITY_SCHEMA) + return True @@ -120,9 +214,9 @@ def setup(self): """Set up a newly discovered Konnected device.""" user_config = self.config() if user_config: - _LOGGER.debug('Configuring Konnected device %s', self.device_id) + _LOGGER.info('Configuring Konnected device %s', self.device_id) self.save_data() - self.hass.async_add_job(self.sync_device) + self.sync_device() self.hass.async_add_job( discovery.async_load_platform( self.hass, 'binary_sensor', @@ -173,9 +267,9 @@ def save_data(self): self.device_id[6:], PIN_TO_ZONE[pin])), ATTR_STATE: initial_state } - _LOGGER.debug('Set up sensor %s (initial state: %s)', - sensors[pin].get('name'), - sensors[pin].get(ATTR_STATE)) + _LOGGER.info('Set up sensor %s (initial state: %s)', + sensors[pin].get('name'), + sensors[pin].get(ATTR_STATE)) actuators = {} for entity in self.config().get(CONF_SWITCHES) or []: @@ -199,9 +293,9 @@ def save_data(self): ATTR_STATE: initial_state, 'activation': entity['activation'], } - _LOGGER.debug('Set up actuator %s (initial state: %s)', - actuators[pin].get(CONF_NAME), - actuators[pin].get(ATTR_STATE)) + _LOGGER.info('Set up actuator %s (initial state: %s)', + actuators[pin].get(CONF_NAME), + actuators[pin].get(ATTR_STATE)) device_data = { 'client': self.client, @@ -211,10 +305,10 @@ def save_data(self): CONF_PORT: self.port, } - if CONF_DEVICES not in self.hass.data[DOMAIN]: + if 'devices' not in self.hass.data[DOMAIN]: self.hass.data[DOMAIN][CONF_DEVICES] = {} - _LOGGER.debug('Storing data in hass.data[konnected]: %s', device_data) + _LOGGER.info('Storing data in hass.data[konnected]: %s', device_data) self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data @property @@ -239,21 +333,21 @@ def sync_device(self): desired_sensor_configuration = self.sensor_configuration() current_sensor_configuration = [ {'pin': s[CONF_PIN]} for s in self.status.get('sensors')] - _LOGGER.debug('%s: desired sensor config: %s', self.device_id, - desired_sensor_configuration) - _LOGGER.debug('%s: current sensor config: %s', self.device_id, - current_sensor_configuration) + _LOGGER.info('%s: desired sensor config: %s', self.device_id, + desired_sensor_configuration) + _LOGGER.info('%s: current sensor config: %s', self.device_id, + current_sensor_configuration) desired_actuator_config = self.actuator_configuration() current_actuator_config = self.status.get('actuators') - _LOGGER.debug('%s: desired actuator config: %s', self.device_id, - desired_actuator_config) - _LOGGER.debug('%s: current actuator config: %s', self.device_id, - current_actuator_config) + _LOGGER.info('%s: desired actuator config: %s', self.device_id, + desired_actuator_config) + _LOGGER.info('%s: current actuator config: %s', self.device_id, + current_actuator_config) if (desired_sensor_configuration != current_sensor_configuration) or \ (current_actuator_config != desired_actuator_config): - _LOGGER.debug('pushing settings to device %s', self.device_id) + _LOGGER.info('pushing settings to device %s', self.device_id) self.client.put_settings( desired_sensor_configuration, desired_actuator_config, @@ -280,7 +374,7 @@ def put(self, request: Request, device_id, pin_num, state) -> Response: data = hass.data[DOMAIN] auth = request.headers.get(AUTHORIZATION, None) - if not hmac.compare_digest('Bearer {}'.format(self.auth_token), auth): + if 'Bearer {}'.format(self.auth_token) != auth: return self.json_message( "unauthorized", status_code=HTTP_UNAUTHORIZED) pin_num = int(pin_num) diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml old mode 100644 new mode 100755 index 746c3c7f4838f..9383c9b6657fb --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -395,18 +395,6 @@ snips: intent_filter: description: Optional Array of Strings - A list of intents names to restrict the NLU resolution to on the first query. example: turnOnLights, turnOffLights - feedback_on: - description: Turns feedback sounds on. - fields: - site_id: - description: Site to turn sounds on, defaults to all sites (optional) - example: bedroom - feedback_off: - description: Turns feedback sounds off. - fields: - site_id: - description: Site to turn sounds on, defaults to all sites (optional) - example: bedroom input_boolean: toggle: @@ -556,3 +544,40 @@ xiaomi_aqara: device_id: description: Hardware address of the device to remove. example: 158d0000000000 + +konnected: + #{"device_id":"34ce00880088", "pin":7, "momentary":100, "times":3, "pause":100} + beepdevice: + description: Beep (turn on) specified device. + fields: + device_id: + description: MAC address of Konnected board hosting device. + example: '34ce00880088' + pin: + description: Pin number on board of device. + example: 7 + momentary: + description: Milliseconds for the device to be turned on. (Optional, default = 1) + example: 100 + times: + description: Number of times to turn the device on. (Optional, default = 1) + example: 3 + pause: + description: Milliseconds to pause between turning on/off (Optional, default = 0) + example: 100 + #{"entity_id":"switch.buzzer", "momentary":100, "times":3, "pause":100} + beepdevice_byentityid: + description: Beep (turn on) specified entity(s). The entity must be a switch. Will search for matching name across all known Konnected boards. + fields: + entity_id: + description: Entity id of device to be turned on. + example: 'switch.buzzer' + momentary: + description: Milliseconds for the device to be turned on. (Optional, default = 1) + example: 100 + times: + description: Number of times to turn the device on. (Optional, default = 1) + example: 3 + pause: + description: Milliseconds to pause between turning on/off (Optional, default = 0) + example: 100