Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions homeassistant/components/input_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
"""
Component to offer a way to enter a value into a text box.

For more details about this component, please refer to the documentation
at https://home-assistant.io/components/input_text/
"""
import asyncio
import logging

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME)
from homeassistant.loader import bind_hass
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import async_get_last_state

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'input_text'
ENTITY_ID_FORMAT = DOMAIN + '.{}'

CONF_INITIAL = 'initial'
CONF_MIN = 'min'
CONF_MAX = 'max'
CONF_DISABLED = 'disabled'

ATTR_VALUE = 'value'
ATTR_MIN = 'min'
ATTR_MAX = 'max'
ATTR_PATTERN = 'pattern'
ATTR_DISABLED = 'disabled'

SERVICE_SELECT_VALUE = 'select_value'

SERVICE_SELECT_VALUE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_VALUE): cv.string,
})


def _cv_input_text(cfg):
"""Configure validation helper for input box (voluptuous)."""
minimum = cfg.get(CONF_MIN)
maximum = cfg.get(CONF_MAX)
if minimum > maximum:
raise vol.Invalid('Max len ({}) is not greater than min len ({})'
.format(minimum, maximum))
state = cfg.get(CONF_INITIAL)
if state is not None and (len(state) < minimum or len(state) > maximum):
raise vol.Invalid('Initial value {} length not in range {}-{}'
.format(state, minimum, maximum))
return cfg


CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: vol.All({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MIN, default=0): vol.Coerce(int),
vol.Optional(CONF_MAX, default=100): vol.Coerce(int),
vol.Optional(CONF_INITIAL, ''): cv.string,
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(ATTR_PATTERN): cv.string,
vol.Optional(CONF_DISABLED, default=False): cv.boolean,
}, _cv_input_text)
})
}, required=True, extra=vol.ALLOW_EXTRA)


@bind_hass
def select_value(hass, entity_id, value):
"""Set input_text to value."""
hass.services.call(DOMAIN, SERVICE_SELECT_VALUE, {
ATTR_ENTITY_ID: entity_id,
ATTR_VALUE: value,
})


@asyncio.coroutine
def async_setup(hass, config):
"""Set up an input text box."""
component = EntityComponent(_LOGGER, DOMAIN, hass)

entities = []

for object_id, cfg in config[DOMAIN].items():
name = cfg.get(CONF_NAME)
minimum = cfg.get(CONF_MIN)
maximum = cfg.get(CONF_MAX)
initial = cfg.get(CONF_INITIAL)
icon = cfg.get(CONF_ICON)
unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT)
pattern = cfg.get(ATTR_PATTERN)
disabled = cfg.get(CONF_DISABLED)

entities.append(InputText(
object_id, name, initial, minimum, maximum, icon, unit,
pattern, disabled))

if not entities:
return False

@asyncio.coroutine
def async_select_value_service(call):
"""Handle a calls to the input box services."""
target_inputs = component.async_extract_from_service(call)

tasks = [input_text.async_select_value(call.data[ATTR_VALUE])
for input_text in target_inputs]
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)

hass.services.async_register(
DOMAIN, SERVICE_SELECT_VALUE, async_select_value_service,
schema=SERVICE_SELECT_VALUE_SCHEMA)

yield from component.async_add_entities(entities)
return True


class InputText(Entity):
"""Represent a text box."""

