Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use voluptuous for input_slider, input_boolean, input_select #3256

Merged
merged 9 commits into from
Sep 23, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
_setup_component
  • Loading branch information
kellerza committed Sep 20, 2016
commit 3795aec283692f5eb2c79c5746b2cff981ac366d
8 changes: 4 additions & 4 deletions homeassistant/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
if hasattr(component, 'CONFIG_SCHEMA'):
try:
config = component.CONFIG_SCHEMA(config)
except vol.MultipleInvalid as ex:
except vol.Invalid as ex:
log_exception(ex, domain, config)
return None

Expand All @@ -155,8 +155,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
# Validate component specific platform schema
try:
p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.MultipleInvalid as ex:
log_exception(ex, domain, p_config)
except vol.Invalid as ex:
log_exception(ex, domain, config)
return None

# Not all platform components follow same pattern for platforms
Expand All @@ -176,7 +176,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
if hasattr(platform, 'PLATFORM_SCHEMA'):
try:
p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.MultipleInvalid as ex:
except vol.Invalid as ex:
log_exception(ex, '{}.{}'.format(domain, p_name),
p_validated)
return None
Expand Down
11 changes: 10 additions & 1 deletion homeassistant/components/input_boolean.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@

from homeassistant.const import (
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
<<<<<<< HEAD
SERVICE_TOGGLE, STATE_ON)
=======
STATE_ON)
from homeassistant.helpers import dict_items_from_list
>>>>>>> _setup_component
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
Expand Down Expand Up @@ -61,7 +66,11 @@ def setup(hass, config):

entities = []

for object_id, cfg in config[DOMAIN].items():
for object_id, cfg, duplicate in dict_items_from_list(config[DOMAIN]):
if duplicate:
_LOGGER.warning('Duplicate values for %s: %s and %s', object_id,
str(duplicate), str(cfg))
continue
if not cfg:
cfg = {}

Expand Down
39 changes: 26 additions & 13 deletions homeassistant/components/input_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
import voluptuous as vol

from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME
from homeassistant.helpers import dict_items_from_list
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent


DOMAIN = 'input_select'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
_LOGGER = logging.getLogger(__name__)
Expand All @@ -30,13 +32,25 @@
vol.Required(ATTR_OPTION): cv.string,
})


def _cv_input_select(cfg):
"""Voluptuous helper for input_select."""
options = cfg[CONF_OPTIONS]
state = cfg.get(CONF_INITIAL, options[0])
if state not in options:
raise vol.Invalid('initial state "{}" is not part of the options: {}'
.format(state, ','.join(options)))
return cfg


PLATFORM_SCHEMA = vol.Schema({
cv.slug: {
cv.slug: vol.All({
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, [cv.string]),
vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1),
[cv.string]),
vol.Optional(CONF_INITIAL): cv.string,
vol.Optional(CONF_ICON): cv.icon,
}}, required=True)
}, _cv_input_select)}, required=True)


def select_option(hass, entity_id, option):
Expand All @@ -53,21 +67,20 @@ def setup(hass, config):

entities = []

for object_id, cfg in config[DOMAIN].items():
for object_id, cfg, duplicate in dict_items_from_list(config[DOMAIN]):
if duplicate:
_LOGGER.warning('Duplicate values for %s: %s and %s', object_id,
str(duplicate), str(cfg))
continue
name = cfg.get(CONF_NAME)
options = cfg.get(CONF_OPTIONS)
state = cfg.get(CONF_INITIAL)

if state not in options:
state = options[0]
if state is not None:
_LOGGER.warning('Initial state %s is not in the options %s',
state, ','.join(options))

