Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 143 additions & 66 deletions homeassistant/components/konnected/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
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)
HTTP_UNAUTHORIZED, CONF_DEVICES, CONF_BINARY_SENSORS, CONF_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)
from homeassistant.helpers import discovery
from homeassistant.helpers import config_validation as cv

_LOGGER = logging.getLogger(__name__)

REQUIREMENTS = ['konnected==0.1.4']
REQUIREMENTS = ['konnected==0.1.5']

DOMAIN = 'konnected'

Expand All @@ -36,6 +37,8 @@
CONF_INVERSE = 'inverse'
CONF_BLINK = 'blink'
CONF_DISCOVERY = 'discovery'
CONF_DHT_SENSORS = 'dht_sensors'
CONF_DS18B20_SENSORS = 'ds18b20_sensors'

STATE_LOW = 'low'
STATE_HIGH = 'high'
Expand All @@ -53,6 +56,16 @@
}), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE)
)

_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):
vol.All(vol.Lower, vol.In(['dht', 'ds18b20'])),
vol.Optional(CONF_NAME): cv.string,
}), 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),
Expand All @@ -79,6 +92,8 @@
vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"),
vol.Optional(CONF_BINARY_SENSORS): vol.All(
cv.ensure_list, [_BINARY_SENSOR_SCHEMA]),
vol.Optional(CONF_SENSORS): vol.All(
cv.ensure_list, [_SENSOR_SCHEMA]),
vol.Optional(CONF_SWITCHES): vol.All(
cv.ensure_list, [_SWITCH_SCHEMA]),
vol.Optional(CONF_HOST): cv.string,
Expand All @@ -96,6 +111,7 @@
ENDPOINT_ROOT = '/api/konnected'
UPDATE_ENDPOINT = (ENDPOINT_ROOT + r'/device/{device_id:[a-zA-Z0-9]+}')
SIGNAL_SENSOR_UPDATE = 'konnected.{}.update'
SIGNAL_DS18B20_NEW = 'konnected.ds18b20.new'


async def async_setup(hass, config):
Expand Down Expand Up @@ -180,30 +196,30 @@ def device_id(self):

def save_data(self):
"""Save the device configuration to `hass.data`."""
sensors = {}
binary_sensors = {}
for entity in self.config.get(CONF_BINARY_SENSORS) or []:
if CONF_ZONE in entity:
pin = ZONE_TO_PIN[entity[CONF_ZONE]]
else:
pin = entity[CONF_PIN]

sensors[pin] = {
binary_sensors[pin] = {
CONF_TYPE: entity[CONF_TYPE],
CONF_NAME: entity.get(CONF_NAME, 'Konnected {} Zone {}'.format(
self.device_id[6:], PIN_TO_ZONE[pin])),
CONF_INVERSE: entity.get(CONF_INVERSE),
ATTR_STATE: None
}
_LOGGER.debug('Set up sensor %s (initial state: %s)',
sensors[pin].get('name'),
sensors[pin].get(ATTR_STATE))
_LOGGER.debug('Set up binary_sensor %s (initial state: %s)',
binary_sensors[pin].get('name'),
binary_sensors[pin].get(ATTR_STATE))

actuators = []
for entity in self.config.get(CONF_SWITCHES) or []:
if 'zone' in entity:
pin = ZONE_TO_PIN[entity['zone']]
if CONF_ZONE in entity:
pin = ZONE_TO_PIN[entity[CONF_ZONE]]
else:
pin = entity['pin']
pin = entity[CONF_PIN]

act = {
CONF_PIN: pin,
Expand All @@ -216,10 +232,32 @@ def save_data(self):
CONF_PAUSE: entity.get(CONF_PAUSE),
CONF_REPEAT: entity.get(CONF_REPEAT)}
actuators.append(act)
_LOGGER.debug('Set up actuator %s', act)
_LOGGER.debug('Set up switch %s', act)

sensors = []
for entity in self.config.get(CONF_SENSORS) or []:
if CONF_ZONE in entity:
pin = ZONE_TO_PIN[entity[CONF_ZONE]]
else:
pin = entity[CONF_PIN]

sensor = {
CONF_PIN: pin,
CONF_NAME: entity.get(
CONF_NAME, 'Konnected {} Sensor {}'.format(
self.device_id[6:], PIN_TO_ZONE[pin])),
CONF_TYPE: entity[CONF_TYPE],
ATTR_STATE: None
}
sensors.append(sensor)
_LOGGER.debug('Set up %s sensor %s (initial state: %s)',
sensor.get(CONF_TYPE),
sensor.get(CONF_NAME),
sensor.get(ATTR_STATE))

device_data = {
CONF_BINARY_SENSORS: sensors,
CONF_BINARY_SENSORS: binary_sensors,
CONF_SENSORS: sensors,
CONF_SWITCHES: actuators,
CONF_BLINK: self.config.get(CONF_BLINK),
CONF_DISCOVERY: self.config.get(CONF_DISCOVERY)
Expand All @@ -235,6 +273,9 @@ def save_data(self):
discovery.load_platform(
self.hass, 'binary_sensor', DOMAIN,
{'device_id': self.device_id}, self.hass_config)
discovery.load_platform(
Comment thread
heythisisnate marked this conversation as resolved.
Outdated
self.hass, 'sensor', DOMAIN,
{'device_id': self.device_id}, self.hass_config)
discovery.load_platform(
self.hass, 'switch', DOMAIN,
{'device_id': self.device_id}, self.hass_config)
Expand Down Expand Up @@ -283,8 +324,8 @@ def stored_configuration(self):
"""Return the configuration stored in `hass.data` for this device."""
return self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id)

def sensor_configuration(self):
"""Return the configuration map for syncing sensors."""
def binary_sensor_configuration(self):
"""Return the configuration map for syncing binary sensors."""
return [{'pin': p} for p in
self.stored_configuration[CONF_BINARY_SENSORS]]

Expand All @@ -295,6 +336,18 @@ def actuator_configuration(self):
else 1)}
for data in self.stored_configuration[CONF_SWITCHES]]

