Utility meter (#19718)
* initial commit * test service calls * lint * float -> Decimal * extra tests * lint * lint * lint * lint * fix self reset * clean * add services * improve service example description * add optional paused initialization * fix * travis fix * fix YEARLY * add tests for previous bug * address comments and suggestions from @ottowinter * lint * remove debug * add discoverability capabilities * no need for _hass * Update homeassistant/components/sensor/utility_meter.py Co-Authored-By: dgomes <diogogomes@gmail.com> * Update homeassistant/components/sensor/utility_meter.py Co-Authored-By: dgomes <diogogomes@gmail.com> * correct comment * improve error handling * address @MartinHjelmare comments * address @MartinHjelmare comments * one patch is enought * follow @ballob suggestion in https://github.com/home-assistant/architecture/issues/131 * fix tests * review fixes * major refactor * lint * lint * address comments by @MartinHjelmare * rename variablepull/20474/head
parent
ed6e349515
commit
1d5ffe9ad5
|
@ -0,0 +1,176 @@
|
||||||
|
"""
|
||||||
|
Component to track utility consumption over given periods of time.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation
|
||||||
|
at https://www.home-assistant.io/components/utility_meter/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import (ATTR_ENTITY_ID, CONF_NAME)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers import discovery
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
|
from .const import (
|
||||||
|
DOMAIN, SIGNAL_RESET_METER, METER_TYPES, CONF_SOURCE_SENSOR,
|
||||||
|
CONF_METER_TYPE, CONF_METER_OFFSET, CONF_TARIFF_ENTITY, CONF_TARIFF,
|
||||||
|
CONF_TARIFFS, CONF_METER, DATA_UTILITY, SERVICE_RESET,
|
||||||
|
SERVICE_SELECT_TARIFF, SERVICE_SELECT_NEXT_TARIFF,
|
||||||
|
ATTR_TARIFF)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TARIFF_ICON = "mdi:clock-outline"
|
||||||
|
|
||||||
|
ATTR_TARIFFS = 'tariffs'
|
||||||
|
|
||||||
|
SERVICE_METER_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
|
})
|
||||||
|
|
||||||
|
SERVICE_SELECT_TARIFF_SCHEMA = SERVICE_METER_SCHEMA.extend({
|
||||||
|
vol.Required(ATTR_TARIFF): cv.string
|
||||||
|
})
|
||||||
|
|
||||||
|
METER_CONFIG_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(CONF_SOURCE_SENSOR): cv.entity_id,
|
||||||
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_METER_TYPE): vol.In(METER_TYPES),
|
||||||
|
vol.Optional(CONF_METER_OFFSET, default=0): cv.positive_int,
|
||||||
|
vol.Optional(CONF_TARIFFS, default=[]): vol.All(
|
||||||
|
cv.ensure_list, [cv.string]),
|
||||||
|
})
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
cv.slug: METER_CONFIG_SCHEMA,
|
||||||
|
}),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass, config):
|
||||||
|
"""Set up an Utility Meter."""
|
||||||
|
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||||
|
hass.data[DATA_UTILITY] = {}
|
||||||
|
|
||||||
|
for meter, conf in config.get(DOMAIN).items():
|
||||||
|
_LOGGER.debug("Setup %s.%s", DOMAIN, meter)
|
||||||
|
|
||||||
|
hass.data[DATA_UTILITY][meter] = conf
|
||||||
|
|
||||||
|
if not conf[CONF_TARIFFS]:
|
||||||
|
# only one entity is required
|
||||||
|
hass.async_create_task(discovery.async_load_platform(
|
||||||
|
hass, SENSOR_DOMAIN, DOMAIN,
|
||||||
|
[{CONF_METER: meter, CONF_NAME: meter}], config))
|
||||||
|
else:
|
||||||
|
# create tariff selection
|
||||||
|
await component.async_add_entities([
|
||||||
|
TariffSelect(meter, list(conf[CONF_TARIFFS]))
|
||||||
|
])
|
||||||
|
hass.data[DATA_UTILITY][meter][CONF_TARIFF_ENTITY] =\
|
||||||
|
"{}.{}".format(DOMAIN, meter)
|
||||||
|
|
||||||
|
# add one meter for each tariff
|
||||||
|
tariff_confs = []
|
||||||
|
for tariff in conf[CONF_TARIFFS]:
|
||||||
|
tariff_confs.append({
|
||||||
|
CONF_METER: meter,
|
||||||
|
CONF_NAME: "{} {}".format(meter, tariff),
|
||||||
|
CONF_TARIFF: tariff,
|
||||||
|
})
|
||||||
|
hass.async_create_task(discovery.async_load_platform(
|
||||||
|
hass, SENSOR_DOMAIN, DOMAIN, tariff_confs, config))
|
||||||
|
|
||||||
|
component.async_register_entity_service(
|
||||||
|
SERVICE_RESET, SERVICE_METER_SCHEMA,
|
||||||
|
'async_reset_meters'
|
||||||
|
)
|
||||||
|
|
||||||
|
component.async_register_entity_service(
|
||||||
|
SERVICE_SELECT_TARIFF, SERVICE_SELECT_TARIFF_SCHEMA,
|
||||||
|
'async_select_tariff'
|
||||||
|
)
|
||||||
|
|
||||||
|
component.async_register_entity_service(
|
||||||
|
SERVICE_SELECT_NEXT_TARIFF, SERVICE_METER_SCHEMA,
|
||||||
|
'async_next_tariff'
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class TariffSelect(RestoreEntity):
|
||||||
|
"""Representation of a Tariff selector."""
|
||||||
|
|
||||||
|
def __init__(self, name, tariffs):
|
||||||
|
"""Initialize a tariff selector."""
|
||||||
|
self._name = name
|
||||||
|
self._current_tariff = None
|
||||||
|
self._tariffs = tariffs
|
||||||
|
self._icon = TARIFF_ICON
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Run when entity about to be added."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
if self._current_tariff is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
state = await self.async_get_last_state()
|
||||||
|
if not state or state.state not in self._tariffs:
|
||||||
|
self._current_tariff = self._tariffs[0]
|
||||||
|
else:
|
||||||
|
self._current_tariff = state.state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""If entity should be polled."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the select input."""
|
||||||
|
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_tariff
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
return {
|
||||||
|
ATTR_TARIFFS: self._tariffs,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_reset_meters(self):
|
||||||
|
"""Reset all sensors of this meter."""
|
||||||
|
async_dispatcher_send(self.hass, SIGNAL_RESET_METER,
|
||||||
|
self.entity_id)
|
||||||
|
|
||||||
|
async def async_select_tariff(self, tariff):
|
||||||
|
"""Select new option."""
|
||||||
|
if tariff not in self._tariffs:
|
||||||
|
_LOGGER.warning('Invalid tariff: %s (possible tariffs: %s)',
|
||||||
|
tariff, ', '.join(self._tariffs))
|
||||||
|
return
|
||||||
|
self._current_tariff = tariff
|
||||||
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
|
async def async_next_tariff(self):
|
||||||
|
"""Offset current index."""
|
||||||
|
current_index = self._tariffs.index(self._current_tariff)
|
||||||
|
new_index = (current_index + 1) % len(self._tariffs)
|
||||||
|
self._current_tariff = self._tariffs[new_index]
|
||||||
|
await self.async_update_ha_state()
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""Constants for the utility meter component."""
|
||||||
|
DOMAIN = 'utility_meter'
|
||||||
|
|
||||||
|
HOURLY = 'hourly'
|
||||||
|
DAILY = 'daily'
|
||||||
|
WEEKLY = 'weekly'
|
||||||
|
MONTHLY = 'monthly'
|
||||||
|
YEARLY = 'yearly'
|
||||||
|
|
||||||
|
METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY]
|
||||||
|
|
||||||
|
DATA_UTILITY = 'utility_meter_data'
|
||||||
|
|
||||||
|
CONF_METER = 'meter'
|
||||||
|
CONF_SOURCE_SENSOR = 'source'
|
||||||
|
CONF_METER_TYPE = 'cycle'
|
||||||
|
CONF_METER_OFFSET = 'offset'
|
||||||
|
CONF_PAUSED = 'paused'
|
||||||
|
CONF_TARIFFS = 'tariffs'
|
||||||
|
CONF_TARIFF = 'tariff'
|
||||||
|
CONF_TARIFF_ENTITY = 'tariff_entity'
|
||||||
|
|
||||||
|
ATTR_TARIFF = 'tariff'
|
||||||
|
|
||||||
|
SIGNAL_START_PAUSE_METER = 'utility_meter_start_pause'
|
||||||
|
SIGNAL_RESET_METER = 'utility_meter_reset'
|
||||||
|
|
||||||
|
SERVICE_RESET = 'reset'
|
||||||
|
SERVICE_SELECT_TARIFF = 'select_tariff'
|
||||||
|
SERVICE_SELECT_NEXT_TARIFF = 'next_tariff'
|
|
@ -0,0 +1,243 @@
|
||||||
|
"""
|
||||||
|
Utility meter from sensors providing raw data.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.utility_meter/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from decimal import Decimal, DecimalException
|
||||||
|
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_NAME, ATTR_UNIT_OF_MEASUREMENT,
|
||||||
|
EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.event import (
|
||||||
|
async_track_state_change, async_track_time_change)
|
||||||
|
from homeassistant.helpers.dispatcher import (
|
||||||
|
async_dispatcher_connect)
|
||||||
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
from .const import (
|
||||||
|
DATA_UTILITY, SIGNAL_RESET_METER,
|
||||||
|
HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY,
|
||||||
|
CONF_SOURCE_SENSOR, CONF_METER_TYPE, CONF_METER_OFFSET,
|
||||||
|
CONF_TARIFF, CONF_TARIFF_ENTITY, CONF_METER)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ATTR_SOURCE_ID = 'source'
|
||||||
|
ATTR_STATUS = 'status'
|
||||||
|
ATTR_PERIOD = 'meter_period'
|
||||||
|
ATTR_LAST_PERIOD = 'last_period'
|
||||||
|
ATTR_LAST_RESET = 'last_reset'
|
||||||
|
ATTR_TARIFF = 'tariff'
|
||||||
|
|
||||||
|
ICON = 'mdi:counter'
|
||||||
|
|
||||||
|
PRECISION = 3
|
||||||
|
PAUSED = 'paused'
|
||||||
|
COLLECTING = 'collecting'
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
|
discovery_info=None):
|
||||||
|
"""Set up the utility meter sensor."""
|
||||||
|
if discovery_info is None:
|
||||||
|
_LOGGER.error("This platform is only available through discovery")
|
||||||
|
return
|
||||||
|
|
||||||
|
meters = []
|
||||||
|
for conf in discovery_info:
|
||||||
|
meter = conf[CONF_METER]
|
||||||
|
conf_meter_source = hass.data[DATA_UTILITY][meter][CONF_SOURCE_SENSOR]
|
||||||
|
conf_meter_type = hass.data[DATA_UTILITY][meter].get(CONF_METER_TYPE)
|
||||||
|
conf_meter_offset = hass.data[DATA_UTILITY][meter][CONF_METER_OFFSET]
|
||||||
|
conf_meter_tariff_entity = hass.data[DATA_UTILITY][meter].get(
|
||||||
|
CONF_TARIFF_ENTITY)
|
||||||
|
|
||||||
|
meters.append(UtilityMeterSensor(conf_meter_source,
|
||||||
|
conf.get(CONF_NAME),
|
||||||
|
conf_meter_type,
|
||||||
|
conf_meter_offset,
|
||||||
|
conf.get(CONF_TARIFF),
|
||||||
|
conf_meter_tariff_entity))
|
||||||
|
|
||||||
|
async_add_entities(meters)
|
||||||
|
|
||||||
|
|
||||||
|
class UtilityMeterSensor(RestoreEntity):
|
||||||
|
"""Representation of an utility meter sensor."""
|
||||||
|
|
||||||
|
def __init__(self, source_entity, name, meter_type, meter_offset=0,
|
||||||
|
tariff=None, tariff_entity=None):
|
||||||
|
"""Initialize the Utility Meter sensor."""
|
||||||
|
self._sensor_source_id = source_entity
|
||||||
|
self._state = 0
|
||||||
|
self._last_period = 0
|
||||||
|
self._last_reset = dt_util.now()
|
||||||
|
self._collecting = None
|
||||||
|
if name:
|
||||||
|
self._name = name
|
||||||
|
else:
|
||||||
|
self._name = '{} meter'.format(source_entity)
|
||||||
|
self._unit_of_measurement = None
|
||||||
|
self._period = meter_type
|
||||||
|
self._period_offset = meter_offset
|
||||||
|
self._tariff = tariff
|
||||||
|
self._tariff_entity = tariff_entity
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_reading(self, entity, old_state, new_state):
|
||||||
|
"""Handle the sensor state changes."""
|
||||||
|
if any([old_state is None,
|
||||||
|
old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE],
|
||||||
|
new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]]):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._unit_of_measurement is None and\
|
||||||
|
new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is not None:
|
||||||
|
self._unit_of_measurement = new_state.attributes.get(
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
|
||||||
|
try:
|
||||||
|
diff = Decimal(new_state.state) - Decimal(old_state.state)
|
||||||
|
|
||||||
|
if diff < 0:
|
||||||
|
# Source sensor just rolled over for unknow reasons,
|
||||||
|
return
|
||||||
|
self._state += diff
|
||||||
|
|
||||||
|
except ValueError as err:
|
||||||
|
_LOGGER.warning("While processing state changes: %s", err)
|
||||||
|
except DecimalException as err:
|
||||||
|
_LOGGER.warning("Invalid state (%s > %s): %s",
|
||||||
|
old_state.state, new_state.state, err)
|
||||||
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_tariff_change(self, entity, old_state, new_state):
|
||||||
|
"""Handle tariff changes."""
|
||||||
|
if self._tariff == new_state.state:
|
||||||
|
self._collecting = async_track_state_change(
|
||||||
|
self.hass, self._sensor_source_id, self.async_reading)
|
||||||
|
else:
|
||||||
|
self._collecting()
|
||||||
|
self._collecting = None
|
||||||
|
|
||||||
|
_LOGGER.debug("%s - %s - source <%s>", self._name,
|
||||||
|
COLLECTING if self._collecting is not None
|
||||||
|
else PAUSED, self._sensor_source_id)
|
||||||
|
|
||||||
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
|
async def _async_reset_meter(self, event):
|
||||||
|
"""Determine cycle - Helper function for larger then daily cycles."""
|
||||||
|
now = dt_util.now()
|
||||||
|
if self._period == WEEKLY and now.weekday() != self._period_offset:
|
||||||
|
return
|
||||||
|
if self._period == MONTHLY and\
|
||||||
|
now.day != (1 + self._period_offset):
|
||||||
|
return
|
||||||
|
if self._period == YEARLY and\
|
||||||
|
(now.month != (1 + self._period_offset) or now.day != 1):
|
||||||
|
return
|
||||||
|
await self.async_reset_meter(self._tariff_entity)
|
||||||
|
|
||||||
|
async def async_reset_meter(self, entity_id):
|
||||||
|
"""Reset meter."""
|
||||||
|
if self._tariff_entity != entity_id:
|
||||||
|
return
|
||||||
|
_LOGGER.debug("Reset utility meter <%s>", self.entity_id)
|
||||||
|
self._last_reset = dt_util.now()
|
||||||
|
self._last_period = str(self._state)
|
||||||
|
self._state = 0
|
||||||
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Handle entity which will be added."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
if self._period == HOURLY:
|
||||||
|
async_track_time_change(self.hass, self._async_reset_meter,
|
||||||
|
minute=self._period_offset, second=0)
|
||||||
|
elif self._period == DAILY:
|
||||||
|
async_track_time_change(self.hass, self._async_reset_meter,
|
||||||
|
hour=self._period_offset, minute=0,
|
||||||
|
second=0)
|
||||||
|
elif self._period in [WEEKLY, MONTHLY, YEARLY]:
|
||||||
|
async_track_time_change(self.hass, self._async_reset_meter,
|
||||||
|
hour=0, minute=0, second=0)
|
||||||
|
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass, SIGNAL_RESET_METER, self.async_reset_meter)
|
||||||
|
|
||||||
|
state = await self.async_get_last_state()
|
||||||
|
if state:
|
||||||
|
self._state = Decimal(state.state)
|
||||||
|
self._unit_of_measurement = state.attributes.get(
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
self._last_period = state.attributes.get(ATTR_LAST_PERIOD)
|
||||||
|
self._last_reset = state.attributes.get(ATTR_LAST_RESET)
|
||||||
|
await self.async_update_ha_state()
|
||||||
|
if state.attributes.get(ATTR_STATUS) == PAUSED:
|
||||||
|
# Fake cancelation function to init the meter paused
|
||||||
|
self._collecting = lambda: None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_source_tracking(event):
|
||||||
|
"""Wait for source to be ready, then start meter."""
|
||||||
|
if self._tariff_entity is not None:
|
||||||
|
_LOGGER.debug("track %s", self._tariff_entity)
|
||||||
|
async_track_state_change(self.hass, self._tariff_entity,
|
||||||
|
self.async_tariff_change)
|
||||||
|
|
||||||
|
tariff_entity_state = self.hass.states.get(self._tariff_entity)
|
||||||
|
if self._tariff != tariff_entity_state.state:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._collecting = async_track_state_change(
|
||||||
|
self.hass, self._sensor_source_id, self.async_reading)
|
||||||
|
|
||||||
|
self.hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_START, async_source_tracking)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit the value is expressed in."""
|
||||||
|
return self._unit_of_measurement
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes of the sensor."""
|
||||||
|
state_attr = {
|
||||||
|
ATTR_SOURCE_ID: self._sensor_source_id,
|
||||||
|
ATTR_STATUS: PAUSED if self._collecting is None else COLLECTING,
|
||||||
|
ATTR_LAST_PERIOD: self._last_period,
|
||||||
|
ATTR_LAST_RESET: self._last_reset,
|
||||||
|
}
|
||||||
|
if self._period is not None:
|
||||||
|
state_attr[ATTR_PERIOD] = self._period
|
||||||
|
if self._tariff is not None:
|
||||||
|
state_attr[ATTR_TARIFF] = self._tariff
|
||||||
|
return state_attr
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon to use in the frontend, if any."""
|
||||||
|
return ICON
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Describes the format for available switch services
|
||||||
|
|
||||||
|
reset:
|
||||||
|
description: Resets the counter of an utility meter.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name(s) of the utility meter to reset
|
||||||
|
example: 'utility_meter.energy'
|
||||||
|
|
||||||
|
next_tariff:
|
||||||
|
description: Changes the tariff to the next one.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name(s) of entities to reset
|
||||||
|
example: 'utility_meter.energy'
|
||||||
|
|
||||||
|
select_tariff:
|
||||||
|
description: selects the current tariff of an utility meter.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the entity to set the tariff for
|
||||||
|
example: 'utility_meter.energy'
|
||||||
|
tariff:
|
||||||
|
description: Name of the tariff to switch to
|
||||||
|
example: 'offpeak'
|
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for Utility Meter component."""
|
|
@ -0,0 +1,102 @@
|
||||||
|
"""The tests for the utility_meter component."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
EVENT_HOMEASSISTANT_START, ATTR_ENTITY_ID)
|
||||||
|
from homeassistant.components.utility_meter.const import (
|
||||||
|
SERVICE_RESET, SERVICE_SELECT_TARIFF, SERVICE_SELECT_NEXT_TARIFF,
|
||||||
|
ATTR_TARIFF)
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
from homeassistant.components.utility_meter.const import DOMAIN
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_services(hass):
|
||||||
|
"""Test energy sensor reset service."""
|
||||||
|
config = {
|
||||||
|
'utility_meter': {
|
||||||
|
'energy_bill': {
|
||||||
|
'source': 'sensor.energy',
|
||||||
|
'cycle': 'hourly',
|
||||||
|
'tariffs': ['peak', 'offpeak'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, DOMAIN, config)
|
||||||
|
assert await async_setup_component(hass, SENSOR_DOMAIN, config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
entity_id = config[DOMAIN]['energy_bill']['source']
|
||||||
|
hass.states.async_set(entity_id, 1, {"unit_of_measurement": "kWh"})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
now = dt_util.utcnow() + timedelta(seconds=10)
|
||||||
|
with patch('homeassistant.util.dt.utcnow',
|
||||||
|
return_value=now):
|
||||||
|
hass.states.async_set(entity_id, 3, {"unit_of_measurement": "kWh"},
|
||||||
|
force_update=True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('sensor.energy_bill_peak')
|
||||||
|
assert state.state == '2'
|
||||||
|
|
||||||
|
state = hass.states.get('sensor.energy_bill_offpeak')
|
||||||
|
assert state.state == '0'
|
||||||
|
|
||||||
|
# Next tariff
|
||||||
|
data = {ATTR_ENTITY_ID: 'utility_meter.energy_bill'}
|
||||||
|
await hass.services.async_call(DOMAIN,
|
||||||
|
SERVICE_SELECT_NEXT_TARIFF, data)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
now += timedelta(seconds=10)
|
||||||
|
with patch('homeassistant.util.dt.utcnow',
|
||||||
|
return_value=now):
|
||||||
|
hass.states.async_set(entity_id, 4, {"unit_of_measurement": "kWh"},
|
||||||
|
force_update=True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('sensor.energy_bill_peak')
|
||||||
|
assert state.state == '2'
|
||||||
|
|
||||||
|
state = hass.states.get('sensor.energy_bill_offpeak')
|
||||||
|
assert state.state == '1'
|
||||||
|
|
||||||
|
# Change tariff
|
||||||
|
data = {ATTR_ENTITY_ID: 'utility_meter.energy_bill',
|
||||||
|
ATTR_TARIFF: 'peak'}
|
||||||
|
await hass.services.async_call(DOMAIN,
|
||||||
|
SERVICE_SELECT_TARIFF, data)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
now += timedelta(seconds=10)
|
||||||
|
with patch('homeassistant.util.dt.utcnow',
|
||||||
|
return_value=now):
|
||||||
|
hass.states.async_set(entity_id, 5, {"unit_of_measurement": "kWh"},
|
||||||
|
force_update=True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('sensor.energy_bill_peak')
|
||||||
|
assert state.state == '3'
|
||||||
|
|
||||||
|
state = hass.states.get('sensor.energy_bill_offpeak')
|
||||||
|
assert state.state == '1'
|
||||||
|
|
||||||
|
# Reset meters
|
||||||
|
data = {ATTR_ENTITY_ID: 'utility_meter.energy_bill'}
|
||||||
|
await hass.services.async_call(DOMAIN, SERVICE_RESET, data)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('sensor.energy_bill_peak')
|
||||||
|
assert state.state == '0'
|
||||||
|
|
||||||
|
state = hass.states.get('sensor.energy_bill_offpeak')
|
||||||
|
assert state.state == '0'
|
|
@ -0,0 +1,136 @@
|
||||||
|
"""The tests for the utility_meter sensor platform."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import patch
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from tests.common import async_fire_time_changed
|
||||||
|
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
from homeassistant.components.utility_meter.const import DOMAIN
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def alter_time(retval):
|
||||||
|
"""Manage multiple time mocks."""
|
||||||
|
patch1 = patch("homeassistant.util.dt.utcnow", return_value=retval)
|
||||||
|
patch2 = patch("homeassistant.util.dt.now", return_value=retval)
|
||||||
|
|
||||||
|
with patch1, patch2:
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
async def test_state(hass):
|
||||||
|
"""Test utility sensor state."""
|
||||||
|
config = {
|
||||||
|
'utility_meter': {
|
||||||
|
'energy_bill': {
|
||||||
|
'source': 'sensor.energy',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, DOMAIN, config)
|
||||||
|
assert await async_setup_component(hass, SENSOR_DOMAIN, config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
entity_id = config[DOMAIN]['energy_bill']['source']
|
||||||
|
hass.states.async_set(entity_id, 2, {"unit_of_measurement": "kWh"})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
now = dt_util.utcnow() + timedelta(seconds=10)
|
||||||
|
with patch('homeassistant.util.dt.utcnow',
|
||||||
|
return_value=now):
|
||||||
|
hass.states.async_set(entity_id, 3, {"unit_of_measurement": "kWh"},
|
||||||
|
force_update=True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('sensor.energy_bill')
|
||||||
|
assert state is not None
|
||||||
|
|
||||||
|
assert state.state == '1'
|
||||||
|
|
||||||
|
|
||||||
|
async def _test_self_reset(hass, cycle, start_time, expect_reset=True):
|
||||||
|
"""Test energy sensor self reset."""
|
||||||
|
config = {
|
||||||
|
'utility_meter': {
|
||||||
|
'energy_bill': {
|
||||||
|
'source': 'sensor.energy',
|
||||||
|
'cycle': cycle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, DOMAIN, config)
|
||||||
|
assert await async_setup_component(hass, SENSOR_DOMAIN, config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
entity_id = config[DOMAIN]['energy_bill']['source']
|
||||||
|
|
||||||
|
now = dt_util.parse_datetime(start_time)
|
||||||
|
with alter_time(now):
|
||||||
|
async_fire_time_changed(hass, now)
|
||||||
|
hass.states.async_set(entity_id, 1, {"unit_of_measurement": "kWh"})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
now += timedelta(seconds=30)
|
||||||
|
with alter_time(now):
|
||||||
|
async_fire_time_changed(hass, now)
|
||||||
|
hass.states.async_set(entity_id, 3, {"unit_of_measurement": "kWh"},
|
||||||
|
force_update=True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
now += timedelta(seconds=30)
|
||||||
|
with alter_time(now):
|
||||||
|
async_fire_time_changed(hass, now)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
hass.states.async_set(entity_id, 6, {"unit_of_measurement": "kWh"},
|
||||||
|
force_update=True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('sensor.energy_bill')
|
||||||
|
if expect_reset:
|
||||||
|
assert state.attributes.get('last_period') == '2'
|
||||||
|
assert state.state == '3'
|
||||||
|
else:
|
||||||
|
assert state.attributes.get('last_period') == 0
|
||||||
|
assert state.state == '5'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_self_reset_hourly(hass):
|
||||||
|
"""Test hourly reset of meter."""
|
||||||
|
await _test_self_reset(hass, 'hourly', "2017-12-31T23:59:00.000000+00:00")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_self_reset_daily(hass):
|
||||||
|
"""Test daily reset of meter."""
|
||||||
|
await _test_self_reset(hass, 'daily', "2017-12-31T23:59:00.000000+00:00")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_self_reset_weekly(hass):
|
||||||
|
"""Test weekly reset of meter."""
|
||||||
|
await _test_self_reset(hass, 'weekly', "2017-12-31T23:59:00.000000+00:00")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_self_reset_monthly(hass):
|
||||||
|
"""Test monthly reset of meter."""
|
||||||
|
await _test_self_reset(hass, 'monthly', "2017-12-31T23:59:00.000000+00:00")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_self_reset_yearly(hass):
|
||||||
|
"""Test yearly reset of meter."""
|
||||||
|
await _test_self_reset(hass, 'yearly', "2017-12-31T23:59:00.000000+00:00")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_self_no_reset_yearly(hass):
|
||||||
|
"""Test yearly reset of meter does not occur after 1st January."""
|
||||||
|
await _test_self_reset(hass, 'yearly', "2018-01-01T23:59:00.000000+00:00",
|
||||||
|
expect_reset=False)
|
Loading…
Reference in New Issue