state = cfg.get(CONF_INITIAL, 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 select services."""
target_inputs = component.extract_from_service(call)
Expand Down
38 changes: 24 additions & 14 deletions homeassistant/components/input_slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import dict_items_from_list
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent

Expand All @@ -35,17 +36,30 @@
vol.Required(ATTR_VALUE): vol.Coerce(float),
})


def _cv_input_slider(cfg):
"""Volutuous helper input_slider."""
minimum = cfg.get(CONF_MIN)
maximum = cfg.get(CONF_MAX)
if minimum >= maximum:
raise vol.Invalid('Maximum ({}) is not greater than minimum ({})'
.format(minimum, maximum))
state = cfg.get(CONF_INITIAL, minimum)
state = max(min(state, maximum), minimum)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all other checks we raise if the value is invalid, but here we actually change the value. Let's raise here too

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be a change in current behaviour, but in the end more consistent. Will do the change

cfg[CONF_INITIAL] = state
return cfg

PLATFORM_SCHEMA = vol.Schema({
cv.slug: {
cv.slug: vol.All({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MIN): vol.Coerce(float),
vol.Optional(CONF_MAX): vol.Coerce(float),
vol.Required(CONF_MIN): vol.Coerce(float),
vol.Required(CONF_MAX): vol.Coerce(float),
vol.Optional(CONF_INITIAL): vol.Coerce(float),
vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float),
vol.Range(min=1e-3)),
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string
}}, required=True)
}, _cv_input_slider)}, required=True)


def select_value(hass, entity_id, value):
Expand All @@ -58,11 +72,16 @@ def select_value(hass, entity_id, value):

def setup(hass, config):
"""Set up input slider."""
# pylint: disable=too-many-locals
component = EntityComponent(_LOGGER, DOMAIN, hass)

entities = []

for object_id, cfg in config[DOMAIN].items():
for object_id, cfg, duplicate in dict_items_from_list(config[DOMAIN]):
if duplicate:
_LOGGER.warning('Duplicate values for %s: %s and %s', object_id,
str(duplicate), str(cfg))
continue
name = cfg.get(CONF_NAME)
minimum = cfg.get(CONF_MIN)
maximum = cfg.get(CONF_MAX)
Expand All @@ -71,15 +90,6 @@ def setup(hass, config):
icon = cfg.get(CONF_ICON)
unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT)

if state < minimum:
state = minimum
if state > maximum:
state = maximum
if minimum >= maximum:
_LOGGER.warning('Maximum (%s) is not greater than minimum (%s)'
' for %s', minimum, maximum, object_id)
continue

entities.append(InputSlider(object_id, name, state, minimum, maximum,
step, icon, unit))

Expand Down
18 changes: 15 additions & 3 deletions homeassistant/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""Helper methods for components within Home Assistant."""
import re

from typing import Any, Iterable, Tuple, List, Dict
from typing import Any, Iterable, Tuple, Sequence, Dict

from homeassistant.const import CONF_PLATFORM

# Typing Imports and TypeAlias
# pylint: disable=using-constant-test,unused-import
# pylint: disable=using-constant-test,unused-import,wrong-import-order
if False:
from logging import Logger # NOQA

Expand Down Expand Up @@ -34,7 +34,19 @@ def config_per_platform(config: ConfigType,
yield platform, item


def extract_domain_configs(config: ConfigType, domain: str) -> List[str]:
def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]:
"""Extract keys from config for given domain name."""
pattern = re.compile(r'^{}(| .+)$'.format(domain))
return [key for key in config.keys() if pattern.match(key)]


def dict_items_from_list(list_of_dicts: Sequence[dict]) \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should no longer need this method.

-> Iterable[Tuple[Any, Any]]:
"""Iterate over all dicts' items in a list."""
if not isinstance(list_of_dicts, list):
list_of_dicts = [list_of_dicts]
seen = {}
for a_dict in list_of_dicts:
for key, val in a_dict.items():
yield key, val, seen.get(key)
seen[key] = val
88 changes: 59 additions & 29 deletions tests/components/test_input_boolean.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import unittest

import voluptuous as vol
from tests.common import get_test_home_assistant

from homeassistant.components import input_boolean
from homeassistant.bootstrap import _setup_component
from homeassistant.components.input_boolean import (
DOMAIN, PLATFORM_SCHEMA, is_on, turn_off, turn_on)
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."""
Expand All @@ -24,38 +25,41 @@ def tearDown(self): # pylint: disable=invalid-name

def test_config(self):
"""Test config."""
with self.assertRaises(vol.Invalid):
input_boolean.PLATFORM_SCHEMA(None)
with self.assertRaises(vol.Invalid):
input_boolean.PLATFORM_SCHEMA({})
with self.assertRaises(vol.Invalid):
input_boolean.PLATFORM_SCHEMA({'name with space': None})
invalid_configs = [
None,
{},
{'name with space': None},
]

for cfg in invalid_configs:
with self.assertRaises(vol.Invalid):
PLATFORM_SCHEMA(cfg)
self.assertFalse(
_setup_component(self.hass, DOMAIN, {DOMAIN: cfg}))

def test_methods(self):
"""Test is_on, turn_on, turn_off methods."""
self.assertTrue(input_boolean.setup(self.hass, {
'input_boolean': {
'test_1': None,
}
}))
self.assertTrue(_setup_component(self.hass, DOMAIN, {DOMAIN: {
'test_1': None,
}}))
entity_id = 'input_boolean.test_1'

self.assertFalse(
input_boolean.is_on(self.hass, entity_id))
is_on(self.hass, entity_id))

input_boolean.turn_on(self.hass, entity_id)
turn_on(self.hass, entity_id)

self.hass.block_till_done()

self.assertTrue(
input_boolean.is_on(self.hass, entity_id))
is_on(self.hass, entity_id))

input_boolean.turn_off(self.hass, entity_id)
turn_off(self.hass, entity_id)

self.hass.block_till_done()

self.assertFalse(
input_boolean.is_on(self.hass, entity_id))
is_on(self.hass, entity_id))

input_boolean.toggle(self.hass, entity_id)

Expand All @@ -68,16 +72,14 @@ def test_config_options(self):
"""Test configuration options."""
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.assertTrue(_setup_component(self.hass, DOMAIN, {DOMAIN: {
'test_1': None,
'test_2': {
'name': 'Hello World',
'icon': 'mdi:work',
'initial': True,
},
}))
}}))

self.assertEqual(count_start + 2, len(self.hass.states.entity_ids()))

Expand All @@ -94,4 +96,32 @@ def test_config_options(self):
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))
self.assertEqual('mdi:work', state_2.attributes.get(ATTR_ICON))

def test_multiple_platform_style(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not supported once you're back to be just using CONFIG_SCHEMA

"""Load from multiple sub components."""
count_start = len(self.hass.states.entity_ids())

self.assertTrue(_setup_component(self.hass, DOMAIN, {
DOMAIN: {
'test_1': None,
'test_2': {
'name': 'Hello World',
'icon': 'mdi:work',
'initial': True,
},
},
DOMAIN+" 2": {
'test_3': None,
}
}))

self.assertEqual(count_start + 3, 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')
state_3 = self.hass.states.get('input_boolean.test_3')

self.assertIsNotNone(state_1)
self.assertIsNotNone(state_2)
self.assertIsNotNone(state_3)
Loading