""" homeassistant.components.configurator ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A component to allow pieces of code to request configuration from the user. Initiate a request by calling the `request_config` method with a callback. This will return a request id that has to be used for future calls. A callback has to be provided to `request_config` which will be called when the user has submitted configuration information. """ import logging from homeassistant.helpers import generate_entity_id from homeassistant.const import EVENT_TIME_CHANGED DOMAIN = "configurator" DEPENDENCIES = [] ENTITY_ID_FORMAT = DOMAIN + ".{}" SERVICE_CONFIGURE = "configure" STATE_CONFIGURE = "configure" STATE_CONFIGURED = "configured" ATTR_CONFIGURE_ID = "configure_id" ATTR_DESCRIPTION = "description" ATTR_DESCRIPTION_IMAGE = "description_image" ATTR_SUBMIT_CAPTION = "submit_caption" ATTR_FIELDS = "fields" ATTR_ERRORS = "errors" _REQUESTS = {} _INSTANCES = {} _LOGGER = logging.getLogger(__name__) # pylint: disable=too-many-arguments def request_config( hass, name, callback, description=None, description_image=None, submit_caption=None, fields=None): """ Create a new request for config. Will return an ID to be used for sequent calls. """ instance = _get_instance(hass) request_id = instance.request_config( name, callback, description, description_image, submit_caption, fields) _REQUESTS[request_id] = instance return request_id def notify_errors(request_id, error): """ Add errors to a config request. """ try: _REQUESTS[request_id].notify_errors(request_id, error) except KeyError: # If request_id does not exist pass def request_done(request_id): """ Mark a config request as done. """ try: _REQUESTS.pop(request_id).request_done(request_id) except KeyError: # If request_id does not exist pass def setup(hass, config): """ Set up Configurator. """ return True def _get_instance(hass): """ Get an instance per hass object. """ try: return _INSTANCES[hass] except KeyError: _INSTANCES[hass] = Configurator(hass) if DOMAIN not in hass.config.components: hass.config.components.append(DOMAIN) return _INSTANCES[hass] class Configurator(object): """ Class to keep track of current configuration requests. """ def __init__(self, hass): self.hass = hass self._cur_id = 0 self._requests = {} hass.services.register( DOMAIN, SERVICE_CONFIGURE, self.handle_service_call) # pylint: disable=too-many-arguments def request_config( self, name, callback, description, description_image, submit_caption, fields): """ Setup a request for configuration. """ entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass) if fields is None: fields = [] request_id = self._generate_unique_id() self._requests[request_id] = (entity_id, fields, callback) data = { ATTR_CONFIGURE_ID: request_id, ATTR_FIELDS: fields, } data.update({ key: value for key, value in [ (ATTR_DESCRIPTION, description), (ATTR_DESCRIPTION_IMAGE, description_image), (ATTR_SUBMIT_CAPTION, submit_caption), ] if value is not None }) self.hass.states.set(entity_id, STATE_CONFIGURE, data) return request_id def notify_errors(self, request_id, error): """ Update the state with errors. """ if not self._validate_request_id(request_id): return entity_id = self._requests[request_id][0] state = self.hass.states.get(entity_id) new_data = state.attributes new_data[ATTR_ERRORS] = error self.hass.states.set(entity_id, STATE_CONFIGURE, new_data) def request_done(self, request_id): """ Remove the config request. """ if not self._validate_request_id(request_id): return entity_id = self._requests.pop(request_id)[0] # If we remove the state right away, it will not be included with # the result fo the service call (current design limitation). # Instead, we will set it to configured to give as feedback but delete # it shortly after so that it is deleted when the client updates. self.hass.states.set(entity_id, STATE_CONFIGURED) def deferred_remove(event): """ Remove the request state. """ self.hass.states.remove(entity_id) self.hass.bus.listen_once(EVENT_TIME_CHANGED, deferred_remove) def handle_service_call(self, call): """ Handle a configure service call. """ request_id = call.data.get(ATTR_CONFIGURE_ID) if not self._validate_request_id(request_id): return # pylint: disable=unused-variable entity_id, fields, callback = self._requests[request_id] # field validation goes here? callback(call.data.get(ATTR_FIELDS, {})) def _generate_unique_id(self): """ Generates a unique configurator id. """ self._cur_id += 1 return "{}-{}".format(id(self), self._cur_id) def _validate_request_id(self, request_id): """ Validate that the request belongs to this instance. """ return request_id in self._requests