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