Simplify utility_meter code base with croniter (#55625)

This commit is contained in:
Diogo Gomes 2021-10-27 12:41:44 +01:00 committed by GitHub
parent 5581f58aad
commit ad48d78315
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 44 additions and 61 deletions

View File

@ -65,6 +65,16 @@ def period_or_cron(config):
return 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( METER_CONFIG_SCHEMA = vol.Schema(
vol.All( vol.All(
{ {
@ -72,7 +82,7 @@ METER_CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_METER_TYPE): vol.In(METER_TYPES), vol.Optional(CONF_METER_TYPE): vol.In(METER_TYPES),
vol.Optional(CONF_METER_OFFSET, default=DEFAULT_OFFSET): vol.All( 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_METER_NET_CONSUMPTION, default=False): cv.boolean,
vol.Optional(CONF_TARIFFS, default=[]): vol.All( vol.Optional(CONF_TARIFFS, default=[]): vol.All(

View File

@ -1,7 +1,6 @@
"""Utility meter from sensors providing raw data.""" """Utility meter from sensors providing raw data."""
from datetime import date, datetime, timedelta from datetime import datetime
import decimal from decimal import Decimal, DecimalException, InvalidOperation
from decimal import Decimal, DecimalException
import logging import logging
from croniter import croniter from croniter import croniter
@ -29,7 +28,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_point_in_time, async_track_point_in_time,
async_track_state_change_event, async_track_state_change_event,
async_track_time_change,
) )
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -59,6 +57,17 @@ from .const import (
YEARLY, 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__) _LOGGER = logging.getLogger(__name__)
ATTR_SOURCE_ID = "source" ATTR_SOURCE_ID = "source"
@ -152,7 +161,15 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity):
self._name = f"{source_entity} meter" self._name = f"{source_entity} meter"
self._unit_of_measurement = None self._unit_of_measurement = None
self._period = meter_type self._period = meter_type
self._period_offset = meter_offset 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._cron_pattern = cron_pattern
self._sensor_net_consumption = net_consumption self._sensor_net_consumption = net_consumption
self._tariff = tariff self._tariff = tariff
@ -233,39 +250,12 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity):
async def _async_reset_meter(self, event): async def _async_reset_meter(self, event):
"""Determine cycle - Helper function for larger than daily cycles.""" """Determine cycle - Helper function for larger than daily cycles."""
now = dt_util.now().date()
if self._cron_pattern is not None: if self._cron_pattern is not None:
async_track_point_in_time( async_track_point_in_time(
self.hass, self.hass,
self._async_reset_meter, self._async_reset_meter,
croniter(self._cron_pattern, dt_util.now()).get_next(datetime), 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) await self.async_reset_meter(self._tariff_entity)
async def async_reset_meter(self, entity_id): async def async_reset_meter(self, entity_id):
@ -294,30 +284,6 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity):
self._async_reset_meter, self._async_reset_meter,
croniter(self._cron_pattern, dt_util.now()).get_next(datetime), 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) async_dispatcher_connect(self.hass, SIGNAL_RESET_METER, self.async_reset_meter)
@ -325,7 +291,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity):
if state: if state:
try: try:
self._state = Decimal(state.state) self._state = Decimal(state.state)
except decimal.InvalidOperation: except InvalidOperation:
_LOGGER.error( _LOGGER.error(
"Could not restore state <%s>. Resetting utility_meter.%s", "Could not restore state <%s>. Resetting utility_meter.%s",
state.state, state.state,

View File

@ -627,7 +627,14 @@ async def test_no_reset_yearly_offset(hass, legacy_patchable_time):
"""Test yearly reset of meter.""" """Test yearly reset of meter."""
await _test_self_reset( await _test_self_reset(
hass, hass,
gen_config("yearly", timedelta(31)), gen_config("yearly", timedelta(27)),
"2018-01-30T23:59:00.000000+00:00", "2018-04-29T23:59:00.000000+00:00",
expect_reset=False, 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))
)