def dht_sensor_configuration(self):
"""Return the configuration map for syncing DHT sensors."""
return [{'pin': sensor[CONF_PIN]} for sensor in
filter(lambda s: s[CONF_TYPE] == 'dht',
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
self.stored_configuration[CONF_SENSORS])]

def ds18b20_sensor_configuration(self):
"""Return the configuration map for syncing DS18B20 sensors."""
return [{'pin': sensor[CONF_PIN]} for sensor in
filter(lambda s: s[CONF_TYPE] == 'ds18b20',
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
self.stored_configuration[CONF_SENSORS])]

def update_initial_states(self):
"""Update the initial state of each sensor from status poll."""
for sensor_data in self.status.get('sensors'):
Expand All @@ -311,50 +364,49 @@ def update_initial_states(self):
SIGNAL_SENSOR_UPDATE.format(entity_id),
state)

def sync_device_config(self):
"""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)

def desired_settings_payload(self):
"""Return a dict representing the desired device configuration."""
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)

if (desired_sensor_configuration != current_sensor_configuration) or \
(current_actuator_config != desired_actuator_config) or \
(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)):

return {
'sensors': self.binary_sensor_configuration(),
'actuators': self.actuator_configuration(),
'dht_sensors': self.dht_sensor_configuration(),
'ds18b20_sensors': self.ds18b20_sensor_configuration(),
'auth_token': self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN),
'endpoint': desired_api_endpoint,
'blink': self.stored_configuration.get(CONF_BLINK),
'discovery': self.stored_configuration.get(CONF_DISCOVERY)
}

def current_settings_payload(self):
"""Return a dict of configuration currently stored on the device."""
settings = self.status['settings']
if not settings:
settings = {}

return {
'sensors': [
{'pin': s[CONF_PIN]} for s in self.status.get('sensors')],
'actuators': self.status.get('actuators'),
'dht_sensors': self.status.get(CONF_DHT_SENSORS),
'ds18b20_sensors': self.status.get(CONF_DS18B20_SENSORS),
'auth_token': settings.get('token'),
'endpoint': settings.get('apiUrl'),
'blink': settings.get(CONF_BLINK),
'discovery': settings.get(CONF_DISCOVERY)
}

def sync_device_config(self):
"""Sync the new pin configuration to the Konnected device if needed."""
_LOGGER.debug('Device %s settings payload: %s', self.device_id,
self.desired_settings_payload())
if self.desired_settings_payload() != self.current_settings_payload():
_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(CONF_ACCESS_TOKEN),
desired_api_endpoint,
blink=self.stored_configuration.get(CONF_BLINK),
discovery=self.stored_configuration.get(CONF_DISCOVERY)
)
self.client.put_settings(**self.desired_settings_payload())


class KonnectedView(HomeAssistantView):
Expand Down Expand Up @@ -415,7 +467,7 @@ async def put(self, request: Request, device_id,
try: # Konnected 2.2.0 and above supports JSON payloads
payload = await request.json()
pin_num = payload['pin']
state = payload['state']
state = payload.get('state')
except json.decoder.JSONDecodeError:
_LOGGER.warning(("Your Konnected device software may be out of "
"date. Visit https://help.konnected.io for "
Expand All @@ -430,20 +482,45 @@ async def put(self, request: Request, device_id,
if device is None:
return self.json_message('unregistered device',
status_code=HTTP_BAD_REQUEST)
pin_data = device[CONF_BINARY_SENSORS].get(pin_num)
pin_data = device[CONF_BINARY_SENSORS].get(pin_num) or \
next((s for s in device[CONF_SENSORS] if s[CONF_PIN] == pin_num),
None)

if pin_data is None:
return self.json_message('unregistered sensor/actuator',
status_code=HTTP_BAD_REQUEST)

entity_id = pin_data.get(ATTR_ENTITY_ID)
if entity_id is None:
return self.json_message('uninitialized sensor/actuator',
status_code=HTTP_NOT_FOUND)
state = bool(int(state))
if pin_data.get(CONF_INVERSE):
state = not state
if state:
Comment thread
heythisisnate marked this conversation as resolved.
Outdated
entity_id = pin_data.get(ATTR_ENTITY_ID)
state = bool(int(state))
if pin_data.get(CONF_INVERSE):
state = not state

async_dispatcher_send(
hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state)

temp, humi = payload.get('temp'), payload.get('humi')
addr = payload.get('addr')

if addr:
entity_id = pin_data.get(addr)
if entity_id:
async_dispatcher_send(
hass, SIGNAL_SENSOR_UPDATE.format(entity_id), temp)
else:
sensor_data = pin_data
sensor_data['device_id'] = device_id
sensor_data['temperature'] = temp
sensor_data['addr'] = addr
async_dispatcher_send(
hass, SIGNAL_DS18B20_NEW, sensor_data)
if temp:
entity_id = pin_data.get('temperature')
async_dispatcher_send(
hass, SIGNAL_SENSOR_UPDATE.format(entity_id), temp)
if humi:
entity_id = pin_data.get('humidity')
async_dispatcher_send(
hass, SIGNAL_SENSOR_UPDATE.format(entity_id), humi)

async_dispatcher_send(
hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state)
return self.json_message('ok')
Loading