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
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(

View file

@ -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,

View file

@ -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))
)