diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index d64b40ed60bf5ec483485bfb20ba07a6b13d20cc..c91bbcb7e578d1fcace7401b0bb7159c3fd2fc1f 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -65,6 +65,16 @@ def period_or_cron(config): return config +def max_28_days(config): + """Check that time period does not include more then 28 days.""" + if config.days >= 28: + raise vol.Invalid( + "Unsupported offset of more then 28 days, please use a cron pattern." + ) + + return config + + METER_CONFIG_SCHEMA = vol.Schema( vol.All( { @@ -72,7 +82,7 @@ METER_CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_METER_TYPE): vol.In(METER_TYPES), vol.Optional(CONF_METER_OFFSET, default=DEFAULT_OFFSET): vol.All( - cv.time_period, cv.positive_timedelta + cv.time_period, cv.positive_timedelta, max_28_days ), vol.Optional(CONF_METER_NET_CONSUMPTION, default=False): cv.boolean, vol.Optional(CONF_TARIFFS, default=[]): vol.All( diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 29ea478f0b3202c0cfef95eb72048da4a8eec4e3..ec553cce58adf610ccf9e3568bf873daaf4d2ee6 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -1,7 +1,6 @@ """Utility meter from sensors providing raw data.""" -from datetime import date, datetime, timedelta -import decimal -from decimal import Decimal, DecimalException +from datetime import datetime +from decimal import Decimal, DecimalException, InvalidOperation import logging from croniter import croniter @@ -29,7 +28,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( async_track_point_in_time, async_track_state_change_event, - async_track_time_change, ) from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.dt as dt_util @@ -59,6 +57,17 @@ from .const import ( YEARLY, ) +PERIOD2CRON = { + QUARTER_HOURLY: "{minute}/15 * * * *", + HOURLY: "{minute} * * * *", + DAILY: "{minute} {hour} * * *", + WEEKLY: "{minute} {hour} * * {day}", + MONTHLY: "{minute} {hour} {day} * *", + BIMONTHLY: "{minute} {hour} {day} */2 *", + QUARTERLY: "{minute} {hour} {day} */3 *", + YEARLY: "{minute} {hour} {day} 1/12 *", +} + _LOGGER = logging.getLogger(__name__) ATTR_SOURCE_ID = "source" @@ -152,8 +161,16 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): self._name = f"{source_entity} meter" self._unit_of_measurement = None self._period = meter_type - self._period_offset = meter_offset - self._cron_pattern = cron_pattern + if meter_type is not None: + # For backwards compatibility reasons we convert the period and offset into a cron pattern + self._cron_pattern = PERIOD2CRON[meter_type].format( + minute=meter_offset.seconds % 3600 // 60, + hour=meter_offset.seconds // 3600, + day=meter_offset.days + 1, + ) + _LOGGER.debug("CRON pattern: %s", self._cron_pattern) + else: + self._cron_pattern = cron_pattern self._sensor_net_consumption = net_consumption self._tariff = tariff self._tariff_entity = tariff_entity @@ -233,39 +250,12 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): async def _async_reset_meter(self, event): """Determine cycle - Helper function for larger than daily cycles.""" - now = dt_util.now().date() if self._cron_pattern is not None: async_track_point_in_time( self.hass, self._async_reset_meter, croniter(self._cron_pattern, dt_util.now()).get_next(datetime), ) - elif ( - self._period == WEEKLY - and now != now - timedelta(days=now.weekday()) + self._period_offset - ): - return - elif ( - self._period == MONTHLY - and now != date(now.year, now.month, 1) + self._period_offset - ): - return - elif ( - self._period == BIMONTHLY - and now - != date(now.year, (((now.month - 1) // 2) * 2 + 1), 1) + self._period_offset - ): - return - elif ( - self._period == QUARTERLY - and now - != date(now.year, (((now.month - 1) // 3) * 3 + 1), 1) + self._period_offset - ): - return - elif ( - self._period == YEARLY and now != date(now.year, 1, 1) + self._period_offset - ): - return await self.async_reset_meter(self._tariff_entity) async def async_reset_meter(self, entity_id): @@ -294,30 +284,6 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): self._async_reset_meter, croniter(self._cron_pattern, dt_util.now()).get_next(datetime), ) - elif self._period == QUARTER_HOURLY: - for quarter in range(4): - async_track_time_change( - self.hass, - self._async_reset_meter, - minute=(quarter * 15) - + self._period_offset.seconds % (15 * 60) // 60, - second=self._period_offset.seconds % 60, - ) - elif self._period == HOURLY: - async_track_time_change( - self.hass, - self._async_reset_meter, - minute=self._period_offset.seconds // 60, - second=self._period_offset.seconds % 60, - ) - elif self._period in [DAILY, WEEKLY, MONTHLY, BIMONTHLY, QUARTERLY, YEARLY]: - async_track_time_change( - self.hass, - self._async_reset_meter, - hour=self._period_offset.seconds // 3600, - minute=self._period_offset.seconds % 3600 // 60, - second=self._period_offset.seconds % 3600 % 60, - ) async_dispatcher_connect(self.hass, SIGNAL_RESET_METER, self.async_reset_meter) @@ -325,7 +291,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): if state: try: self._state = Decimal(state.state) - except decimal.InvalidOperation: + except InvalidOperation: _LOGGER.error( "Could not restore state <%s>. Resetting utility_meter.%s", state.state, diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 91b03f7a1bb31a15a18038c2dbd459d8ce3b16ff..a41ddcfa9fce6f22b4bf63b554502d16b6e8ad11 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -627,7 +627,14 @@ async def test_no_reset_yearly_offset(hass, legacy_patchable_time): """Test yearly reset of meter.""" await _test_self_reset( hass, - gen_config("yearly", timedelta(31)), - "2018-01-30T23:59:00.000000+00:00", + gen_config("yearly", timedelta(27)), + "2018-04-29T23:59:00.000000+00:00", expect_reset=False, ) + + +async def test_bad_offset(hass, legacy_patchable_time): + """Test bad offset of meter.""" + assert not await async_setup_component( + hass, DOMAIN, gen_config("monthly", timedelta(days=31)) + )