def __init__(self, object_id, name, initial, minimum, maximum, icon,
unit, pattern, disabled):
"""Initialize a select input."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name
self._current_value = initial
self._minimum = minimum
self._maximum = maximum
self._icon = icon
self._unit = unit
self._pattern = pattern
self._disabled = disabled

@property
def should_poll(self):
"""If entity should be polled."""
return False

@property
def name(self):
"""Return the name of the select input box."""
return self._name

@property
def icon(self):
"""Return the icon to be used for this entity."""
return self._icon

@property
def state(self):
"""Return the state of the component."""
return self._current_value

@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit

@property
def disabled(self):
"""Return the disabled flag."""
return self._disabled

@property
def state_attributes(self):
"""Return the state attributes."""
return {
ATTR_MIN: self._minimum,
ATTR_MAX: self._maximum,
ATTR_PATTERN: self._pattern,
ATTR_DISABLED: self._disabled,
}

@asyncio.coroutine
def async_added_to_hass(self):
"""Run when entity about to be added to hass."""
if self._current_value is not None:
return

state = yield from async_get_last_state(self.hass, self.entity_id)
value = state and state.state

# Check against None because value can be 0
if value is not None and self._minimum <= len(value) <= self._maximum:
self._current_value = value

@asyncio.coroutine
def async_select_value(self, value):
"""Select new value."""
if len(value) < self._minimum or len(value) > self._maximum:
_LOGGER.warning("Invalid value: %s (length range %s - %s)",
value, self._minimum, self._maximum)
return
self._current_value = value
yield from self.async_update_ha_state()
147 changes: 147 additions & 0 deletions tests/components/test_input_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""The tests for the Input text component."""
# pylint: disable=protected-access
import asyncio
import unittest

from homeassistant.core import CoreState, State
from homeassistant.setup import setup_component, async_setup_component
from homeassistant.components.input_text import (DOMAIN, select_value)

from tests.common import get_test_home_assistant, mock_restore_cache


class TestInputText(unittest.TestCase):
"""Test the input slider component."""

# pylint: disable=invalid-name
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()

# pylint: disable=invalid-name
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()

def test_config(self):
"""Test config."""
invalid_configs = [
None,
{},
{'name with space': None},
{'test_1': {
'min': 50,
'max': 50,
}},
]
for cfg in invalid_configs:
self.assertFalse(
setup_component(self.hass, DOMAIN, {DOMAIN: cfg}))

def test_select_value(self):
"""Test select_value method."""
self.assertTrue(setup_component(self.hass, DOMAIN, {DOMAIN: {
'test_1': {
'initial': 'test',
'min': 3,
'max': 10,
},
}}))
entity_id = 'input_text.test_1'

state = self.hass.states.get(entity_id)
self.assertEqual('test', str(state.state))

select_value(self.hass, entity_id, 'testing')
self.hass.block_till_done()

state = self.hass.states.get(entity_id)
self.assertEqual('testing', str(state.state))

select_value(self.hass, entity_id, 'testing too long')
self.hass.block_till_done()

state = self.hass.states.get(entity_id)
self.assertEqual('testing', str(state.state))


@asyncio.coroutine
def test_restore_state(hass):
"""Ensure states are restored on startup."""
mock_restore_cache(hass, (
State('input_text.b1', 'test'),
State('input_text.b2', 'testing too long'),
))

hass.state = CoreState.starting

yield from async_setup_component(hass, DOMAIN, {
DOMAIN: {
'b1': {
'min': 0,
'max': 10,
},
'b2': {
'min': 0,
'max': 10,
},
}})

state = hass.states.get('input_text.b1')
assert state
assert str(state.state) == 'test'

state = hass.states.get('input_text.b2')
assert state
assert str(state.state) == 'unknown'


@asyncio.coroutine
def test_initial_state_overrules_restore_state(hass):
"""Ensure states are restored on startup."""
mock_restore_cache(hass, (
State('input_text.b1', 'testing'),
State('input_text.b2', 'testing too long'),
))

hass.state = CoreState.starting

yield from async_setup_component(hass, DOMAIN, {
DOMAIN: {
'b1': {
'initial': 'test',
'min': 0,
'max': 10,
},
'b2': {
'initial': 'test',
'min': 0,
'max': 10,
},
}})

state = hass.states.get('input_text.b1')
assert state
assert str(state.state) == 'test'

state = hass.states.get('input_text.b2')
assert state
assert str(state.state) == 'test'


@asyncio.coroutine
def test_no_initial_state_and_no_restore_state(hass):
"""Ensure that entity is create without initial and restore feature."""
hass.state = CoreState.starting

yield from async_setup_component(hass, DOMAIN, {
DOMAIN: {
'b1': {
'min': 0,
'max': 100,
},
}})

state = hass.states.get('input_text.b1')
assert state
assert str(state.state) == 'unknown'