diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py new file mode 100644 index 00000000000..27216782ba1 --- /dev/null +++ b/homeassistant/components/input_boolean.py @@ -0,0 +1,124 @@ +""" +Component to keep track of user controlled booleans for within automation. + +For more details about this component, please refer to the documentation +at https://home-assistant.io/components/input_boolean/ +""" +import logging + +from homeassistant.const import ( + STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.util import slugify + +DOMAIN = 'input_boolean' + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +_LOGGER = logging.getLogger(__name__) + +CONF_NAME = "name" +CONF_INITIAL = "initial" +CONF_ICON = "icon" + + +def is_on(hass, entity_id): + """Test if input_boolean is True.""" + return hass.states.is_state(entity_id, STATE_ON) + + +def turn_on(hass, entity_id): + """Set input_boolean to True.""" + hass.services.call(DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}) + + +def turn_off(hass, entity_id): + """Set input_boolean to False.""" + hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) + + +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: + cfg = {} + + name = cfg.get(CONF_NAME) + state = cfg.get(CONF_INITIAL, False) + icon = cfg.get(CONF_ICON) + + entities.append(InputBoolean(object_id, name, state, icon)) + + if not entities: + return False + + component.add_entities(entities) + + def toggle_service(service): + """Handle a calls to the input boolean services.""" + target_inputs = component.extract_from_service(service) + + for input_b in target_inputs: + if service.service == SERVICE_TURN_ON: + input_b.turn_on() + else: + input_b.turn_off() + + hass.services.register(DOMAIN, SERVICE_TURN_OFF, toggle_service) + hass.services.register(DOMAIN, SERVICE_TURN_ON, toggle_service) + + return True + + +class InputBoolean(ToggleEntity): + """Represent a boolean input within Home Assistant.""" + + def __init__(self, object_id, name, state, icon): + """Initialize a boolean input.""" + self.entity_id = ENTITY_ID_FORMAT.format(object_id) + self._name = name + self._state = state + 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 is_on(self): + """True if entity is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the entity on.""" + self._state = True + self.update_ha_state() + + def turn_off(self, **kwargs): + """Turn the entity off.""" + self._state = False + self.update_ha_state() diff --git a/tests/components/test_input_boolean.py b/tests/components/test_input_boolean.py new file mode 100644 index 00000000000..a7366d91fd3 --- /dev/null +++ b/tests/components/test_input_boolean.py @@ -0,0 +1,95 @@ +""" +tests.components.test_input_boolean +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests input_boolean component. +""" +# pylint: disable=too-many-public-methods,protected-access +import unittest + +from homeassistant.components import input_boolean +from homeassistant.const import ( + STATE_ON, STATE_OFF, ATTR_ICON, ATTR_FRIENDLY_NAME) + +from tests.common import get_test_home_assistant + + +class TestInputBoolean(unittest.TestCase): + """ Test the input boolean 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_boolean.setup(self.hass, { + 'input_boolean': { + 'test 1': None, + } + })) + + self.assertFalse(input_boolean.setup(self.hass, { + 'input_boolean': { + } + })) + + def test_methods(self): + """ Test is_on, turn_on, turn_off methods. """ + self.assertTrue(input_boolean.setup(self.hass, { + 'input_boolean': { + 'test_1': None, + } + })) + entity_id = 'input_boolean.test_1' + + self.assertFalse( + input_boolean.is_on(self.hass, entity_id)) + + input_boolean.turn_on(self.hass, entity_id) + + self.hass.pool.block_till_done() + + self.assertTrue( + input_boolean.is_on(self.hass, entity_id)) + + input_boolean.turn_off(self.hass, entity_id) + + self.hass.pool.block_till_done() + + self.assertFalse( + input_boolean.is_on(self.hass, entity_id)) + + def test_config_options(self): + count_start = len(self.hass.states.entity_ids()) + + self.assertTrue(input_boolean.setup(self.hass, { + 'input_boolean': { + 'test_1': None, + 'test_2': { + 'name': 'Hello World', + 'icon': 'work', + 'initial': True, + }, + }, + })) + + self.assertEqual(count_start + 2, len(self.hass.states.entity_ids())) + + state_1 = self.hass.states.get('input_boolean.test_1') + state_2 = self.hass.states.get('input_boolean.test_2') + + self.assertIsNotNone(state_1) + self.assertIsNotNone(state_2) + + self.assertEqual(STATE_OFF, state_1.state) + self.assertNotIn(ATTR_ICON, state_1.attributes) + self.assertNotIn(ATTR_FRIENDLY_NAME, state_1.attributes) + + self.assertEqual(STATE_ON, state_2.state) + self.assertEqual('Hello World', + state_2.attributes.get(ATTR_FRIENDLY_NAME)) + self.assertEqual('work', state_2.attributes.get(ATTR_ICON))