From 5e3a036e7cd6b1377241b6a723335926d84a7b36 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Wed, 22 Aug 2018 15:51:09 -0400 Subject: [PATCH 1/2] decouple entity setup from discovery --- homeassistant/components/konnected.py | 149 ++++++++++--------- homeassistant/components/switch/konnected.py | 17 ++- 2 files changed, 93 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 9e85e85818d09b..a6eb073086354f 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -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) @@ -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, @@ -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), @@ -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.""" @@ -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() @@ -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, @@ -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) diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py index 06d3385f567fa6..e78e04464ada2f 100644 --- a/homeassistant/components/switch/konnected.py +++ b/homeassistant/components/switch/konnected.py @@ -27,9 +27,8 @@ async def async_setup_platform(hass, config, async_add_devices, 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_devices(switches) @@ -37,7 +36,7 @@ async def async_setup_platform(hass, config, async_add_devices, 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 @@ -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 @@ -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, @@ -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: From 3b43bc9ed91dd5b2e8bd0981f0882ff53f5ce6ca Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Thu, 23 Aug 2018 14:25:11 -0400 Subject: [PATCH 2/2] validate that device_id is a full MAC address --- homeassistant/components/konnected.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index a6eb073086354f..3df285863139ab 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -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(