diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py new file mode 100644 index 00000000000..0019bca5a6a --- /dev/null +++ b/homeassistant/components/input_select.py @@ -0,0 +1,142 @@ +""" +homeassistant.components.input_select +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Component to offer a way to select an option from a list. + +For more details about this component, please refer to the documentation +at https://home-assistant.io/components/input_select/ +""" +import logging + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify + +DOMAIN = 'input_select' + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +_LOGGER = logging.getLogger(__name__) + +CONF_NAME = 'name' +CONF_INITIAL = 'initial' +CONF_ICON = 'icon' +CONF_OPTIONS = 'options' + +ATTR_OPTION = 'option' +ATTR_OPTIONS = 'options' + +SERVICE_SELECT_OPTION = 'select_option' + + +def select_option(hass, entity_id, option): + """Set input_boolean to False.""" + hass.services.call(DOMAIN, SERVICE_SELECT_OPTION, { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: option, + }) + + +def setup(hass, config): + """Set up input booleans.""" + if not isinstance(config.get(DOMAIN), dict): + _LOGGER.error('Expected %s config to be a dictionary', DOMAIN) + return False + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + if object_id != slugify(object_id): + _LOGGER.warning("Found invalid key for boolean input: %s. " + "Use %s instead", object_id, slugify(object_id)) + continue + if not cfg: + _LOGGER.warning("No configuration specified for %s", object_id) + continue + + name = cfg.get(CONF_NAME) + options = cfg.get(CONF_OPTIONS) + + if not isinstance(options, list) or len(options) == 0: + _LOGGER.warning('Key %s should be a list of options', CONF_OPTIONS) + continue + + options = [str(val) for val in options] + + state = cfg.get(CONF_INITIAL) + + if state not in options: + state = options[0] + + icon = cfg.get(CONF_ICON) + + entities.append(InputSelect(object_id, name, state, options, icon)) + + if not entities: + return False + + def select_option_service(call): + """Handle a calls to the input boolean services.""" + target_inputs = component.extract_from_service(call) + + for input_select in target_inputs: + input_select.select_option(call.data.get(ATTR_OPTION)) + + hass.services.register(DOMAIN, SERVICE_SELECT_OPTION, + select_option_service) + + component.add_entities(entities) + + return True + + +class InputSelect(Entity): + """Represent a select input within Home Assistant.""" + + # pylint: disable=too-many-arguments + def __init__(self, object_id, name, state, options, icon): + """Initialize a boolean input.""" + self.entity_id = ENTITY_ID_FORMAT.format(object_id) + self._name = name + self._current_option = state + self._options = options + self._icon = icon + + @property + def should_poll(self): + """If entitiy should be polled.""" + return False + + @property + def name(self): + """Name of the boolean input.""" + return self._name + + @property + def icon(self): + """Icon to be used for this entity.""" + return self._icon + + @property + def state(self): + """State of the component.""" + return self._current_option + + @property + def state_attributes(self): + """State attributes.""" + return { + ATTR_OPTIONS: self._options, + } + + def select_option(self, option): + """Select new option.""" + if option not in self._options: + _LOGGER.warning('Invalid option: %s (possible options: %s)', + option, ', '.join(self._options)) + return + self._current_option = option + self.update_ha_state() diff --git a/tests/components/test_input_select.py b/tests/components/test_input_select.py new file mode 100644 index 00000000000..f7aa87d2a0d --- /dev/null +++ b/tests/components/test_input_select.py @@ -0,0 +1,132 @@ +""" +tests.components.test_input_select +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests input_select component. +""" +# pylint: disable=too-many-public-methods,protected-access +import unittest + +from homeassistant.components import input_select +from homeassistant.const import ( + ATTR_ICON, ATTR_FRIENDLY_NAME) + +from tests.common import get_test_home_assistant + + +class TestInputSelect(unittest.TestCase): + """ Test the input select module. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_config(self): + """Test config.""" + self.assertFalse(input_select.setup(self.hass, { + 'input_select': None + })) + + self.assertFalse(input_select.setup(self.hass, { + 'input_select': { + } + })) + + self.assertFalse(input_select.setup(self.hass, { + 'input_select': { + 'name with space': None + } + })) + + self.assertFalse(input_select.setup(self.hass, { + 'input_select': { + 'hello': { + 'options': None + } + } + })) + + self.assertFalse(input_select.setup(self.hass, { + 'input_select': { + 'hello': None + } + })) + + def test_select_option(self): + """ Test select_option methods. """ + self.assertTrue(input_select.setup(self.hass, { + 'input_select': { + 'test_1': { + 'options': [ + 'some option', + 'another option', + ], + }, + } + })) + entity_id = 'input_select.test_1' + + state = self.hass.states.get(entity_id) + self.assertEqual('some option', state.state) + + input_select.select_option(self.hass, entity_id, 'another option') + self.hass.pool.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual('another option', state.state) + + input_select.select_option(self.hass, entity_id, 'non existing option') + self.hass.pool.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual('another option', state.state) + + def test_config_options(self): + count_start = len(self.hass.states.entity_ids()) + + test_2_options = [ + 'Good Option', + 'Better Option', + 'Best Option', + ] + + self.assertTrue(input_select.setup(self.hass, { + 'input_select': { + 'test_1': { + 'options': [ + 1, + 2, + ], + }, + 'test_2': { + 'name': 'Hello World', + 'icon': 'work', + 'options': test_2_options, + 'initial': 'Better Option', + }, + }, + })) + + self.assertEqual(count_start + 2, len(self.hass.states.entity_ids())) + + state_1 = self.hass.states.get('input_select.test_1') + state_2 = self.hass.states.get('input_select.test_2') + + self.assertIsNotNone(state_1) + self.assertIsNotNone(state_2) + + self.assertEqual('1', state_1.state) + self.assertEqual(['1', '2'], + state_1.attributes.get(input_select.ATTR_OPTIONS)) + self.assertNotIn(ATTR_ICON, state_1.attributes) + self.assertNotIn(ATTR_FRIENDLY_NAME, state_1.attributes) + + self.assertEqual('Better Option', state_2.state) + self.assertEqual(test_2_options, + state_2.attributes.get(input_select.ATTR_OPTIONS)) + self.assertEqual('Hello World', + state_2.attributes.get(ATTR_FRIENDLY_NAME)) + self.assertEqual('work', state_2.attributes.get(ATTR_ICON))