From 1d5ffe9ad5d4a0a918cce33d338419962ac21545 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 26 Jan 2019 15:33:11 +0000 Subject: [PATCH] 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 * Update homeassistant/components/sensor/utility_meter.py Co-Authored-By: dgomes * 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 variable --- .../components/utility_meter/__init__.py | 176 +++++++++++++ .../components/utility_meter/const.py | 30 +++ .../components/utility_meter/sensor.py | 243 ++++++++++++++++++ .../components/utility_meter/services.yaml | 25 ++ tests/components/utility_meter/__init__.py | 1 + tests/components/utility_meter/test_init.py | 102 ++++++++ tests/components/utility_meter/test_sensor.py | 136 ++++++++++ 7 files changed, 713 insertions(+) create mode 100644 homeassistant/components/utility_meter/__init__.py create mode 100644 homeassistant/components/utility_meter/const.py create mode 100644 homeassistant/components/utility_meter/sensor.py create mode 100644 homeassistant/components/utility_meter/services.yaml create mode 100644 tests/components/utility_meter/__init__.py create mode 100644 tests/components/utility_meter/test_init.py create mode 100644 tests/components/utility_meter/test_sensor.py diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py new file mode 100644 index 00000000000..8a8e669ba88 --- /dev/null +++ b/homeassistant/components/utility_meter/__init__.py @@ -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() diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py new file mode 100644 index 00000000000..4d2df0372b5 --- /dev/null +++ b/homeassistant/components/utility_meter/const.py @@ -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' diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py new file mode 100644 index 00000000000..cd86f9c0bd0 --- /dev/null +++ b/homeassistant/components/utility_meter/sensor.py @@ -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 diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml new file mode 100644 index 00000000000..7c09117d48f --- /dev/null +++ b/homeassistant/components/utility_meter/services.yaml @@ -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' diff --git a/tests/components/utility_meter/__init__.py b/tests/components/utility_meter/__init__.py new file mode 100644 index 00000000000..bcb65403918 --- /dev/null +++ b/tests/components/utility_meter/__init__.py @@ -0,0 +1 @@ +"""Tests for Utility Meter component.""" diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py new file mode 100644 index 00000000000..51a458506fb --- /dev/null +++ b/tests/components/utility_meter/test_init.py @@ -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' diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py new file mode 100644 index 00000000000..23fc8872570 --- /dev/null +++ b/tests/components/utility_meter/test_sensor.py @@ -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)