Add input_select component
parent
c95c3d9198
commit
96710ad410
|
@ -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()
|
|
@ -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))
|
Loading…
Reference in New Issue