Skip to content

Commit

Permalink
Decouple Konnected entity setup from discovery (#16146)
Browse files Browse the repository at this point in the history
* decouple entity setup from discovery

* validate that device_id is a full MAC address
  • Loading branch information
heythisisnate authored and balloob committed Aug 24, 2018
1 parent 84365cd commit 647b3ff
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 74 deletions.
151 changes: 83 additions & 68 deletions homeassistant/components/konnected.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
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_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)
Expand Down Expand Up @@ -74,7 +74,7 @@
vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Optional(CONF_API_HOST): vol.Url(),
vol.Required(CONF_DEVICES): [{
vol.Required(CONF_ID): cv.string,
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_SWITCHES): vol.All(
Expand Down Expand Up @@ -107,12 +107,18 @@ async def async_setup(hass, config):

def 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)
port = info.get(CONF_PORT)
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)

device = KonnectedDevice(hass, host, port, cfg)
device.setup()
for device in cfg.get(CONF_DEVICES):
ConfiguredDevice(hass, device).save_data()

discovery.async_listen(
hass,
Expand All @@ -124,98 +130,51 @@ def device_discovered(service, info):
return True


class KonnectedDevice:
"""A representation of a single Konnected device."""
class ConfiguredDevice:
"""A representation of a configured Konnected device."""

def __init__(self, hass, host, port, config):
def __init__(self, hass, 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.debug('Configuring Konnected device %s', self.device_id)
self.save_data()
self.sync_device_config()
discovery.load_platform(
self.hass, 'binary_sensor',
DOMAIN, {'device_id': self.device_id})
discovery.load_platform(
self.hass, 'switch', DOMAIN,
{'device_id': self.device_id})
self.config = config

@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[CONF_DEVICES]
return next((device for device in
configured_devices if device[CONF_ID] in valid_keys),
None)
return self.config.get(CONF_ID)

def save_data(self):
"""Save the device configuration to `hass.data`."""
sensors = {}
for entity in self.config().get(CONF_BINARY_SENSORS) or []:
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]

sensor_status = next((sensor for sensor in
self.status.get('sensors') if
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] = {
CONF_TYPE: entity[CONF_TYPE],
CONF_NAME: entity.get(CONF_NAME, 'Konnected {} Zone {}'.format(
self.device_id[6:], PIN_TO_ZONE[pin])),
ATTR_STATE: initial_state
ATTR_STATE: None
}
_LOGGER.debug('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 []:
for entity in self.config.get(CONF_SWITCHES) 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(ATTR_STATE):
initial_state = bool(int(actuator_status.get(ATTR_STATE)))
else:
initial_state = None

act = {
CONF_PIN: pin,
CONF_NAME: entity.get(
CONF_NAME, 'Konnected {} Actuator {}'.format(
self.device_id[6:], PIN_TO_ZONE[pin])),
ATTR_STATE: initial_state,
ATTR_STATE: None,
CONF_ACTIVATION: entity[CONF_ACTIVATION],
CONF_MOMENTARY: entity.get(CONF_MOMENTARY),
CONF_PAUSE: entity.get(CONF_PAUSE),
Expand All @@ -224,23 +183,67 @@ def save_data(self):
_LOGGER.debug('Set up actuator %s', act)

device_data = {
'client': self.client,
CONF_BINARY_SENSORS: sensors,
CONF_SWITCHES: actuators,
CONF_HOST: self.host,
CONF_PORT: self.port,
}

if CONF_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.debug('Storing data in hass.data[%s][%s][%s]: %s',
DOMAIN, CONF_DEVICES, self.device_id, device_data)
self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data

discovery.load_platform(
self.hass, 'binary_sensor',
DOMAIN, {'device_id': self.device_id})
discovery.load_platform(
self.hass, 'switch', DOMAIN,
{'device_id': self.device_id})


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))

@property
def stored_configuration(self):
"""Return the configuration stored in `hass.data` for this device."""
return self.hass.data[DOMAIN][CONF_DEVICES][self.device_id]
return self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id)

def sensor_configuration(self):
"""Return the configuration map for syncing sensors."""
Expand All @@ -254,6 +257,18 @@ def actuator_configuration(self):
else 1)}
for data in self.stored_configuration[CONF_SWITCHES]]

def update_initial_states(self):
"""Update the initial state of each sensor from status poll."""
for sensor in self.status.get('sensors'):
entity_id = self.stored_configuration[CONF_BINARY_SENSORS]. \
get(sensor.get(CONF_PIN), {}). \
get(ATTR_ENTITY_ID)

async_dispatcher_send(
self.hass,
SIGNAL_SENSOR_UPDATE.format(entity_id),
bool(sensor.get(ATTR_STATE)))

def sync_device_config(self):
"""Sync the new pin configuration to the Konnected device."""
desired_sensor_configuration = self.sensor_configuration()
Expand Down Expand Up @@ -285,7 +300,7 @@ def sync_device_config(self):
if (desired_sensor_configuration != current_sensor_configuration) or \
(current_actuator_config != desired_actuator_config) or \
(current_api_endpoint != desired_api_endpoint):
_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,
Expand Down Expand Up @@ -340,7 +355,7 @@ async def put(self, request: Request, device_id,
entity_id = pin_data.get(ATTR_ENTITY_ID)
if entity_id is None:
return self.json_message('uninitialized sensor/actuator',
status_code=HTTP_INTERNAL_SERVER_ERROR)
status_code=HTTP_NOT_FOUND)

async_dispatcher_send(
hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state)
Expand Down
17 changes: 11 additions & 6 deletions homeassistant/components/switch/konnected.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,16 @@ async def async_setup_platform(hass, config, async_add_entities,

data = hass.data[KONNECTED_DOMAIN]
device_id = discovery_info['device_id']
client = data[CONF_DEVICES][device_id]['client']
switches = [
KonnectedSwitch(device_id, pin_data.get(CONF_PIN), pin_data, client)
KonnectedSwitch(device_id, pin_data.get(CONF_PIN), pin_data)
for pin_data in data[CONF_DEVICES][device_id][CONF_SWITCHES]]
async_add_entities(switches)


class KonnectedSwitch(ToggleEntity):
"""Representation of a Konnected switch."""

def __init__(self, device_id, pin_num, data, client):
def __init__(self, device_id, pin_num, data):
"""Initialize the switch."""
self._data = data
self._device_id = device_id
Expand All @@ -50,7 +49,6 @@ def __init__(self, device_id, pin_num, data, client):
self._name = self._data.get(
'name', 'Konnected {} Actuator {}'.format(
device_id, PIN_TO_ZONE[pin_num]))
self._client = client
_LOGGER.debug('Created new switch: %s', self._name)

@property
Expand All @@ -63,9 +61,16 @@ def is_on(self):
"""Return the status of the sensor."""
return self._state

@property
def client(self):
"""Return the Konnected HTTP client."""
return \
self.hass.data[KONNECTED_DOMAIN][CONF_DEVICES][self._device_id].\
get('client')

def turn_on(self, **kwargs):
"""Send a command to turn on the switch."""
resp = self._client.put_device(
resp = self.client.put_device(
self._pin_num,
int(self._activation == STATE_HIGH),
self._momentary,
Expand All @@ -82,7 +87,7 @@ def turn_on(self, **kwargs):

def turn_off(self, **kwargs):
"""Send a command to turn off the switch."""
resp = self._client.put_device(
resp = self.client.put_device(
self._pin_num, int(self._activation == STATE_LOW))

if resp.get(ATTR_STATE) is not None:
Expand Down

0 comments on commit 647b3ff

Please sign in to comment.