From f588fef3b42ab6c487b3d539f28a4c073be3eb30 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Thu, 18 Apr 2019 19:02:01 +0900 Subject: [PATCH] Add minimum/maximum to counter (#22608) * Added minimum/maximum to counter * Added min/max testcases * remove duplicate * cosmetic changes * removed blank lines at eof * added newline at eof * type cv -> vol * more fixes * - fixed min/max warnings - fixed failing tests * Added linewrap * - Added cast to int - Fixed double quotes * - removed None check in __init__ - fixed failing test * copy paste fix * copy paste fix * Added possibility to change counter properties trough service call * fixed copy paste errors * Added '.' to comment * rephrased docstring * Fix tests after rebase * Clean up per previous code review comments * Replace setup service with configure * Update services description * Update tests to use configure instead of setup --- homeassistant/components/counter/__init__.py | 75 ++++++-- .../components/counter/services.yaml | 17 +- tests/components/counter/test_init.py | 165 +++++++++++++++++- 3 files changed, 238 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index ab7ada618fe..53aa21c91c6 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -3,7 +3,9 @@ import logging import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME,\ + CONF_MAXIMUM, CONF_MINIMUM + import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -12,6 +14,8 @@ _LOGGER = logging.getLogger(__name__) ATTR_INITIAL = 'initial' ATTR_STEP = 'step' +ATTR_MINIMUM = 'minimum' +ATTR_MAXIMUM = 'maximum' CONF_INITIAL = 'initial' CONF_RESTORE = 'restore' @@ -26,11 +30,19 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' SERVICE_DECREMENT = 'decrement' SERVICE_INCREMENT = 'increment' SERVICE_RESET = 'reset' +SERVICE_CONFIGURE = 'configure' -SERVICE_SCHEMA = vol.Schema({ +SERVICE_SCHEMA_SIMPLE = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) +SERVICE_SCHEMA_CONFIGURE = vol.Schema({ + ATTR_ENTITY_ID: cv.comp_entity_ids, + vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_STEP): cv.positive_int, +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: cv.schema_with_slug_keys( vol.Any({ @@ -38,6 +50,10 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int, vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MAXIMUM, default=None): + vol.Any(None, vol.Coerce(int)), + vol.Optional(CONF_MINIMUM, default=None): + vol.Any(None, vol.Coerce(int)), vol.Optional(CONF_RESTORE, default=True): cv.boolean, vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, }, None) @@ -60,21 +76,27 @@ async def async_setup(hass, config): restore = cfg.get(CONF_RESTORE) step = cfg.get(CONF_STEP) icon = cfg.get(CONF_ICON) + minimum = cfg.get(CONF_MINIMUM) + maximum = cfg.get(CONF_MAXIMUM) - entities.append(Counter(object_id, name, initial, restore, step, icon)) + entities.append(Counter(object_id, name, initial, minimum, maximum, + restore, step, icon)) if not entities: return False component.async_register_entity_service( - SERVICE_INCREMENT, SERVICE_SCHEMA, + SERVICE_INCREMENT, SERVICE_SCHEMA_SIMPLE, 'async_increment') component.async_register_entity_service( - SERVICE_DECREMENT, SERVICE_SCHEMA, + SERVICE_DECREMENT, SERVICE_SCHEMA_SIMPLE, 'async_decrement') component.async_register_entity_service( - SERVICE_RESET, SERVICE_SCHEMA, + SERVICE_RESET, SERVICE_SCHEMA_SIMPLE, 'async_reset') + component.async_register_entity_service( + SERVICE_CONFIGURE, SERVICE_SCHEMA_CONFIGURE, + 'async_configure') await component.async_add_entities(entities) return True @@ -83,13 +105,16 @@ async def async_setup(hass, config): class Counter(RestoreEntity): """Representation of a counter.""" - def __init__(self, object_id, name, initial, restore, step, icon): + def __init__(self, object_id, name, initial, minimum, maximum, + restore, step, icon): """Initialize a counter.""" self.entity_id = ENTITY_ID_FORMAT.format(object_id) self._name = name self._restore = restore self._step = step self._state = self._initial = initial + self._min = minimum + self._max = maximum self._icon = icon @property @@ -115,10 +140,24 @@ class Counter(RestoreEntity): @property def state_attributes(self): """Return the state attributes.""" - return { + ret = { ATTR_INITIAL: self._initial, ATTR_STEP: self._step, } + if self._min is not None: + ret[CONF_MINIMUM] = self._min + if self._max is not None: + ret[CONF_MAXIMUM] = self._max + return ret + + def compute_next_state(self, state): + """Keep the state within the range of min/max values.""" + if self._min is not None: + state = max(self._min, state) + if self._max is not None: + state = min(self._max, state) + + return state async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" @@ -128,19 +167,31 @@ class Counter(RestoreEntity): if self._restore: state = await self.async_get_last_state() if state is not None: - self._state = int(state.state) + self._state = self.compute_next_state(int(state.state)) async def async_decrement(self): """Decrement the counter.""" - self._state -= self._step + self._state = self.compute_next_state(self._state - self._step) await self.async_update_ha_state() async def async_increment(self): """Increment a counter.""" - self._state += self._step + self._state = self.compute_next_state(self._state + self._step) await self.async_update_ha_state() async def async_reset(self): """Reset a counter.""" - self._state = self._initial + self._state = self.compute_next_state(self._initial) + await self.async_update_ha_state() + + async def async_configure(self, **kwargs): + """Change the counter's settings with a service.""" + if CONF_MINIMUM in kwargs: + self._min = kwargs[CONF_MINIMUM] + if CONF_MAXIMUM in kwargs: + self._max = kwargs[CONF_MAXIMUM] + if CONF_STEP in kwargs: + self._step = kwargs[CONF_STEP] + + self._state = self.compute_next_state(self._state) await self.async_update_ha_state() diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml index ef76f9b9eac..fc3f0ad36cb 100644 --- a/homeassistant/components/counter/services.yaml +++ b/homeassistant/components/counter/services.yaml @@ -17,4 +17,19 @@ reset: fields: entity_id: description: Entity id of the counter to reset. - example: 'counter.count0' \ No newline at end of file + example: 'counter.count0' +configure: + description: Change counter parameters + fields: + entity_id: + description: Entity id of the counter to change. + example: 'counter.count0' + minimum: + description: New minimum value for the counter or None to remove minimum + example: 0 + maximum: + description: New maximum value for the counter or None to remove maximum + example: 100 + step: + description: New value for step + example: 2 diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index 97a39cdeb73..4ed303474d5 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -3,13 +3,13 @@ import asyncio import logging -from homeassistant.core import CoreState, State, Context +from homeassistant.components.counter import (CONF_ICON, CONF_INITIAL, + CONF_NAME, CONF_RESTORE, + CONF_STEP, DOMAIN) +from homeassistant.const import (ATTR_FRIENDLY_NAME, ATTR_ICON) +from homeassistant.core import Context, CoreState, State from homeassistant.setup import async_setup_component -from homeassistant.components.counter import ( - DOMAIN, CONF_INITIAL, CONF_RESTORE, CONF_STEP, CONF_NAME, CONF_ICON) -from homeassistant.const import (ATTR_ICON, ATTR_FRIENDLY_NAME) - -from tests.common import mock_restore_cache +from tests.common import (mock_restore_cache) from tests.components.counter.common import ( async_decrement, async_increment, async_reset) @@ -243,3 +243,156 @@ async def test_counter_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +async def test_counter_min(hass, hass_admin_user): + """Test that min works.""" + assert await async_setup_component(hass, 'counter', { + 'counter': { + 'test': { + 'minimum': '0', + 'initial': '0' + } + } + }) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '0' + + await hass.services.async_call('counter', 'decrement', { + 'entity_id': state.entity_id, + }, True, Context(user_id=hass_admin_user.id)) + + state2 = hass.states.get('counter.test') + assert state2 is not None + assert state2.state == '0' + + await hass.services.async_call('counter', 'increment', { + 'entity_id': state.entity_id, + }, True, Context(user_id=hass_admin_user.id)) + + state2 = hass.states.get('counter.test') + assert state2 is not None + assert state2.state == '1' + + +async def test_counter_max(hass, hass_admin_user): + """Test that max works.""" + assert await async_setup_component(hass, 'counter', { + 'counter': { + 'test': { + 'maximum': '0', + 'initial': '0' + } + } + }) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '0' + + await hass.services.async_call('counter', 'increment', { + 'entity_id': state.entity_id, + }, True, Context(user_id=hass_admin_user.id)) + + state2 = hass.states.get('counter.test') + assert state2 is not None + assert state2.state == '0' + + await hass.services.async_call('counter', 'decrement', { + 'entity_id': state.entity_id, + }, True, Context(user_id=hass_admin_user.id)) + + state2 = hass.states.get('counter.test') + assert state2 is not None + assert state2.state == '-1' + + +async def test_configure(hass, hass_admin_user): + """Test that setting values through configure works.""" + assert await async_setup_component(hass, 'counter', { + 'counter': { + 'test': { + 'maximum': '10', + 'initial': '10' + } + } + }) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '10' + assert 10 == state.attributes.get('maximum') + + # update max + await hass.services.async_call('counter', 'configure', { + 'entity_id': state.entity_id, + 'maximum': 0, + }, True, Context(user_id=hass_admin_user.id)) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '0' + assert 0 == state.attributes.get('maximum') + + # disable max + await hass.services.async_call('counter', 'configure', { + 'entity_id': state.entity_id, + 'maximum': None, + }, True, Context(user_id=hass_admin_user.id)) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '0' + assert state.attributes.get('maximum') is None + + # update min + assert state.attributes.get('minimum') is None + await hass.services.async_call('counter', 'configure', { + 'entity_id': state.entity_id, + 'minimum': 5, + }, True, Context(user_id=hass_admin_user.id)) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '5' + assert 5 == state.attributes.get('minimum') + + # disable min + await hass.services.async_call('counter', 'configure', { + 'entity_id': state.entity_id, + 'minimum': None, + }, True, Context(user_id=hass_admin_user.id)) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '5' + assert state.attributes.get('minimum') is None + + # update step + assert 1 == state.attributes.get('step') + await hass.services.async_call('counter', 'configure', { + 'entity_id': state.entity_id, + 'step': 3, + }, True, Context(user_id=hass_admin_user.id)) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '5' + assert 3 == state.attributes.get('step') + + # update all + await hass.services.async_call('counter', 'configure', { + 'entity_id': state.entity_id, + 'step': 5, + 'minimum': 0, + 'maximum': 9, + }, True, Context(user_id=hass_admin_user.id)) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '5' + assert 5 == state.attributes.get('step') + assert 0 == state.attributes.get('minimum') + assert 9 == state.attributes.get('maximum')