Water heater support (#17058)

* Moved econet to water_heater

* Wink and Econet are now in water heater!

* Removed away mode from econet and added demo water heater

* Added demo tests

* Updated coveragerc

* Fix lint issues.

* updated requirements all

* Requirements all actually updated.

* Reset wink and econet and fixed service.

* Reset wink and econet to the correct dev state

* Reset requirements_all and .coveragerc and removed the new econet and wink water_heater files

* Removed @bind_hass service methods

* Actually reset the .coverage file

* Fixed the tests

* Addressed @MartinHjelmare's comments

* Removed unused import

* Switched to async_add_executor_job

* Fixed lint

* Removed is_on

* Added celsius demo water heater and tests.

* Removed metric import
This commit is contained in:
William Scanlon 2018-10-08 04:38:07 -04:00 committed by Martin Hjelmare
parent ff4204244b
commit f2d8f3bcb8
6 changed files with 572 additions and 0 deletions

View file

@ -0,0 +1,263 @@
"""
Provides functionality to interact with water heater devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/water_heater/
"""
from datetime import timedelta
import logging
import functools as ft
import voluptuous as vol
from homeassistant.helpers.temperature import display_temp as show_temp
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
STATE_ON, STATE_OFF, TEMP_CELSIUS, PRECISION_WHOLE,
PRECISION_TENTHS, TEMP_FAHRENHEIT)
DEFAULT_MIN_TEMP = 110
DEFAULT_MAX_TEMP = 140
DOMAIN = 'water_heater'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SCAN_INTERVAL = timedelta(seconds=60)
SERVICE_SET_AWAY_MODE = 'set_away_mode'
SERVICE_SET_TEMPERATURE = 'set_temperature'
SERVICE_SET_OPERATION_MODE = 'set_operation_mode'
STATE_ECO = 'eco'
STATE_ELECTRIC = 'electric'
STATE_PERFORMANCE = 'performance'
STATE_HIGH_DEMAND = 'high_demand'
STATE_HEAT_PUMP = 'heat_pump'
STATE_GAS = 'gas'
SUPPORT_TARGET_TEMPERATURE = 1
SUPPORT_OPERATION_MODE = 2
SUPPORT_AWAY_MODE = 4
ATTR_MAX_TEMP = 'max_temp'
ATTR_MIN_TEMP = 'min_temp'
ATTR_AWAY_MODE = 'away_mode'
ATTR_OPERATION_MODE = 'operation_mode'
ATTR_OPERATION_LIST = 'operation_list'
CONVERTIBLE_ATTRIBUTE = [
ATTR_TEMPERATURE,
]
_LOGGER = logging.getLogger(__name__)
ON_OFF_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
SET_AWAY_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_AWAY_MODE): cv.boolean,
})
SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All(
{
vol.Required(ATTR_TEMPERATURE, 'temperature'): vol.Coerce(float),
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_OPERATION_MODE): cv.string,
}
))
SET_OPERATION_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_OPERATION_MODE): cv.string,
})
async def async_setup(hass, config):
"""Set up water_heater devices."""
component = hass.data[DOMAIN] = \
EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
await component.async_setup(config)
component.async_register_entity_service(
SERVICE_SET_AWAY_MODE, SET_AWAY_MODE_SCHEMA,
async_service_away_mode
)
component.async_register_entity_service(
SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA,
async_service_temperature_set
)
component.async_register_entity_service(
SERVICE_SET_OPERATION_MODE, SET_OPERATION_MODE_SCHEMA,
'async_set_operation_mode'
)
component.async_register_entity_service(
SERVICE_TURN_OFF, ON_OFF_SERVICE_SCHEMA,
'async_turn_off'
)
component.async_register_entity_service(
SERVICE_TURN_ON, ON_OFF_SERVICE_SCHEMA,
'async_turn_on'
)
return True
async def async_setup_entry(hass, entry):
"""Set up a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
class WaterHeaterDevice(Entity):
"""Representation of a water_heater device."""
@property
def state(self):
"""Return the current state."""
return self.current_operation
@property
def precision(self):
"""Return the precision of the system."""
if self.hass.config.units.temperature_unit == TEMP_CELSIUS:
return PRECISION_TENTHS
return PRECISION_WHOLE
@property
def state_attributes(self):
"""Return the optional state attributes."""
data = {
ATTR_MIN_TEMP: show_temp(
self.hass, self.min_temp, self.temperature_unit,
self.precision),
ATTR_MAX_TEMP: show_temp(
self.hass, self.max_temp, self.temperature_unit,
self.precision),
ATTR_TEMPERATURE: show_temp(
self.hass, self.target_temperature, self.temperature_unit,
self.precision),
}
supported_features = self.supported_features
if supported_features & SUPPORT_OPERATION_MODE:
data[ATTR_OPERATION_MODE] = self.current_operation
if self.operation_list:
data[ATTR_OPERATION_LIST] = self.operation_list
if supported_features & SUPPORT_AWAY_MODE:
is_away = self.is_away_mode_on
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
return data
@property
def temperature_unit(self):
"""Return the unit of measurement used by the platform."""
raise NotImplementedError
@property
def current_operation(self):
"""Return current operation ie. eco, electric, performance, ..."""
return None
@property
def operation_list(self):
"""Return the list of available operation modes."""
return None
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return None
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return None
def set_temperature(self, **kwargs):
"""Set new target temperature."""
raise NotImplementedError()
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
await self.hass.async_add_executor_job(
ft.partial(self.set_temperature, **kwargs))
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
raise NotImplementedError()
async def async_set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
await self.hass.async_add_executor_job(self.set_operation_mode,
operation_mode)
def turn_away_mode_on(self):
"""Turn away mode on."""
raise NotImplementedError()
async def async_turn_away_mode_on(self):
"""Turn away mode on."""
await self.hass.async_add_executor_job(self.turn_away_mode_on)
def turn_away_mode_off(self):
"""Turn away mode off."""
raise NotImplementedError()
async def async_turn_away_mode_off(self):
"""Turn away mode off."""
await self.hass.async_add_executor_job(self.turn_away_mode_off)
@property
def supported_features(self):
"""Return the list of supported features."""
raise NotImplementedError()
@property
def min_temp(self):
"""Return the minimum temperature."""
return convert_temperature(DEFAULT_MIN_TEMP, TEMP_FAHRENHEIT,
self.temperature_unit)
@property
def max_temp(self):
"""Return the maximum temperature."""
return convert_temperature(DEFAULT_MAX_TEMP, TEMP_FAHRENHEIT,
self.temperature_unit)
async def async_service_away_mode(entity, service):
"""Handle away mode service."""
if service.data[ATTR_AWAY_MODE]:
await entity.async_turn_away_mode_on()
else:
await entity.async_turn_away_mode_off()
async def async_service_temperature_set(entity, service):
"""Handle set temperature service."""
hass = entity.hass
kwargs = {}
for value, temp in service.data.items():
if value in CONVERTIBLE_ATTRIBUTE:
kwargs[value] = convert_temperature(
temp,
hass.config.units.temperature_unit,
entity.temperature_unit
)
else:
kwargs[value] = temp
await entity.async_set_temperature(**kwargs)

View file

@ -0,0 +1,110 @@
"""
Demo platform that offers a fake water_heater device.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.water_heater import (
WaterHeaterDevice,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_AWAY_MODE,
SUPPORT_OPERATION_MODE)
from homeassistant.const import TEMP_FAHRENHEIT, ATTR_TEMPERATURE, TEMP_CELSIUS
SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Demo water_heater devices."""
add_entities([
DemoWaterHeater('Demo Water Heater', 119,
TEMP_FAHRENHEIT, False, 'eco'),
DemoWaterHeater('Demo Water Heater Celsius', 45,
TEMP_CELSIUS, True, 'eco')
])
class DemoWaterHeater(WaterHeaterDevice):
"""Representation of a demo water_heater device."""
def __init__(self, name, target_temperature, unit_of_measurement,
away, current_operation):
"""Initialize the water_heater device."""
self._name = name
self._support_flags = SUPPORT_FLAGS_HEATER
if target_temperature is not None:
self._support_flags = \
self._support_flags | SUPPORT_TARGET_TEMPERATURE
if away is not None:
self._support_flags = self._support_flags | SUPPORT_AWAY_MODE
if current_operation is not None:
self._support_flags = self._support_flags | SUPPORT_OPERATION_MODE
self._target_temperature = target_temperature
self._unit_of_measurement = unit_of_measurement
self._away = away
self._current_operation = current_operation
self._operation_list = ['eco', 'electric', 'performance',
'high_demand', 'heat_pump', 'gas',
'off']
@property
def supported_features(self):
"""Return the list of supported features."""
return self._support_flags
@property
def should_poll(self):
"""Return the polling state."""
return False
@property
def name(self):
"""Return the name of the water_heater device."""
return self._name
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self._current_operation
@property
def operation_list(self):
"""Return the list of available operation modes."""
return self._operation_list
@property
def is_away_mode_on(self):
"""Return if away mode is on."""
return self._away
def set_temperature(self, **kwargs):
"""Set new target temperatures."""
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
self.schedule_update_ha_state()
def set_operation_mode(self, operation_mode):
"""Set new operation mode."""
self._current_operation = operation_mode
self.schedule_update_ha_state()
def turn_away_mode_on(self):
"""Turn away mode on."""
self._away = True
self.schedule_update_ha_state()
def turn_away_mode_off(self):
"""Turn away mode off."""
self._away = False
self.schedule_update_ha_state()

View file

@ -0,0 +1,29 @@
# Describes the format for available water_heater services
set_away_mode:
description: Turn away mode on/off for water_heater device.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'water_heater.water_heater'
away_mode:
description: New value of away mode.
example: true
set_temperature:
description: Set target temperature of water_heater device.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'water_heater.water_heater'
temperature:
description: New target temperature for water heater.
example: 25
set_operation_mode:
description: Set operation mode for water_heater device.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'water_heater.water_heater'
operation_mode:
description: New value of operation mode.
example: eco

View file

@ -0,0 +1 @@
"""The tests for water heater component."""

View file

@ -0,0 +1,51 @@
"""Collection of helper methods.
All containing methods are legacy helpers that should not be used by new
components. Instead call the service directly.
"""
from homeassistant.components.water_heater import (
_LOGGER, ATTR_AWAY_MODE,
ATTR_OPERATION_MODE, DOMAIN, SERVICE_SET_AWAY_MODE,
SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE)
from homeassistant.loader import bind_hass
@bind_hass
def set_away_mode(hass, away_mode, entity_id=None):
"""Turn all or specified water_heater devices away mode on."""
data = {
ATTR_AWAY_MODE: away_mode
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data)
@bind_hass
def set_temperature(hass, temperature=None, entity_id=None,
operation_mode=None):
"""Set new target temperature."""
kwargs = {
key: value for key, value in [
(ATTR_TEMPERATURE, temperature),
(ATTR_ENTITY_ID, entity_id),
(ATTR_OPERATION_MODE, operation_mode)
] if value is not None
}
_LOGGER.debug("set_temperature start data=%s", kwargs)
hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs)
@bind_hass
def set_operation_mode(hass, operation_mode, entity_id=None):
"""Set new target operation mode."""
data = {ATTR_OPERATION_MODE: operation_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data)

View file

@ -0,0 +1,118 @@
"""The tests for the demo water_heater component."""
import unittest
from homeassistant.util.unit_system import (
IMPERIAL_SYSTEM
)
from homeassistant.setup import setup_component
from homeassistant.components import water_heater
from tests.common import get_test_home_assistant
from tests.components.water_heater import common
ENTITY_WATER_HEATER = 'water_heater.demo_water_heater'
ENTITY_WATER_HEATER_CELSIUS = 'water_heater.demo_water_heater_celsius'
class TestDemowater_heater(unittest.TestCase):
"""Test the demo water_heater."""
def setUp(self): # pylint: disable=invalid-name
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.hass.config.units = IMPERIAL_SYSTEM
self.assertTrue(setup_component(self.hass, water_heater.DOMAIN, {
'water_heater': {
'platform': 'demo',
}}))
def tearDown(self): # pylint: disable=invalid-name
"""Stop down everything that was started."""
self.hass.stop()
def test_setup_params(self):
"""Test the initial parameters."""
state = self.hass.states.get(ENTITY_WATER_HEATER)
self.assertEqual(119, state.attributes.get('temperature'))
self.assertEqual('off', state.attributes.get('away_mode'))
self.assertEqual("eco", state.attributes.get('operation_mode'))
def test_default_setup_params(self):
"""Test the setup with default parameters."""
state = self.hass.states.get(ENTITY_WATER_HEATER)
self.assertEqual(110, state.attributes.get('min_temp'))
self.assertEqual(140, state.attributes.get('max_temp'))
def test_set_only_target_temp_bad_attr(self):
"""Test setting the target temperature without required attribute."""
state = self.hass.states.get(ENTITY_WATER_HEATER)
self.assertEqual(119, state.attributes.get('temperature'))
common.set_temperature(self.hass, None, ENTITY_WATER_HEATER)
self.hass.block_till_done()
self.assertEqual(119, state.attributes.get('temperature'))
def test_set_only_target_temp(self):
"""Test the setting of the target temperature."""
state = self.hass.states.get(ENTITY_WATER_HEATER)
self.assertEqual(119, state.attributes.get('temperature'))
common.set_temperature(self.hass, 110, ENTITY_WATER_HEATER)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_WATER_HEATER)
self.assertEqual(110, state.attributes.get('temperature'))
def test_set_operation_bad_attr_and_state(self):
"""Test setting operation mode without required attribute.
Also check the state.
"""
state = self.hass.states.get(ENTITY_WATER_HEATER)
self.assertEqual("eco", state.attributes.get('operation_mode'))
self.assertEqual("eco", state.state)
common.set_operation_mode(self.hass, None, ENTITY_WATER_HEATER)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_WATER_HEATER)
self.assertEqual("eco", state.attributes.get('operation_mode'))
self.assertEqual("eco", state.state)
def test_set_operation(self):
"""Test setting of new operation mode."""
state = self.hass.states.get(ENTITY_WATER_HEATER)
self.assertEqual("eco", state.attributes.get('operation_mode'))
self.assertEqual("eco", state.state)
common.set_operation_mode(self.hass, "electric", ENTITY_WATER_HEATER)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_WATER_HEATER)
self.assertEqual("electric", state.attributes.get('operation_mode'))
self.assertEqual("electric", state.state)
def test_set_away_mode_bad_attr(self):
"""Test setting the away mode without required attribute."""
state = self.hass.states.get(ENTITY_WATER_HEATER)
self.assertEqual('off', state.attributes.get('away_mode'))
common.set_away_mode(self.hass, None, ENTITY_WATER_HEATER)
self.hass.block_till_done()
self.assertEqual('off', state.attributes.get('away_mode'))
def test_set_away_mode_on(self):
"""Test setting the away mode on/true."""
common.set_away_mode(self.hass, True, ENTITY_WATER_HEATER)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_WATER_HEATER)
self.assertEqual('on', state.attributes.get('away_mode'))
def test_set_away_mode_off(self):
"""Test setting the away mode off/false."""
common.set_away_mode(self.hass, False, ENTITY_WATER_HEATER_CELSIUS)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_WATER_HEATER_CELSIUS)
self.assertEqual('off', state.attributes.get('away_mode'))
def test_set_only_target_temp_with_convert(self):
"""Test the setting of the target temperature."""
state = self.hass.states.get(ENTITY_WATER_HEATER_CELSIUS)
self.assertEqual(113, state.attributes.get('temperature'))
common.set_temperature(self.hass, 114, ENTITY_WATER_HEATER_CELSIUS)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_WATER_HEATER_CELSIUS)
self.assertEqual(114, state.attributes.get('temperature'))