2018-05-15 17:58:00 +00:00
|
|
|
"""
|
|
|
|
Support for Konnected devices.
|
|
|
|
|
|
|
|
For more details about this component, please refer to the documentation at
|
|
|
|
https://home-assistant.io/components/konnected/
|
|
|
|
"""
|
2018-10-16 22:39:32 +00:00
|
|
|
import asyncio
|
2018-05-15 17:58:00 +00:00
|
|
|
import hmac
|
|
|
|
import json
|
2018-10-16 22:39:32 +00:00
|
|
|
import logging
|
|
|
|
|
2018-05-15 17:58:00 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
|
|
|
from aiohttp.hdrs import AUTHORIZATION
|
2018-06-25 17:05:07 +00:00
|
|
|
from aiohttp.web import Request, Response
|
2018-05-15 17:58:00 +00:00
|
|
|
|
|
|
|
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 (
|
2018-10-16 22:39:32 +00:00
|
|
|
EVENT_HOMEASSISTANT_START, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
|
|
|
HTTP_UNAUTHORIZED, CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES,
|
|
|
|
CONF_HOST, CONF_PORT, CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE,
|
|
|
|
CONF_ACCESS_TOKEN, ATTR_ENTITY_ID, ATTR_STATE, STATE_ON)
|
|
|
|
from homeassistant.helpers.dispatcher import (
|
|
|
|
async_dispatcher_send, dispatcher_send)
|
2018-05-15 17:58:00 +00:00
|
|
|
from homeassistant.helpers import discovery
|
|
|
|
from homeassistant.helpers import config_validation as cv
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2018-10-16 22:39:32 +00:00
|
|
|
REQUIREMENTS = ['konnected==0.1.4']
|
2018-05-15 17:58:00 +00:00
|
|
|
|
|
|
|
DOMAIN = 'konnected'
|
|
|
|
|
|
|
|
CONF_ACTIVATION = 'activation'
|
2018-06-10 11:50:25 +00:00
|
|
|
CONF_API_HOST = 'api_host'
|
2018-08-14 19:15:33 +00:00
|
|
|
CONF_MOMENTARY = 'momentary'
|
|
|
|
CONF_PAUSE = 'pause'
|
|
|
|
CONF_REPEAT = 'repeat'
|
2018-09-12 11:54:38 +00:00
|
|
|
CONF_INVERSE = 'inverse'
|
2018-10-16 22:39:32 +00:00
|
|
|
CONF_BLINK = 'blink'
|
|
|
|
CONF_DISCOVERY = 'discovery'
|
2018-08-14 19:15:33 +00:00
|
|
|
|
2018-05-15 17:58:00 +00:00
|
|
|
STATE_LOW = 'low'
|
|
|
|
STATE_HIGH = 'high'
|
|
|
|
|
|
|
|
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()}
|
|
|
|
|
|
|
|
_BINARY_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): cv.string,
|
2018-10-16 22:39:32 +00:00
|
|
|
vol.Optional(CONF_INVERSE, default=False): cv.boolean,
|
2018-05-15 17:58:00 +00:00
|
|
|
}), cv.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): cv.string,
|
|
|
|
vol.Optional(CONF_ACTIVATION, default=STATE_HIGH):
|
2018-08-14 19:15:33 +00:00
|
|
|
vol.All(vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)),
|
|
|
|
vol.Optional(CONF_MOMENTARY):
|
|
|
|
vol.All(vol.Coerce(int), vol.Range(min=10)),
|
|
|
|
vol.Optional(CONF_PAUSE):
|
|
|
|
vol.All(vol.Coerce(int), vol.Range(min=10)),
|
|
|
|
vol.Optional(CONF_REPEAT):
|
|
|
|
vol.All(vol.Coerce(int), vol.Range(min=-1)),
|
2018-05-15 17:58:00 +00:00
|
|
|
}), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE)
|
|
|
|
)
|
|
|
|
|
2018-06-10 11:50:25 +00:00
|
|
|
# pylint: disable=no-value-for-parameter
|
2018-05-15 17:58:00 +00:00
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
DOMAIN: vol.Schema({
|
|
|
|
vol.Required(CONF_ACCESS_TOKEN): cv.string,
|
2018-06-10 11:50:25 +00:00
|
|
|
vol.Optional(CONF_API_HOST): vol.Url(),
|
2018-05-15 17:58:00 +00:00
|
|
|
vol.Required(CONF_DEVICES): [{
|
2018-08-24 21:29:25 +00:00
|
|
|
vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"),
|
2018-05-15 17:58:00 +00:00
|
|
|
vol.Optional(CONF_BINARY_SENSORS): vol.All(
|
|
|
|
cv.ensure_list, [_BINARY_SENSOR_SCHEMA]),
|
|
|
|
vol.Optional(CONF_SWITCHES): vol.All(
|
|
|
|
cv.ensure_list, [_SWITCH_SCHEMA]),
|
2018-10-16 22:39:32 +00:00
|
|
|
vol.Optional(CONF_HOST): cv.string,
|
|
|
|
vol.Optional(CONF_PORT): cv.port,
|
|
|
|
vol.Optional(CONF_BLINK, default=True): cv.boolean,
|
|
|
|
vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
|
2018-05-15 17:58:00 +00:00
|
|
|
}],
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
extra=vol.ALLOW_EXTRA,
|
|
|
|
)
|
|
|
|
|
|
|
|
DEPENDENCIES = ['http', 'discovery']
|
|
|
|
|
|
|
|
ENDPOINT_ROOT = '/api/konnected'
|
|
|
|
UPDATE_ENDPOINT = (ENDPOINT_ROOT + r'/device/{device_id:[a-zA-Z0-9]+}')
|
2018-05-17 18:19:05 +00:00
|
|
|
SIGNAL_SENSOR_UPDATE = 'konnected.{}.update'
|
2018-05-15 17:58:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def async_setup(hass, config):
|
|
|
|
"""Set up the Konnected platform."""
|
2018-10-16 22:39:32 +00:00
|
|
|
import konnected
|
|
|
|
|
2018-05-15 17:58:00 +00:00
|
|
|
cfg = config.get(DOMAIN)
|
|
|
|
if cfg is None:
|
|
|
|
cfg = {}
|
|
|
|
|
|
|
|
access_token = cfg.get(CONF_ACCESS_TOKEN)
|
|
|
|
if DOMAIN not in hass.data:
|
2018-06-10 11:50:25 +00:00
|
|
|
hass.data[DOMAIN] = {
|
|
|
|
CONF_ACCESS_TOKEN: access_token,
|
|
|
|
CONF_API_HOST: cfg.get(CONF_API_HOST)
|
|
|
|
}
|
2018-05-15 17:58:00 +00:00
|
|
|
|
2018-10-16 22:39:32 +00:00
|
|
|
def setup_device(host, port):
|
|
|
|
"""Set up a Konnected device at `host` listening on `port`."""
|
2018-08-24 21:29:25 +00:00
|
|
|
discovered = DiscoveredDevice(hass, host, port)
|
|
|
|
if discovered.is_configured:
|
|
|
|
discovered.setup()
|
|
|
|
else:
|
|
|
|
_LOGGER.warning("Konnected device %s was discovered on the network"
|
|
|
|
" but not specified in configuration.yaml",
|
|
|
|
discovered.device_id)
|
2018-05-15 17:58:00 +00:00
|
|
|
|
2018-10-16 22:39:32 +00:00
|
|
|
def device_discovered(service, info):
|
|
|
|
"""Call when a Konnected device has been discovered."""
|
|
|
|
host = info.get(CONF_HOST)
|
|
|
|
port = info.get(CONF_PORT)
|
|
|
|
setup_device(host, port)
|
|
|
|
|
|
|
|
async def manual_discovery(event):
|
|
|
|
"""Init devices on the network with manually assigned addresses."""
|
|
|
|
specified = [dev for dev in cfg.get(CONF_DEVICES) if
|
|
|
|
dev.get(CONF_HOST) and dev.get(CONF_PORT)]
|
|
|
|
|
|
|
|
while specified:
|
|
|
|
for dev in specified:
|
|
|
|
_LOGGER.debug("Discovering Konnected device %s at %s:%s",
|
|
|
|
dev.get(CONF_ID),
|
|
|
|
dev.get(CONF_HOST),
|
|
|
|
dev.get(CONF_PORT))
|
|
|
|
try:
|
|
|
|
await hass.async_add_executor_job(setup_device,
|
|
|
|
dev.get(CONF_HOST),
|
|
|
|
dev.get(CONF_PORT))
|
|
|
|
specified.remove(dev)
|
|
|
|
except konnected.Client.ClientError as err:
|
|
|
|
_LOGGER.error(err)
|
|
|
|
await asyncio.sleep(10) # try again in 10 seconds
|
|
|
|
|
|
|
|
# Initialize devices specified in the configuration on boot
|
2018-08-24 21:29:25 +00:00
|
|
|
for device in cfg.get(CONF_DEVICES):
|
2018-10-31 06:42:33 +00:00
|
|
|
ConfiguredDevice(hass, device, config).save_data()
|
2018-05-15 17:58:00 +00:00
|
|
|
|
|
|
|
discovery.async_listen(
|
|
|
|
hass,
|
|
|
|
SERVICE_KONNECTED,
|
2018-05-17 18:19:05 +00:00
|
|
|
device_discovered)
|
2018-05-15 17:58:00 +00:00
|
|
|
|
|
|
|
hass.http.register_view(KonnectedView(access_token))
|
2018-10-16 22:39:32 +00:00
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, manual_discovery)
|
2018-05-15 17:58:00 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2018-08-24 21:29:25 +00:00
|
|
|
class ConfiguredDevice:
|
|
|
|
"""A representation of a configured Konnected device."""
|
2018-05-15 17:58:00 +00:00
|
|
|
|
2018-10-31 06:42:33 +00:00
|
|
|
def __init__(self, hass, config, hass_config):
|
2018-05-15 17:58:00 +00:00
|
|
|
"""Initialize the Konnected device."""
|
|
|
|
self.hass = hass
|
2018-08-24 21:29:25 +00:00
|
|
|
self.config = config
|
2018-10-31 06:42:33 +00:00
|
|
|
self.hass_config = hass_config
|
2018-05-15 17:58:00 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def device_id(self):
|
|
|
|
"""Device id is the MAC address as string with punctuation removed."""
|
2018-08-24 21:29:25 +00:00
|
|
|
return self.config.get(CONF_ID)
|
2018-05-15 17:58:00 +00:00
|
|
|
|
|
|
|
def save_data(self):
|
|
|
|
"""Save the device configuration to `hass.data`."""
|
|
|
|
sensors = {}
|
2018-08-24 21:29:25 +00:00
|
|
|
for entity in self.config.get(CONF_BINARY_SENSORS) or []:
|
2018-05-15 17:58:00 +00:00
|
|
|
if CONF_ZONE in entity:
|
|
|
|
pin = ZONE_TO_PIN[entity[CONF_ZONE]]
|
|
|
|
else:
|
|
|
|
pin = entity[CONF_PIN]
|
|
|
|
|
|
|
|
sensors[pin] = {
|
|
|
|
CONF_TYPE: entity[CONF_TYPE],
|
|
|
|
CONF_NAME: entity.get(CONF_NAME, 'Konnected {} Zone {}'.format(
|
|
|
|
self.device_id[6:], PIN_TO_ZONE[pin])),
|
2018-09-12 11:54:38 +00:00
|
|
|
CONF_INVERSE: entity.get(CONF_INVERSE),
|
2018-08-24 21:29:25 +00:00
|
|
|
ATTR_STATE: None
|
2018-05-15 17:58:00 +00:00
|
|
|
}
|
|
|
|
_LOGGER.debug('Set up sensor %s (initial state: %s)',
|
|
|
|
sensors[pin].get('name'),
|
|
|
|
sensors[pin].get(ATTR_STATE))
|
|
|
|
|
2018-08-14 19:15:33 +00:00
|
|
|
actuators = []
|
2018-08-24 21:29:25 +00:00
|
|
|
for entity in self.config.get(CONF_SWITCHES) or []:
|
2018-05-15 17:58:00 +00:00
|
|
|
if 'zone' in entity:
|
|
|
|
pin = ZONE_TO_PIN[entity['zone']]
|
|
|
|
else:
|
|
|
|
pin = entity['pin']
|
|
|
|
|
2018-08-14 19:15:33 +00:00
|
|
|
act = {
|
|
|
|
CONF_PIN: pin,
|
2018-05-15 17:58:00 +00:00
|
|
|
CONF_NAME: entity.get(
|
|
|
|
CONF_NAME, 'Konnected {} Actuator {}'.format(
|
|
|
|
self.device_id[6:], PIN_TO_ZONE[pin])),
|
2018-08-24 21:29:25 +00:00
|
|
|
ATTR_STATE: None,
|
2018-05-15 17:58:00 +00:00
|
|
|
CONF_ACTIVATION: entity[CONF_ACTIVATION],
|
2018-08-14 19:15:33 +00:00
|
|
|
CONF_MOMENTARY: entity.get(CONF_MOMENTARY),
|
|
|
|
CONF_PAUSE: entity.get(CONF_PAUSE),
|
|
|
|
CONF_REPEAT: entity.get(CONF_REPEAT)}
|
|
|
|
actuators.append(act)
|
|
|
|
_LOGGER.debug('Set up actuator %s', act)
|
2018-05-15 17:58:00 +00:00
|
|
|
|
|
|
|
device_data = {
|
|
|
|
CONF_BINARY_SENSORS: sensors,
|
|
|
|
CONF_SWITCHES: actuators,
|
2018-10-16 22:39:32 +00:00
|
|
|
CONF_BLINK: self.config.get(CONF_BLINK),
|
|
|
|
CONF_DISCOVERY: self.config.get(CONF_DISCOVERY)
|
2018-05-15 17:58:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if CONF_DEVICES not in self.hass.data[DOMAIN]:
|
|
|
|
self.hass.data[DOMAIN][CONF_DEVICES] = {}
|
|
|
|
|
2018-08-24 21:29:25 +00:00
|
|
|
_LOGGER.debug('Storing data in hass.data[%s][%s][%s]: %s',
|
|
|
|
DOMAIN, CONF_DEVICES, self.device_id, device_data)
|
2018-05-15 17:58:00 +00:00
|
|
|
self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data
|
|
|
|
|
2018-08-24 21:29:25 +00:00
|
|
|
discovery.load_platform(
|
2018-10-31 06:42:33 +00:00
|
|
|
self.hass, 'binary_sensor', DOMAIN,
|
|
|
|
{'device_id': self.device_id}, self.hass_config)
|
2018-08-24 21:29:25 +00:00
|
|
|
discovery.load_platform(
|
|
|
|
self.hass, 'switch', DOMAIN,
|
2018-10-31 06:42:33 +00:00
|
|
|
{'device_id': self.device_id}, self.hass_config)
|
2018-08-24 21:29:25 +00:00
|
|
|
|
|
|
|
|
|
|
|
class DiscoveredDevice:
|
|
|
|
"""A representation of a discovered Konnected device."""
|
|
|
|
|
|
|
|
def __init__(self, hass, host, port):
|
|
|
|
"""Initialize the Konnected device."""
|
|
|
|
self.hass = hass
|
|
|
|
self.host = host
|
|
|
|
self.port = port
|
|
|
|
|
|
|
|
import konnected
|
|
|
|
self.client = konnected.Client(host, str(port))
|
|
|
|
self.status = self.client.get_status()
|
|
|
|
|
|
|
|
def setup(self):
|
|
|
|
"""Set up a newly discovered Konnected device."""
|
|
|
|
_LOGGER.info('Discovered Konnected device %s. Open http://%s:%s in a '
|
|
|
|
'web browser to view device status.',
|
|
|
|
self.device_id, self.host, self.port)
|
|
|
|
self.save_data()
|
|
|
|
self.update_initial_states()
|
|
|
|
self.sync_device_config()
|
|
|
|
|
|
|
|
def save_data(self):
|
|
|
|
"""Save the discovery information to `hass.data`."""
|
|
|
|
self.stored_configuration['client'] = self.client
|
|
|
|
self.stored_configuration['host'] = self.host
|
|
|
|
self.stored_configuration['port'] = self.port
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_id(self):
|
|
|
|
"""Device id is the MAC address as string with punctuation removed."""
|
|
|
|
return self.status['mac'].replace(':', '')
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_configured(self):
|
|
|
|
"""Return true if device_id is specified in the configuration."""
|
|
|
|
return bool(self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id))
|
|
|
|
|
2018-05-15 17:58:00 +00:00
|
|
|
@property
|
|
|
|
def stored_configuration(self):
|
|
|
|
"""Return the configuration stored in `hass.data` for this device."""
|
2018-08-24 21:29:25 +00:00
|
|
|
return self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id)
|
2018-05-15 17:58:00 +00:00
|
|
|
|
|
|
|
def sensor_configuration(self):
|
|
|
|
"""Return the configuration map for syncing sensors."""
|
|
|
|
return [{'pin': p} for p in
|
2018-05-17 18:19:05 +00:00
|
|
|
self.stored_configuration[CONF_BINARY_SENSORS]]
|
2018-05-15 17:58:00 +00:00
|
|
|
|
|
|
|
def actuator_configuration(self):
|
|
|
|
"""Return the configuration map for syncing actuators."""
|
2018-08-14 19:15:33 +00:00
|
|
|
return [{'pin': data.get(CONF_PIN),
|
2018-05-15 17:58:00 +00:00
|
|
|
'trigger': (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW]
|
|
|
|
else 1)}
|
2018-08-14 19:15:33 +00:00
|
|
|
for data in self.stored_configuration[CONF_SWITCHES]]
|
2018-05-15 17:58:00 +00:00
|
|
|
|
2018-08-24 21:29:25 +00:00
|
|
|
def update_initial_states(self):
|
|
|
|
"""Update the initial state of each sensor from status poll."""
|
2018-09-12 11:54:38 +00:00
|
|
|
for sensor_data in self.status.get('sensors'):
|
|
|
|
sensor_config = self.stored_configuration[CONF_BINARY_SENSORS]. \
|
|
|
|
get(sensor_data.get(CONF_PIN), {})
|
|
|
|
entity_id = sensor_config.get(ATTR_ENTITY_ID)
|
|
|
|
|
|
|
|
state = bool(sensor_data.get(ATTR_STATE))
|
|
|
|
if sensor_config.get(CONF_INVERSE):
|
|
|
|
state = not state
|
2018-08-24 21:29:25 +00:00
|
|
|
|
2018-10-16 22:39:32 +00:00
|
|
|
dispatcher_send(
|
2018-08-24 21:29:25 +00:00
|
|
|
self.hass,
|
|
|
|
SIGNAL_SENSOR_UPDATE.format(entity_id),
|
2018-09-12 11:54:38 +00:00
|
|
|
state)
|
2018-08-24 21:29:25 +00:00
|
|
|
|
2018-05-17 18:19:05 +00:00
|
|
|
def sync_device_config(self):
|
2018-05-15 17:58:00 +00:00
|
|
|
"""Sync the new pin configuration to the Konnected device."""
|
|
|
|
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)
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
2018-06-10 11:50:25 +00:00
|
|
|
desired_api_host = \
|
|
|
|
self.hass.data[DOMAIN].get(CONF_API_HOST) or \
|
|
|
|
self.hass.config.api.base_url
|
|
|
|
desired_api_endpoint = desired_api_host + ENDPOINT_ROOT
|
|
|
|
current_api_endpoint = self.status.get('endpoint')
|
|
|
|
|
|
|
|
_LOGGER.debug('%s: desired api endpoint: %s', self.device_id,
|
|
|
|
desired_api_endpoint)
|
|
|
|
_LOGGER.debug('%s: current api endpoint: %s', self.device_id,
|
|
|
|
current_api_endpoint)
|
|
|
|
|
2018-05-15 17:58:00 +00:00
|
|
|
if (desired_sensor_configuration != current_sensor_configuration) or \
|
2018-06-10 11:50:25 +00:00
|
|
|
(current_actuator_config != desired_actuator_config) or \
|
2018-10-16 22:39:32 +00:00
|
|
|
(current_api_endpoint != desired_api_endpoint) or \
|
|
|
|
(self.status.get(CONF_BLINK) !=
|
|
|
|
self.stored_configuration.get(CONF_BLINK)) or \
|
|
|
|
(self.status.get(CONF_DISCOVERY) !=
|
|
|
|
self.stored_configuration.get(CONF_DISCOVERY)):
|
2018-08-24 21:29:25 +00:00
|
|
|
_LOGGER.info('pushing settings to device %s', self.device_id)
|
2018-05-15 17:58:00 +00:00
|
|
|
self.client.put_settings(
|
|
|
|
desired_sensor_configuration,
|
|
|
|
desired_actuator_config,
|
|
|
|
self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN),
|
2018-10-16 22:39:32 +00:00
|
|
|
desired_api_endpoint,
|
|
|
|
blink=self.stored_configuration.get(CONF_BLINK),
|
|
|
|
discovery=self.stored_configuration.get(CONF_DISCOVERY)
|
2018-05-15 17:58:00 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class KonnectedView(HomeAssistantView):
|
|
|
|
"""View creates an endpoint to receive push updates from the device."""
|
|
|
|
|
|
|
|
url = UPDATE_ENDPOINT
|
|
|
|
extra_urls = [UPDATE_ENDPOINT + '/{pin_num}/{state}']
|
|
|
|
name = 'api:konnected'
|
|
|
|
requires_auth = False # Uses access token from configuration
|
|
|
|
|
|
|
|
def __init__(self, auth_token):
|
|
|
|
"""Initialize the view."""
|
|
|
|
self.auth_token = auth_token
|
|
|
|
|
2018-09-12 11:54:38 +00:00
|
|
|
@staticmethod
|
|
|
|
def binary_value(state, activation):
|
|
|
|
"""Return binary value for GPIO based on state and activation."""
|
|
|
|
if activation == STATE_HIGH:
|
|
|
|
return 1 if state == STATE_ON else 0
|
|
|
|
return 0 if state == STATE_ON else 1
|
|
|
|
|
|
|
|
async def get(self, request: Request, device_id) -> Response:
|
|
|
|
"""Return the current binary state of a switch."""
|
|
|
|
hass = request.app['hass']
|
|
|
|
pin_num = int(request.query.get('pin'))
|
|
|
|
data = hass.data[DOMAIN]
|
|
|
|
|
|
|
|
device = data[CONF_DEVICES][device_id]
|
|
|
|
if not device:
|
|
|
|
return self.json_message(
|
|
|
|
'Device ' + device_id + ' not configured',
|
|
|
|
status_code=HTTP_NOT_FOUND)
|
|
|
|
|
|
|
|
try:
|
|
|
|
pin = next(filter(
|
|
|
|
lambda switch: switch[CONF_PIN] == pin_num,
|
|
|
|
device[CONF_SWITCHES]))
|
|
|
|
except StopIteration:
|
|
|
|
pin = None
|
|
|
|
|
|
|
|
if not pin:
|
|
|
|
return self.json_message(
|
|
|
|
'Switch on pin ' + pin_num + ' not configured',
|
|
|
|
status_code=HTTP_NOT_FOUND)
|
|
|
|
|
|
|
|
return self.json(
|
|
|
|
{'pin': pin_num,
|
|
|
|
'state': self.binary_value(
|
|
|
|
hass.states.get(pin[ATTR_ENTITY_ID]).state,
|
|
|
|
pin[CONF_ACTIVATION])})
|
|
|
|
|
2018-05-15 17:58:00 +00:00
|
|
|
async def put(self, request: Request, device_id,
|
|
|
|
pin_num=None, state=None) -> Response:
|
|
|
|
"""Receive a sensor update via PUT request and async set state."""
|
|
|
|
hass = request.app['hass']
|
|
|
|
data = hass.data[DOMAIN]
|
|
|
|
|
|
|
|
try: # Konnected 2.2.0 and above supports JSON payloads
|
|
|
|
payload = await request.json()
|
|
|
|
pin_num = payload['pin']
|
|
|
|
state = payload['state']
|
|
|
|
except json.decoder.JSONDecodeError:
|
|
|
|
_LOGGER.warning(("Your Konnected device software may be out of "
|
|
|
|
"date. Visit https://help.konnected.io for "
|
|
|
|
"updating instructions."))
|
|
|
|
|
|
|
|
auth = request.headers.get(AUTHORIZATION, None)
|
|
|
|
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)
|
|
|
|
device = data[CONF_DEVICES].get(device_id)
|
|
|
|
if device is None:
|
|
|
|
return self.json_message('unregistered device',
|
|
|
|
status_code=HTTP_BAD_REQUEST)
|
2018-08-14 19:15:33 +00:00
|
|
|
pin_data = device[CONF_BINARY_SENSORS].get(pin_num)
|
2018-05-15 17:58:00 +00:00
|
|
|
|
|
|
|
if pin_data is None:
|
|
|
|
return self.json_message('unregistered sensor/actuator',
|
|
|
|
status_code=HTTP_BAD_REQUEST)
|
2018-05-17 18:19:05 +00:00
|
|
|
|
|
|
|
entity_id = pin_data.get(ATTR_ENTITY_ID)
|
|
|
|
if entity_id is None:
|
2018-05-15 17:58:00 +00:00
|
|
|
return self.json_message('uninitialized sensor/actuator',
|
2018-08-24 21:29:25 +00:00
|
|
|
status_code=HTTP_NOT_FOUND)
|
2018-09-12 11:54:38 +00:00
|
|
|
state = bool(int(state))
|
|
|
|
if pin_data.get(CONF_INVERSE):
|
|
|
|
state = not state
|
2018-05-15 17:58:00 +00:00
|
|
|
|
2018-05-17 18:19:05 +00:00
|
|
|
async_dispatcher_send(
|
|
|
|
hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state)
|
2018-05-15 17:58:00 +00:00
|
|
|
return self.json_message('ok')
|