Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for DHT and DS18B20 sensors via Konnected firmware #21189

Merged
merged 12 commits into from Mar 4, 2019
209 changes: 143 additions & 66 deletions homeassistant/components/konnected/__init__.py
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(
heythisisnate marked this conversation as resolved.
Show resolved Hide resolved
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',
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
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',
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
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:
heythisisnate marked this conversation as resolved.
Show resolved Hide resolved
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')