mirror of
https://github.com/home-assistant/core.git
synced 2026-06-29 18:56:05 +01:00
Populate hourly statistics in srp_energy (#167371)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> Signed-off-by: Branden Cash <203336+ammmze@users.noreply.github.com>
This commit is contained in:
Generated
+2
-2
@@ -1712,8 +1712,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/sql/ @gjohansson-ST @dougiteixeira
|
||||
/homeassistant/components/squeezebox/ @rajlaud @pssc @peteS-UK
|
||||
/tests/components/squeezebox/ @rajlaud @pssc @peteS-UK
|
||||
/homeassistant/components/srp_energy/ @briglx
|
||||
/tests/components/srp_energy/ @briglx
|
||||
/homeassistant/components/srp_energy/ @briglx @ammmze
|
||||
/tests/components/srp_energy/ @briglx @ammmze
|
||||
/homeassistant/components/starline/ @anonym-tsk
|
||||
/tests/components/starline/ @anonym-tsk
|
||||
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
|
||||
|
||||
@@ -11,7 +11,7 @@ DEFAULT_NAME = "Home"
|
||||
CONF_IS_TOU = "is_tou"
|
||||
|
||||
PHOENIX_TIME_ZONE = "America/Phoenix"
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1440)
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(hours=4)
|
||||
|
||||
DEVICE_CONFIG_URL = "https://www.srpnet.com/"
|
||||
DEVICE_MANUFACTURER = "srpnet.com"
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
"""DataUpdateCoordinator for the srp_energy integration."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import override
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, override
|
||||
|
||||
from srpenergy.client import SrpEnergyClient
|
||||
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.components.recorder.models import (
|
||||
StatisticData,
|
||||
StatisticMeanType,
|
||||
StatisticMetaData,
|
||||
)
|
||||
from homeassistant.components.recorder.statistics import (
|
||||
async_add_external_statistics,
|
||||
get_last_statistics,
|
||||
statistics_during_period,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
from homeassistant.util.unit_conversion import EnergyConverter
|
||||
|
||||
from .const import (
|
||||
CONF_IS_TOU,
|
||||
@@ -24,6 +38,49 @@ PHOENIX_ZONE_INFO = dt_util.get_time_zone(PHOENIX_TIME_ZONE)
|
||||
|
||||
type SRPEnergyConfigEntry = ConfigEntry[SRPEnergyDataUpdateCoordinator]
|
||||
|
||||
# SRP finalizes yesterday's hourly data by approximately this hour (Phoenix time).
|
||||
# Polling after this point until the next midnight is unnecessary.
|
||||
DATA_COMPLETE_HOUR = 6
|
||||
|
||||
type HourlyUsageTuple = tuple[
|
||||
str, str, str, float, float
|
||||
] # (date, time, iso_timestamp, kwh, cost)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class Usage:
|
||||
"""Hourly energy usage data."""
|
||||
|
||||
start_time: datetime
|
||||
end_time: datetime
|
||||
kwh: float = 0.0
|
||||
cost: float = 0.0
|
||||
|
||||
@staticmethod
|
||||
def from_tuple(usage: HourlyUsageTuple) -> Usage | None:
|
||||
"""Initialize Usage from a raw API tuple, or None if unparsable."""
|
||||
if not usage or len(usage) != 5:
|
||||
return None
|
||||
parsed = dt_util.parse_datetime(usage[2])
|
||||
if parsed is None:
|
||||
return None
|
||||
try:
|
||||
kwh = float(usage[3])
|
||||
cost = float(usage[4])
|
||||
except TypeError, ValueError:
|
||||
return None
|
||||
start_time = (
|
||||
parsed.replace(tzinfo=PHOENIX_ZONE_INFO)
|
||||
if parsed.tzinfo is None
|
||||
else parsed.astimezone(PHOENIX_ZONE_INFO)
|
||||
)
|
||||
return Usage(
|
||||
start_time=start_time,
|
||||
end_time=start_time + timedelta(hours=1),
|
||||
kwh=kwh,
|
||||
cost=cost,
|
||||
)
|
||||
|
||||
|
||||
class SRPEnergyDataUpdateCoordinator(DataUpdateCoordinator[float]):
|
||||
"""A srp_energy Data Update Coordinator."""
|
||||
@@ -39,6 +96,7 @@ class SRPEnergyDataUpdateCoordinator(DataUpdateCoordinator[float]):
|
||||
"""Initialize the srp_energy data coordinator."""
|
||||
self._client = client
|
||||
self._is_time_of_use = config_entry.data[CONF_IS_TOU]
|
||||
self._data_complete_until: datetime | None = None
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
@@ -55,19 +113,26 @@ class SRPEnergyDataUpdateCoordinator(DataUpdateCoordinator[float]):
|
||||
so entities can quickly look up their data.
|
||||
"""
|
||||
LOGGER.debug("async_update_data enter")
|
||||
# Fetch srp_energy data
|
||||
end_date = dt_util.now(PHOENIX_ZONE_INFO)
|
||||
start_date = end_date - timedelta(days=1)
|
||||
try:
|
||||
async with asyncio.timeout(TIMEOUT):
|
||||
hourly_usage = await self.hass.async_add_executor_job(
|
||||
self._client.usage,
|
||||
start_date,
|
||||
end_date,
|
||||
self._is_time_of_use,
|
||||
)
|
||||
except (ValueError, TypeError) as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
now = dt_util.now(PHOENIX_ZONE_INFO)
|
||||
|
||||
# SRP finalizes yesterday's data by DATA_COMPLETE_HOUR. Once we've
|
||||
# confirmed completeness, skip the API call until the next midnight
|
||||
# when a new day's data becomes available.
|
||||
if self._data_complete_until and now < self._data_complete_until:
|
||||
LOGGER.debug(
|
||||
"Data is complete until %s, skipping fetch", self._data_complete_until
|
||||
)
|
||||
return self.data or 0.0
|
||||
|
||||
# Because SRP provides hourly usage/cost for the previous day we need to
|
||||
# insert data into statistics ourselves.
|
||||
await self._insert_statistics()
|
||||
|
||||
# Fetch last 24 hours of srp_energy data, but most recent could be almost
|
||||
# 24 hours ago, so we will use last 2 days and then take the last 24 hours
|
||||
end_date = now
|
||||
start_date = end_date - timedelta(days=2)
|
||||
hourly_usage = (await self._async_read_data(start_date, end_date))[-24:]
|
||||
|
||||
LOGGER.debug(
|
||||
"async_update_data: Received %s record(s) from %s to %s",
|
||||
@@ -76,13 +141,264 @@ class SRPEnergyDataUpdateCoordinator(DataUpdateCoordinator[float]):
|
||||
end_date,
|
||||
)
|
||||
|
||||
previous_daily_usage = 0.0
|
||||
for _, _, _, kwh, _ in hourly_usage:
|
||||
previous_daily_usage += float(kwh)
|
||||
previous_daily_usage = sum(float(hour.kwh) for hour in hourly_usage)
|
||||
|
||||
LOGGER.debug(
|
||||
"async_update_data: previous_daily_usage %s",
|
||||
previous_daily_usage,
|
||||
)
|
||||
|
||||
# Yesterday's data is finalized once it's past DATA_COMPLETE_HOUR.
|
||||
# Cache this so subsequent hourly ticks skip the API until next midnight.
|
||||
if now.hour >= DATA_COMPLETE_HOUR:
|
||||
next_midnight = (now + timedelta(days=1)).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
self._data_complete_until = next_midnight
|
||||
LOGGER.debug(
|
||||
"Yesterday's data is complete. Next fetch after %s", next_midnight
|
||||
)
|
||||
|
||||
return previous_daily_usage
|
||||
|
||||
async def _insert_statistics(self) -> None:
|
||||
"""Insert SRP statistics."""
|
||||
id_prefix = slugify(f"{self.config_entry.data[CONF_ID]}").lower()
|
||||
cost_statistic_id = f"{DOMAIN}:{id_prefix}_energy_cost"
|
||||
consumption_statistic_id = f"{DOMAIN}:{id_prefix}_energy_consumption"
|
||||
LOGGER.debug(
|
||||
"Updating Statistics for %s, and %s",
|
||||
cost_statistic_id,
|
||||
consumption_statistic_id,
|
||||
)
|
||||
name_prefix = f"SRP {self.config_entry.title}"
|
||||
cost_metadata = StatisticMetaData(
|
||||
mean_type=StatisticMeanType.NONE,
|
||||
has_sum=True,
|
||||
name=f"{name_prefix} electric cost",
|
||||
source=DOMAIN,
|
||||
statistic_id=cost_statistic_id,
|
||||
unit_class=None,
|
||||
unit_of_measurement=None,
|
||||
)
|
||||
|
||||
consumption_metadata = StatisticMetaData(
|
||||
mean_type=StatisticMeanType.NONE,
|
||||
has_sum=True,
|
||||
name=f"{name_prefix} electric consumption",
|
||||
source=DOMAIN,
|
||||
statistic_id=consumption_statistic_id,
|
||||
unit_class=EnergyConverter.UNIT_CLASS,
|
||||
unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
)
|
||||
|
||||
# Get the last 2 statistics we have recorded for both consumption and
|
||||
# cost. We will use the oldest one as the baseline for the sum and
|
||||
# re-import the data after it. The last non-zero reported statistic
|
||||
# from SRP has potential to get updated as they fill out more data.
|
||||
last_stat, last_cost_stat = await asyncio.gather(
|
||||
get_instance(self.hass).async_add_executor_job(
|
||||
get_last_statistics,
|
||||
self.hass,
|
||||
2,
|
||||
consumption_statistic_id,
|
||||
True,
|
||||
{"sum"},
|
||||
),
|
||||
get_instance(self.hass).async_add_executor_job(
|
||||
get_last_statistics, self.hass, 2, cost_statistic_id, True, {"sum"}
|
||||
),
|
||||
)
|
||||
LOGGER.debug("Last statistics: %s", last_stat)
|
||||
if not last_stat:
|
||||
LOGGER.debug("Updating statistic for the first time")
|
||||
start_date = (dt_util.now(PHOENIX_ZONE_INFO) - timedelta(days=30)).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
hourly_usage = await self._async_read_data(start_date=start_date)
|
||||
cost_sum = 0.0
|
||||
consumption_sum = 0.0
|
||||
last_stats_time = None
|
||||
else:
|
||||
start_date = datetime.fromtimestamp(
|
||||
last_stat[consumption_statistic_id][-1]["start"], PHOENIX_ZONE_INFO
|
||||
)
|
||||
LOGGER.debug(
|
||||
"Last statistics for %s at %s", consumption_statistic_id, start_date
|
||||
)
|
||||
hourly_usage = await self._async_read_data(start_date=start_date)
|
||||
if not hourly_usage:
|
||||
LOGGER.debug(
|
||||
"No recent usage/cost data after %s. Skipping update", start_date
|
||||
)
|
||||
return
|
||||
start = hourly_usage[0].start_time
|
||||
LOGGER.debug("Getting statistics at: %s", start)
|
||||
|
||||
def _find_baseline_stats() -> dict:
|
||||
for end in (start + timedelta(seconds=1), None):
|
||||
result = statistics_during_period(
|
||||
self.hass,
|
||||
start,
|
||||
end,
|
||||
{cost_statistic_id, consumption_statistic_id},
|
||||
"hour",
|
||||
None,
|
||||
{"sum"},
|
||||
)
|
||||
if result:
|
||||
return result
|
||||
if end:
|
||||
LOGGER.debug(
|
||||
"Not found. Trying to find the oldest statistic after %s",
|
||||
start,
|
||||
)
|
||||
return {}
|
||||
|
||||
stats = await get_instance(self.hass).async_add_executor_job(
|
||||
_find_baseline_stats
|
||||
)
|
||||
|
||||
def _safe_get_first(
|
||||
records: list[Any], key: str, default: float | None
|
||||
) -> float | None:
|
||||
if records and records[0] and key in records[0]:
|
||||
return float(records[0][key])
|
||||
return default
|
||||
|
||||
if stats:
|
||||
LOGGER.debug("Statistics: %s", stats.get(cost_statistic_id, []))
|
||||
cost_sum = (
|
||||
_safe_get_first(stats.get(cost_statistic_id, []), "sum", 0.0) or 0.0
|
||||
)
|
||||
consumption_sum = (
|
||||
_safe_get_first(stats.get(consumption_statistic_id, []), "sum", 0.0)
|
||||
or 0.0
|
||||
)
|
||||
last_stats_time = _safe_get_first(
|
||||
stats.get(consumption_statistic_id, []), "start", None
|
||||
)
|
||||
else:
|
||||
# hourly_usage starts after the baseline stat period — SRP no
|
||||
# longer returns data that far back. Fall back to the most
|
||||
# recent known sums so we can continue the running total.
|
||||
LOGGER.debug(
|
||||
"No statistics found at %s; using most recent known sums", start
|
||||
)
|
||||
cost_sum = (
|
||||
_safe_get_first(
|
||||
last_cost_stat.get(cost_statistic_id, []), "sum", 0.0
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
consumption_sum = (
|
||||
_safe_get_first(
|
||||
last_stat.get(consumption_statistic_id, []), "sum", 0.0
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
last_stats_time = _safe_get_first(
|
||||
last_stat.get(consumption_statistic_id, []), "start", None
|
||||
)
|
||||
LOGGER.debug(
|
||||
"Last statistics for %s: Consumption sum: %s, Cost sum: %s, Last stat time: %s",
|
||||
consumption_statistic_id,
|
||||
consumption_sum,
|
||||
cost_sum,
|
||||
last_stats_time,
|
||||
)
|
||||
|
||||
cost_statistics = []
|
||||
consumption_statistics = []
|
||||
|
||||
# SRP pads incomplete/future hours with zeros at the tail. Strip those
|
||||
# trailing zeros so we don't create gaps for genuinely-zero hours earlier
|
||||
# in the dataset.
|
||||
last_nonzero = -1
|
||||
for i, usage in enumerate(hourly_usage):
|
||||
if usage.kwh != 0 or usage.cost != 0:
|
||||
last_nonzero = i
|
||||
hourly_usage = hourly_usage[: last_nonzero + 1]
|
||||
|
||||
for usage in hourly_usage:
|
||||
start = usage.start_time
|
||||
LOGGER.debug(
|
||||
"Processing usage data for %s. Last stat time: %s. Usage: %s, Cost: %s",
|
||||
start.timestamp(),
|
||||
last_stats_time,
|
||||
usage.kwh,
|
||||
usage.cost,
|
||||
)
|
||||
|
||||
if last_stats_time is not None and start.timestamp() <= last_stats_time:
|
||||
continue
|
||||
|
||||
cost_state = max(0, usage.cost)
|
||||
consumption_state = max(0, usage.kwh)
|
||||
|
||||
cost_sum += cost_state
|
||||
consumption_sum += consumption_state
|
||||
|
||||
cost_statistics.append(
|
||||
StatisticData(start=start, state=cost_state, sum=cost_sum)
|
||||
)
|
||||
consumption_statistics.append(
|
||||
StatisticData(start=start, state=consumption_state, sum=consumption_sum)
|
||||
)
|
||||
|
||||
LOGGER.debug(
|
||||
"Adding %s statistics for %s",
|
||||
len(cost_statistics),
|
||||
cost_statistic_id,
|
||||
)
|
||||
async_add_external_statistics(self.hass, cost_metadata, cost_statistics)
|
||||
LOGGER.debug(
|
||||
"Adding %s statistics for %s",
|
||||
len(consumption_statistics),
|
||||
consumption_statistic_id,
|
||||
)
|
||||
async_add_external_statistics(
|
||||
self.hass, consumption_metadata, consumption_statistics
|
||||
)
|
||||
|
||||
async def _async_read_data(
|
||||
self, start_date: datetime | None = None, end_date: datetime | None = None
|
||||
) -> list[Usage]:
|
||||
"""Read data from srp_energy client.
|
||||
|
||||
There are some limitations here. The current api *client* only knows how to fetch the hourly usage data.
|
||||
The hourly data for SRP is only available for the last month (30 days?).
|
||||
The SRP api does provide daily usage for the last 12 months and monthly usage for the last 3 years,
|
||||
however the api client will need to be updated to support these additional timeframes.
|
||||
"""
|
||||
try:
|
||||
async with asyncio.timeout(TIMEOUT):
|
||||
end_date = end_date or dt_util.now(PHOENIX_ZONE_INFO)
|
||||
start_date = start_date or (end_date - timedelta(days=31)).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
|
||||
def _fetch_and_parse() -> list[Usage]:
|
||||
# Filter out results outside the requested range — the SRP API
|
||||
# only accepts dates so the returned window may be wider.
|
||||
return [
|
||||
u
|
||||
for raw in self._client.usage(
|
||||
start_date, end_date, self._is_time_of_use
|
||||
)
|
||||
if (u := Usage.from_tuple(raw)) is not None
|
||||
and u.start_time >= start_date
|
||||
and u.end_time <= end_date
|
||||
]
|
||||
|
||||
results = await self.hass.async_add_executor_job(_fetch_and_parse)
|
||||
LOGGER.debug(
|
||||
"async_read_data: Received %s record(s) from %s to %s",
|
||||
len(results) if results else "None",
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
return results
|
||||
except (ValueError, TypeError) as err:
|
||||
LOGGER.error("Error communicating with API: %s", err)
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"domain": "srp_energy",
|
||||
"name": "SRP Energy",
|
||||
"codeowners": ["@briglx"],
|
||||
"codeowners": ["@briglx", "@ammmze"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["recorder"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/srp_energy",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -32,65 +32,151 @@ TEST_CONFIG_CABIN: Final[dict[str, str]] = {
|
||||
}
|
||||
|
||||
MOCK_USAGE = [
|
||||
("7/31/2022", "00:00 AM", "2022-07-31T00:00:00", "1.2", "0.19"),
|
||||
("7/31/2022", "01:00 AM", "2022-07-31T01:00:00", "1.3", "0.20"),
|
||||
("7/31/2022", "02:00 AM", "2022-07-31T02:00:00", "1.1", "0.17"),
|
||||
("7/31/2022", "03:00 AM", "2022-07-31T03:00:00", "1.2", "0.18"),
|
||||
("7/31/2022", "04:00 AM", "2022-07-31T04:00:00", "0.8", "0.13"),
|
||||
("7/31/2022", "05:00 AM", "2022-07-31T05:00:00", "0.9", "0.14"),
|
||||
("7/31/2022", "06:00 AM", "2022-07-31T06:00:00", "1.6", "0.24"),
|
||||
("7/31/2022", "07:00 AM", "2022-07-31T07:00:00", "3.7", "0.53"),
|
||||
("7/31/2022", "08:00 AM", "2022-07-31T08:00:00", "1.0", "0.16"),
|
||||
("7/31/2022", "09:00 AM", "2022-07-31T09:00:00", "0.7", "0.12"),
|
||||
("7/31/2022", "10:00 AM", "2022-07-31T10:00:00", "1.9", "0.28"),
|
||||
("7/31/2022", "11:00 AM", "2022-07-31T11:00:00", "4.3", "0.61"),
|
||||
("7/31/2022", "12:00 PM", "2022-07-31T12:00:00", "2.0", "0.29"),
|
||||
("7/31/2022", "01:00 PM", "2022-07-31T13:00:00", "3.9", "0.55"),
|
||||
("7/31/2022", "02:00 PM", "2022-07-31T14:00:00", "5.3", "0.75"),
|
||||
("7/31/2022", "03:00 PM", "2022-07-31T15:00:00", "5.0", "0.70"),
|
||||
("7/31/2022", "04:00 PM", "2022-07-31T16:00:00", "2.2", "0.31"),
|
||||
("7/31/2022", "05:00 PM", "2022-07-31T17:00:00", "2.6", "0.37"),
|
||||
("7/31/2022", "06:00 PM", "2022-07-31T18:00:00", "4.5", "0.64"),
|
||||
("7/31/2022", "07:00 PM", "2022-07-31T19:00:00", "2.5", "0.35"),
|
||||
("7/31/2022", "08:00 PM", "2022-07-31T20:00:00", "2.9", "0.42"),
|
||||
("7/31/2022", "09:00 PM", "2022-07-31T21:00:00", "2.2", "0.32"),
|
||||
("7/31/2022", "10:00 PM", "2022-07-31T22:00:00", "2.1", "0.30"),
|
||||
("7/31/2022", "11:00 PM", "2022-07-31T23:00:00", "2.0", "0.28"),
|
||||
("8/01/2022", "00:00 AM", "2022-08-01T00:00:00", "1.8", "0.26"),
|
||||
("8/01/2022", "01:00 AM", "2022-08-01T01:00:00", "1.7", "0.26"),
|
||||
("8/01/2022", "02:00 AM", "2022-08-01T02:00:00", "1.7", "0.26"),
|
||||
("8/01/2022", "03:00 AM", "2022-08-01T03:00:00", "0.8", "0.14"),
|
||||
("8/01/2022", "04:00 AM", "2022-08-01T04:00:00", "1.2", "0.19"),
|
||||
("8/01/2022", "05:00 AM", "2022-08-01T05:00:00", "1.6", "0.23"),
|
||||
("8/01/2022", "06:00 AM", "2022-08-01T06:00:00", "1.2", "0.18"),
|
||||
("8/01/2022", "07:00 AM", "2022-08-01T07:00:00", "3.1", "0.44"),
|
||||
("8/01/2022", "08:00 AM", "2022-08-01T08:00:00", "2.5", "0.35"),
|
||||
("8/01/2022", "09:00 AM", "2022-08-01T09:00:00", "3.3", "0.47"),
|
||||
("8/01/2022", "10:00 AM", "2022-08-01T10:00:00", "2.6", "0.37"),
|
||||
("8/01/2022", "11:00 AM", "2022-08-01T11:00:00", "0.8", "0.13"),
|
||||
("8/01/2022", "12:00 PM", "2022-08-01T12:00:00", "0.6", "0.11"),
|
||||
("8/01/2022", "01:00 PM", "2022-08-01T13:00:00", "6.4", "0.9"),
|
||||
("8/01/2022", "02:00 PM", "2022-08-01T14:00:00", "3.6", "0.52"),
|
||||
("8/01/2022", "03:00 PM", "2022-08-01T15:00:00", "5.5", "0.79"),
|
||||
("8/01/2022", "04:00 PM", "2022-08-01T16:00:00", "3", "0.43"),
|
||||
("8/01/2022", "05:00 PM", "2022-08-01T17:00:00", "5", "0.71"),
|
||||
("8/01/2022", "06:00 PM", "2022-08-01T18:00:00", "4.4", "0.63"),
|
||||
("8/01/2022", "07:00 PM", "2022-08-01T19:00:00", "3.8", "0.54"),
|
||||
("8/01/2022", "08:00 PM", "2022-08-01T20:00:00", "3.6", "0.51"),
|
||||
("8/01/2022", "09:00 PM", "2022-08-01T21:00:00", "2.9", "0.4"),
|
||||
("8/01/2022", "10:00 PM", "2022-08-01T22:00:00", "3.4", "0.49"),
|
||||
("8/01/2022", "11:00 PM", "2022-08-01T23:00:00", "2.9", "0.41"),
|
||||
("8/02/2022", "00:00 AM", "2022-08-02T00:00:00", "2", "0.3"),
|
||||
("8/02/2022", "01:00 AM", "2022-08-02T01:00:00", "2", "0.29"),
|
||||
("8/02/2022", "02:00 AM", "2022-08-02T02:00:00", "1.9", "0.28"),
|
||||
("8/02/2022", "03:00 AM", "2022-08-02T03:00:00", "1.8", "0.27"),
|
||||
("8/02/2022", "04:00 AM", "2022-08-02T04:00:00", "1.8", "0.26"),
|
||||
("8/02/2022", "05:00 AM", "2022-08-02T05:00:00", "1.6", "0.23"),
|
||||
("8/02/2022", "06:00 AM", "2022-08-02T06:00:00", "0.8", "0.14"),
|
||||
("8/02/2022", "07:00 AM", "2022-08-02T07:00:00", "4", "0.56"),
|
||||
("8/02/2022", "08:00 AM", "2022-08-02T08:00:00", "2.4", "0.34"),
|
||||
("8/02/2022", "09:00 AM", "2022-08-02T09:00:00", "4.1", "0.58"),
|
||||
("8/02/2022", "10:00 AM", "2022-08-02T10:00:00", "2.6", "0.37"),
|
||||
("8/02/2022", "11:00 AM", "2022-08-02T11:00:00", "0.5", "0.1"),
|
||||
("8/02/2022", "00:00 AM", "2022-08-02T12:00:00", "1", "0.16"),
|
||||
("7/31/2022", "00:00 AM", "2022-07-31T00:00:00", 1.2, 0.19),
|
||||
("7/31/2022", "01:00 AM", "2022-07-31T01:00:00", 1.3, 0.20),
|
||||
("7/31/2022", "02:00 AM", "2022-07-31T02:00:00", 1.1, 0.17),
|
||||
("7/31/2022", "03:00 AM", "2022-07-31T03:00:00", 1.2, 0.18),
|
||||
("7/31/2022", "04:00 AM", "2022-07-31T04:00:00", 0.8, 0.13),
|
||||
("7/31/2022", "05:00 AM", "2022-07-31T05:00:00", 0.9, 0.14),
|
||||
("7/31/2022", "06:00 AM", "2022-07-31T06:00:00", 1.6, 0.24),
|
||||
("7/31/2022", "07:00 AM", "2022-07-31T07:00:00", 3.7, 0.53),
|
||||
("7/31/2022", "08:00 AM", "2022-07-31T08:00:00", 1.0, 0.16),
|
||||
("7/31/2022", "09:00 AM", "2022-07-31T09:00:00", 0.7, 0.12),
|
||||
("7/31/2022", "10:00 AM", "2022-07-31T10:00:00", 1.9, 0.28),
|
||||
("7/31/2022", "11:00 AM", "2022-07-31T11:00:00", 4.3, 0.61),
|
||||
("7/31/2022", "12:00 PM", "2022-07-31T12:00:00", 2.0, 0.29),
|
||||
("7/31/2022", "01:00 PM", "2022-07-31T13:00:00", 3.9, 0.55),
|
||||
("7/31/2022", "02:00 PM", "2022-07-31T14:00:00", 5.3, 0.75),
|
||||
("7/31/2022", "03:00 PM", "2022-07-31T15:00:00", 5.0, 0.70),
|
||||
("7/31/2022", "04:00 PM", "2022-07-31T16:00:00", 2.2, 0.31),
|
||||
("7/31/2022", "05:00 PM", "2022-07-31T17:00:00", 2.6, 0.37),
|
||||
("7/31/2022", "06:00 PM", "2022-07-31T18:00:00", 4.5, 0.64),
|
||||
("7/31/2022", "07:00 PM", "2022-07-31T19:00:00", 2.5, 0.35),
|
||||
("7/31/2022", "08:00 PM", "2022-07-31T20:00:00", 2.9, 0.42),
|
||||
("7/31/2022", "09:00 PM", "2022-07-31T21:00:00", 2.2, 0.32),
|
||||
("7/31/2022", "10:00 PM", "2022-07-31T22:00:00", 2.1, 0.30),
|
||||
("7/31/2022", "11:00 PM", "2022-07-31T23:00:00", 2.0, 0.28),
|
||||
("8/01/2022", "00:00 AM", "2022-08-01T00:00:00", 1.8, 0.26),
|
||||
("8/01/2022", "01:00 AM", "2022-08-01T01:00:00", 1.7, 0.26),
|
||||
("8/01/2022", "02:00 AM", "2022-08-01T02:00:00", 1.7, 0.26),
|
||||
("8/01/2022", "03:00 AM", "2022-08-01T03:00:00", 0.8, 0.14),
|
||||
("8/01/2022", "04:00 AM", "2022-08-01T04:00:00", 1.2, 0.19),
|
||||
("8/01/2022", "05:00 AM", "2022-08-01T05:00:00", 1.6, 0.23),
|
||||
("8/01/2022", "06:00 AM", "2022-08-01T06:00:00", 1.2, 0.18),
|
||||
("8/01/2022", "07:00 AM", "2022-08-01T07:00:00", 3.1, 0.44),
|
||||
("8/01/2022", "08:00 AM", "2022-08-01T08:00:00", 2.5, 0.35),
|
||||
("8/01/2022", "09:00 AM", "2022-08-01T09:00:00", 3.3, 0.47),
|
||||
("8/01/2022", "10:00 AM", "2022-08-01T10:00:00", 2.6, 0.37),
|
||||
("8/01/2022", "11:00 AM", "2022-08-01T11:00:00", 0.8, 0.13),
|
||||
("8/01/2022", "12:00 PM", "2022-08-01T12:00:00", 0.6, 0.11),
|
||||
("8/01/2022", "01:00 PM", "2022-08-01T13:00:00", 6.4, 0.9),
|
||||
("8/01/2022", "02:00 PM", "2022-08-01T14:00:00", 3.6, 0.52),
|
||||
("8/01/2022", "03:00 PM", "2022-08-01T15:00:00", 5.5, 0.79),
|
||||
("8/01/2022", "04:00 PM", "2022-08-01T16:00:00", 3.0, 0.43),
|
||||
("8/01/2022", "05:00 PM", "2022-08-01T17:00:00", 5.0, 0.71),
|
||||
("8/01/2022", "06:00 PM", "2022-08-01T18:00:00", 4.4, 0.63),
|
||||
("8/01/2022", "07:00 PM", "2022-08-01T19:00:00", 3.8, 0.54),
|
||||
("8/01/2022", "08:00 PM", "2022-08-01T20:00:00", 3.6, 0.51),
|
||||
("8/01/2022", "09:00 PM", "2022-08-01T21:00:00", 2.9, 0.4),
|
||||
("8/01/2022", "10:00 PM", "2022-08-01T22:00:00", 3.4, 0.49),
|
||||
("8/01/2022", "11:00 PM", "2022-08-01T23:00:00", 2.9, 0.41),
|
||||
("8/02/2022", "00:00 AM", "2022-08-02T00:00:00", 2.0, 0.3),
|
||||
("8/02/2022", "01:00 AM", "2022-08-02T01:00:00", 2.0, 0.29),
|
||||
("8/02/2022", "02:00 AM", "2022-08-02T02:00:00", 1.9, 0.28),
|
||||
("8/02/2022", "03:00 AM", "2022-08-02T03:00:00", 1.8, 0.27),
|
||||
("8/02/2022", "04:00 AM", "2022-08-02T04:00:00", 1.8, 0.26),
|
||||
("8/02/2022", "05:00 AM", "2022-08-02T05:00:00", 1.6, 0.23),
|
||||
("8/02/2022", "06:00 AM", "2022-08-02T06:00:00", 0.8, 0.14),
|
||||
("8/02/2022", "07:00 AM", "2022-08-02T07:00:00", 4.0, 0.56),
|
||||
("8/02/2022", "08:00 AM", "2022-08-02T08:00:00", 2.4, 0.34),
|
||||
("8/02/2022", "09:00 AM", "2022-08-02T09:00:00", 4.1, 0.58),
|
||||
("8/02/2022", "10:00 AM", "2022-08-02T10:00:00", 2.6, 0.37),
|
||||
("8/02/2022", "11:00 AM", "2022-08-02T11:00:00", 0.5, 0.1),
|
||||
("8/02/2022", "12:00 PM", "2022-08-02T12:00:00", 1.0, 0.16),
|
||||
]
|
||||
|
||||
# Majority of the day the latest data we can see is yesterdays data
|
||||
MOCK_USAGE_DAY_AUG_1_HOUR_5 = [
|
||||
("7/31/2022", "00:00 AM", "2022-07-31T00:00:00", 1.2, 0.19),
|
||||
("7/31/2022", "01:00 AM", "2022-07-31T01:00:00", 1.3, 0.20),
|
||||
("7/31/2022", "02:00 AM", "2022-07-31T02:00:00", 1.1, 0.17),
|
||||
("7/31/2022", "03:00 AM", "2022-07-31T03:00:00", 1.2, 0.18),
|
||||
("7/31/2022", "04:00 AM", "2022-07-31T04:00:00", 0.8, 0.13),
|
||||
("7/31/2022", "05:00 AM", "2022-07-31T05:00:00", 0.9, 0.14),
|
||||
("7/31/2022", "06:00 AM", "2022-07-31T06:00:00", 1.6, 0.24),
|
||||
("7/31/2022", "07:00 AM", "2022-07-31T07:00:00", 3.7, 0.53),
|
||||
("7/31/2022", "08:00 AM", "2022-07-31T08:00:00", 1.0, 0.16),
|
||||
("7/31/2022", "09:00 AM", "2022-07-31T09:00:00", 0.7, 0.12),
|
||||
("7/31/2022", "10:00 AM", "2022-07-31T10:00:00", 1.9, 0.28),
|
||||
("7/31/2022", "11:00 AM", "2022-07-31T11:00:00", 4.3, 0.61),
|
||||
("7/31/2022", "12:00 PM", "2022-07-31T12:00:00", 2.0, 0.29),
|
||||
("7/31/2022", "01:00 PM", "2022-07-31T13:00:00", 3.9, 0.55),
|
||||
("7/31/2022", "02:00 PM", "2022-07-31T14:00:00", 5.3, 0.75),
|
||||
("7/31/2022", "03:00 PM", "2022-07-31T15:00:00", 5.0, 0.70),
|
||||
("7/31/2022", "04:00 PM", "2022-07-31T16:00:00", 2.2, 0.31),
|
||||
("7/31/2022", "05:00 PM", "2022-07-31T17:00:00", 2.6, 0.37),
|
||||
("7/31/2022", "06:00 PM", "2022-07-31T18:00:00", 4.5, 0.64),
|
||||
("7/31/2022", "07:00 PM", "2022-07-31T19:00:00", 2.5, 0.35),
|
||||
("7/31/2022", "08:00 PM", "2022-07-31T20:00:00", 2.9, 0.42),
|
||||
("7/31/2022", "09:00 PM", "2022-07-31T21:00:00", 2.2, 0.32),
|
||||
("7/31/2022", "10:00 PM", "2022-07-31T22:00:00", 2.1, 0.30),
|
||||
("7/31/2022", "11:00 PM", "2022-07-31T23:00:00", 2.0, 0.28),
|
||||
]
|
||||
|
||||
# At midnight the next day, we get yesterday's data BUT the last few hours
|
||||
# incomplete.
|
||||
MOCK_USAGE_DAY_AUG_2_HOUR_0 = [
|
||||
# everything from aug 1
|
||||
*MOCK_USAGE_DAY_AUG_1_HOUR_5,
|
||||
("8/01/2022", "00:00 AM", "2022-08-01T00:00:00", 1.8, 0.26),
|
||||
("8/01/2022", "01:00 AM", "2022-08-01T01:00:00", 1.7, 0.26),
|
||||
("8/01/2022", "02:00 AM", "2022-08-01T02:00:00", 1.7, 0.26),
|
||||
("8/01/2022", "03:00 AM", "2022-08-01T03:00:00", 0.8, 0.14),
|
||||
("8/01/2022", "04:00 AM", "2022-08-01T04:00:00", 1.2, 0.19),
|
||||
("8/01/2022", "05:00 AM", "2022-08-01T05:00:00", 1.6, 0.23),
|
||||
("8/01/2022", "06:00 AM", "2022-08-01T06:00:00", 1.2, 0.18),
|
||||
("8/01/2022", "07:00 AM", "2022-08-01T07:00:00", 3.1, 0.44),
|
||||
("8/01/2022", "08:00 AM", "2022-08-01T08:00:00", 2.5, 0.35),
|
||||
("8/01/2022", "09:00 AM", "2022-08-01T09:00:00", 3.3, 0.47),
|
||||
("8/01/2022", "10:00 AM", "2022-08-01T10:00:00", 2.6, 0.37),
|
||||
("8/01/2022", "11:00 AM", "2022-08-01T11:00:00", 0.8, 0.13),
|
||||
("8/01/2022", "12:00 PM", "2022-08-01T12:00:00", 0.6, 0.11),
|
||||
("8/01/2022", "01:00 PM", "2022-08-01T13:00:00", 6.4, 0.9),
|
||||
("8/01/2022", "02:00 PM", "2022-08-01T14:00:00", 3.6, 0.52),
|
||||
("8/01/2022", "03:00 PM", "2022-08-01T15:00:00", 5.5, 0.79),
|
||||
("8/01/2022", "04:00 PM", "2022-08-01T16:00:00", 3.0, 0.43),
|
||||
("8/01/2022", "05:00 PM", "2022-08-01T17:00:00", 5.0, 0.71),
|
||||
("8/01/2022", "06:00 PM", "2022-08-01T18:00:00", 4.4, 0.63),
|
||||
("8/01/2022", "07:00 PM", "2022-08-01T19:00:00", 1.9, 0.27),
|
||||
("8/01/2022", "08:00 PM", "2022-08-01T20:00:00", 0.0, 0.0),
|
||||
("8/01/2022", "09:00 PM", "2022-08-01T21:00:00", 0.0, 0.0),
|
||||
("8/01/2022", "10:00 PM", "2022-08-01T22:00:00", 0.0, 0.0),
|
||||
("8/01/2022", "11:00 PM", "2022-08-01T23:00:00", 0.0, 0.0),
|
||||
]
|
||||
|
||||
# Around an hour after midnight, we get more of yesterdays data BUT the last
|
||||
# record typically only accounts for part of the last hour, so it will get
|
||||
# updated again.
|
||||
MOCK_USAGE_DAY_AUG_2_HOUR_1 = [
|
||||
# everything from hour 0 except the last 5 entries, replaced by the updated values below
|
||||
*MOCK_USAGE_DAY_AUG_2_HOUR_0[:-5],
|
||||
("8/01/2022", "07:00 PM", "2022-08-01T19:00:00", 3.8, 0.54),
|
||||
("8/01/2022", "08:00 PM", "2022-08-01T20:00:00", 3.6, 0.51),
|
||||
("8/01/2022", "09:00 PM", "2022-08-01T21:00:00", 2.9, 0.4),
|
||||
("8/01/2022", "10:00 PM", "2022-08-01T22:00:00", 3.4, 0.49),
|
||||
("8/01/2022", "11:00 PM", "2022-08-01T23:00:00", 1.4, 0.2),
|
||||
]
|
||||
|
||||
# Finally around 5 AM, we get the finalized data
|
||||
MOCK_USAGE_DAY_AUG_2_HOUR_5 = [
|
||||
# everything from hour 1 except the last 1 entry, replaced by the updated values below
|
||||
*MOCK_USAGE_DAY_AUG_2_HOUR_1[:-1],
|
||||
("8/01/2022", "11:00 PM", "2022-08-01T23:00:00", 2.9, 0.41),
|
||||
]
|
||||
|
||||
MOCK_USAGE_AT_TIMES = [
|
||||
("2022-08-01T05:00:00-07:00", MOCK_USAGE_DAY_AUG_1_HOUR_5),
|
||||
("2022-08-02T00:00:00-07:00", MOCK_USAGE_DAY_AUG_2_HOUR_0),
|
||||
("2022-08-02T01:00:00-07:00", MOCK_USAGE_DAY_AUG_2_HOUR_1),
|
||||
("2022-08-02T05:00:00-07:00", MOCK_USAGE_DAY_AUG_2_HOUR_5),
|
||||
]
|
||||
|
||||
@@ -7,17 +7,21 @@ from unittest.mock import MagicMock, patch
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.components.srp_energy.const import DOMAIN, PHOENIX_TIME_ZONE
|
||||
from homeassistant.components.srp_energy.coordinator import HourlyUsageTuple
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import MOCK_USAGE, TEST_CONFIG_HOME
|
||||
from . import MOCK_USAGE_AT_TIMES, TEST_CONFIG_HOME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
PHOENIX_ZONE_INFO = dt_util.get_time_zone(PHOENIX_TIME_ZONE)
|
||||
|
||||
@pytest.fixture(name="setup_hass_config", autouse=True)
|
||||
|
||||
@pytest.fixture(name="setup_hass_config")
|
||||
async def fixture_setup_hass_config(hass: HomeAssistant) -> None:
|
||||
"""Set up things to be run when tests are started."""
|
||||
hass.config.latitude = 33.27
|
||||
@@ -34,17 +38,43 @@ def fixture_hass_tz_info(hass: HomeAssistant, setup_hass_config) -> dt.tzinfo |
|
||||
@pytest.fixture(name="test_date")
|
||||
def fixture_test_date(hass: HomeAssistant, hass_tz_info) -> dt.datetime | None:
|
||||
"""Return test datetime for the hass timezone."""
|
||||
return dt.datetime(2022, 8, 2, 0, 0, 0, 0, tzinfo=hass_tz_info)
|
||||
# Default to run in the middle of the day on aug 2
|
||||
return dt.datetime(2022, 8, 2, 12, 0, 0, 0, tzinfo=hass_tz_info)
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_config_entry")
|
||||
def fixture_mock_config_entry() -> MockConfigEntry:
|
||||
def fixture_mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN, data=TEST_CONFIG_HOME, unique_id=TEST_CONFIG_HOME[CONF_ID]
|
||||
)
|
||||
|
||||
|
||||
def _mock_usage(
|
||||
start_date: dt.datetime, end_date: dt.datetime, is_tou: bool
|
||||
) -> list[HourlyUsageTuple]:
|
||||
now = dt_util.now(PHOENIX_ZONE_INFO).replace(minute=0, second=0, microsecond=0)
|
||||
data = MOCK_USAGE_AT_TIMES[-1][1]
|
||||
start_date = start_date.replace(
|
||||
hour=0, minute=0, second=0, microsecond=0, tzinfo=PHOENIX_ZONE_INFO
|
||||
)
|
||||
end_date = end_date.replace(
|
||||
hour=23, minute=59, second=59, microsecond=999999, tzinfo=PHOENIX_ZONE_INFO
|
||||
)
|
||||
for key, value in reversed(MOCK_USAGE_AT_TIMES):
|
||||
if now >= dt_util.parse_datetime(key).replace(tzinfo=PHOENIX_ZONE_INFO):
|
||||
data = value
|
||||
break
|
||||
|
||||
def is_in_range(ts: str) -> bool:
|
||||
item_time = dt_util.parse_datetime(ts).replace(tzinfo=PHOENIX_ZONE_INFO)
|
||||
after_start = item_time >= start_date
|
||||
before_end = item_time <= end_date
|
||||
return after_start and before_end
|
||||
|
||||
return [item for item in data if is_in_range(item[2])]
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_srp_energy")
|
||||
def fixture_mock_srp_energy() -> Generator[MagicMock]:
|
||||
"""Return a mocked SrpEnergyClient client."""
|
||||
@@ -53,7 +83,7 @@ def fixture_mock_srp_energy() -> Generator[MagicMock]:
|
||||
) as srp_energy_mock:
|
||||
client = srp_energy_mock.return_value
|
||||
client.validate.return_value = True
|
||||
client.usage.return_value = MOCK_USAGE
|
||||
client.usage.side_effect = _mock_usage
|
||||
yield client
|
||||
|
||||
|
||||
@@ -65,12 +95,13 @@ def fixture_mock_srp_energy_config_flow() -> Generator[MagicMock]:
|
||||
) as srp_energy_mock:
|
||||
client = srp_energy_mock.return_value
|
||||
client.validate.return_value = True
|
||||
client.usage.return_value = MOCK_USAGE
|
||||
client.usage.side_effect = _mock_usage
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
test_date: dt.datetime,
|
||||
|
||||
@@ -0,0 +1,677 @@
|
||||
# serializer version: 1
|
||||
# name: test_coordinator_first_run
|
||||
defaultdict({
|
||||
'srp_energy:123456789_energy_consumption': list([
|
||||
dict({
|
||||
'end': 1659254400.0,
|
||||
'start': 1659250800.0,
|
||||
'state': 1.2,
|
||||
'sum': 1.2,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659258000.0,
|
||||
'start': 1659254400.0,
|
||||
'state': 1.3,
|
||||
'sum': 2.5,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659261600.0,
|
||||
'start': 1659258000.0,
|
||||
'state': 1.1,
|
||||
'sum': 3.6,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659265200.0,
|
||||
'start': 1659261600.0,
|
||||
'state': 1.2,
|
||||
'sum': 4.8,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659268800.0,
|
||||
'start': 1659265200.0,
|
||||
'state': 0.8,
|
||||
'sum': 5.6,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659272400.0,
|
||||
'start': 1659268800.0,
|
||||
'state': 0.9,
|
||||
'sum': 6.5,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659276000.0,
|
||||
'start': 1659272400.0,
|
||||
'state': 1.6,
|
||||
'sum': 8.1,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659279600.0,
|
||||
'start': 1659276000.0,
|
||||
'state': 3.7,
|
||||
'sum': 11.8,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659283200.0,
|
||||
'start': 1659279600.0,
|
||||
'state': 1.0,
|
||||
'sum': 12.8,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659286800.0,
|
||||
'start': 1659283200.0,
|
||||
'state': 0.7,
|
||||
'sum': 13.5,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659290400.0,
|
||||
'start': 1659286800.0,
|
||||
'state': 1.9,
|
||||
'sum': 15.4,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659294000.0,
|
||||
'start': 1659290400.0,
|
||||
'state': 4.3,
|
||||
'sum': 19.7,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659297600.0,
|
||||
'start': 1659294000.0,
|
||||
'state': 2.0,
|
||||
'sum': 21.7,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659301200.0,
|
||||
'start': 1659297600.0,
|
||||
'state': 3.9,
|
||||
'sum': 25.599999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659304800.0,
|
||||
'start': 1659301200.0,
|
||||
'state': 5.3,
|
||||
'sum': 30.9,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659308400.0,
|
||||
'start': 1659304800.0,
|
||||
'state': 5.0,
|
||||
'sum': 35.9,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659312000.0,
|
||||
'start': 1659308400.0,
|
||||
'state': 2.2,
|
||||
'sum': 38.1,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659315600.0,
|
||||
'start': 1659312000.0,
|
||||
'state': 2.6,
|
||||
'sum': 40.7,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659319200.0,
|
||||
'start': 1659315600.0,
|
||||
'state': 4.5,
|
||||
'sum': 45.2,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659322800.0,
|
||||
'start': 1659319200.0,
|
||||
'state': 2.5,
|
||||
'sum': 47.7,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659326400.0,
|
||||
'start': 1659322800.0,
|
||||
'state': 2.9,
|
||||
'sum': 50.6,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659330000.0,
|
||||
'start': 1659326400.0,
|
||||
'state': 2.2,
|
||||
'sum': 52.800000000000004,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659333600.0,
|
||||
'start': 1659330000.0,
|
||||
'state': 2.1,
|
||||
'sum': 54.900000000000006,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659337200.0,
|
||||
'start': 1659333600.0,
|
||||
'state': 2.0,
|
||||
'sum': 56.900000000000006,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659340800.0,
|
||||
'start': 1659337200.0,
|
||||
'state': 1.8,
|
||||
'sum': 58.7,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659344400.0,
|
||||
'start': 1659340800.0,
|
||||
'state': 1.7,
|
||||
'sum': 60.400000000000006,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659348000.0,
|
||||
'start': 1659344400.0,
|
||||
'state': 1.7,
|
||||
'sum': 62.10000000000001,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659351600.0,
|
||||
'start': 1659348000.0,
|
||||
'state': 0.8,
|
||||
'sum': 62.900000000000006,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659355200.0,
|
||||
'start': 1659351600.0,
|
||||
'state': 1.2,
|
||||
'sum': 64.10000000000001,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659358800.0,
|
||||
'start': 1659355200.0,
|
||||
'state': 1.6,
|
||||
'sum': 65.7,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659362400.0,
|
||||
'start': 1659358800.0,
|
||||
'state': 1.2,
|
||||
'sum': 66.9,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659366000.0,
|
||||
'start': 1659362400.0,
|
||||
'state': 3.1,
|
||||
'sum': 70.0,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659369600.0,
|
||||
'start': 1659366000.0,
|
||||
'state': 2.5,
|
||||
'sum': 72.5,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659373200.0,
|
||||
'start': 1659369600.0,
|
||||
'state': 3.3,
|
||||
'sum': 75.8,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659376800.0,
|
||||
'start': 1659373200.0,
|
||||
'state': 2.6,
|
||||
'sum': 78.39999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659380400.0,
|
||||
'start': 1659376800.0,
|
||||
'state': 0.8,
|
||||
'sum': 79.19999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659384000.0,
|
||||
'start': 1659380400.0,
|
||||
'state': 0.6,
|
||||
'sum': 79.79999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659387600.0,
|
||||
'start': 1659384000.0,
|
||||
'state': 6.4,
|
||||
'sum': 86.19999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659391200.0,
|
||||
'start': 1659387600.0,
|
||||
'state': 3.6,
|
||||
'sum': 89.79999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659394800.0,
|
||||
'start': 1659391200.0,
|
||||
'state': 5.5,
|
||||
'sum': 95.29999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659398400.0,
|
||||
'start': 1659394800.0,
|
||||
'state': 3.0,
|
||||
'sum': 98.29999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659402000.0,
|
||||
'start': 1659398400.0,
|
||||
'state': 5.0,
|
||||
'sum': 103.29999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659405600.0,
|
||||
'start': 1659402000.0,
|
||||
'state': 4.4,
|
||||
'sum': 107.69999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659409200.0,
|
||||
'start': 1659405600.0,
|
||||
'state': 3.8,
|
||||
'sum': 111.49999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659412800.0,
|
||||
'start': 1659409200.0,
|
||||
'state': 3.6,
|
||||
'sum': 115.09999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659416400.0,
|
||||
'start': 1659412800.0,
|
||||
'state': 2.9,
|
||||
'sum': 117.99999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659420000.0,
|
||||
'start': 1659416400.0,
|
||||
'state': 3.4,
|
||||
'sum': 121.39999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659423600.0,
|
||||
'start': 1659420000.0,
|
||||
'state': 2.9,
|
||||
'sum': 124.3,
|
||||
}),
|
||||
]),
|
||||
'srp_energy:123456789_energy_cost': list([
|
||||
dict({
|
||||
'end': 1659254400.0,
|
||||
'start': 1659250800.0,
|
||||
'state': 0.19,
|
||||
'sum': 0.19,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659258000.0,
|
||||
'start': 1659254400.0,
|
||||
'state': 0.2,
|
||||
'sum': 0.39,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659261600.0,
|
||||
'start': 1659258000.0,
|
||||
'state': 0.17,
|
||||
'sum': 0.56,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659265200.0,
|
||||
'start': 1659261600.0,
|
||||
'state': 0.18,
|
||||
'sum': 0.74,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659268800.0,
|
||||
'start': 1659265200.0,
|
||||
'state': 0.13,
|
||||
'sum': 0.87,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659272400.0,
|
||||
'start': 1659268800.0,
|
||||
'state': 0.14,
|
||||
'sum': 1.01,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659276000.0,
|
||||
'start': 1659272400.0,
|
||||
'state': 0.24,
|
||||
'sum': 1.25,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659279600.0,
|
||||
'start': 1659276000.0,
|
||||
'state': 0.53,
|
||||
'sum': 1.78,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659283200.0,
|
||||
'start': 1659279600.0,
|
||||
'state': 0.16,
|
||||
'sum': 1.94,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659286800.0,
|
||||
'start': 1659283200.0,
|
||||
'state': 0.12,
|
||||
'sum': 2.06,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659290400.0,
|
||||
'start': 1659286800.0,
|
||||
'state': 0.28,
|
||||
'sum': 2.34,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659294000.0,
|
||||
'start': 1659290400.0,
|
||||
'state': 0.61,
|
||||
'sum': 2.9499999999999997,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659297600.0,
|
||||
'start': 1659294000.0,
|
||||
'state': 0.29,
|
||||
'sum': 3.2399999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659301200.0,
|
||||
'start': 1659297600.0,
|
||||
'state': 0.55,
|
||||
'sum': 3.79,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659304800.0,
|
||||
'start': 1659301200.0,
|
||||
'state': 0.75,
|
||||
'sum': 4.54,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659308400.0,
|
||||
'start': 1659304800.0,
|
||||
'state': 0.7,
|
||||
'sum': 5.24,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659312000.0,
|
||||
'start': 1659308400.0,
|
||||
'state': 0.31,
|
||||
'sum': 5.55,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659315600.0,
|
||||
'start': 1659312000.0,
|
||||
'state': 0.37,
|
||||
'sum': 5.92,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659319200.0,
|
||||
'start': 1659315600.0,
|
||||
'state': 0.64,
|
||||
'sum': 6.56,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659322800.0,
|
||||
'start': 1659319200.0,
|
||||
'state': 0.35,
|
||||
'sum': 6.909999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659326400.0,
|
||||
'start': 1659322800.0,
|
||||
'state': 0.42,
|
||||
'sum': 7.329999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659330000.0,
|
||||
'start': 1659326400.0,
|
||||
'state': 0.32,
|
||||
'sum': 7.6499999999999995,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659333600.0,
|
||||
'start': 1659330000.0,
|
||||
'state': 0.3,
|
||||
'sum': 7.949999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659337200.0,
|
||||
'start': 1659333600.0,
|
||||
'state': 0.28,
|
||||
'sum': 8.229999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659340800.0,
|
||||
'start': 1659337200.0,
|
||||
'state': 0.26,
|
||||
'sum': 8.489999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659344400.0,
|
||||
'start': 1659340800.0,
|
||||
'state': 0.26,
|
||||
'sum': 8.749999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659348000.0,
|
||||
'start': 1659344400.0,
|
||||
'state': 0.26,
|
||||
'sum': 9.009999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659351600.0,
|
||||
'start': 1659348000.0,
|
||||
'state': 0.14,
|
||||
'sum': 9.149999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659355200.0,
|
||||
'start': 1659351600.0,
|
||||
'state': 0.19,
|
||||
'sum': 9.339999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659358800.0,
|
||||
'start': 1659355200.0,
|
||||
'state': 0.23,
|
||||
'sum': 9.569999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659362400.0,
|
||||
'start': 1659358800.0,
|
||||
'state': 0.18,
|
||||
'sum': 9.749999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659366000.0,
|
||||
'start': 1659362400.0,
|
||||
'state': 0.44,
|
||||
'sum': 10.189999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659369600.0,
|
||||
'start': 1659366000.0,
|
||||
'state': 0.35,
|
||||
'sum': 10.539999999999997,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659373200.0,
|
||||
'start': 1659369600.0,
|
||||
'state': 0.47,
|
||||
'sum': 11.009999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659376800.0,
|
||||
'start': 1659373200.0,
|
||||
'state': 0.37,
|
||||
'sum': 11.379999999999997,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659380400.0,
|
||||
'start': 1659376800.0,
|
||||
'state': 0.13,
|
||||
'sum': 11.509999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659384000.0,
|
||||
'start': 1659380400.0,
|
||||
'state': 0.11,
|
||||
'sum': 11.619999999999997,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659387600.0,
|
||||
'start': 1659384000.0,
|
||||
'state': 0.9,
|
||||
'sum': 12.519999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659391200.0,
|
||||
'start': 1659387600.0,
|
||||
'state': 0.52,
|
||||
'sum': 13.039999999999997,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659394800.0,
|
||||
'start': 1659391200.0,
|
||||
'state': 0.79,
|
||||
'sum': 13.829999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659398400.0,
|
||||
'start': 1659394800.0,
|
||||
'state': 0.43,
|
||||
'sum': 14.259999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659402000.0,
|
||||
'start': 1659398400.0,
|
||||
'state': 0.71,
|
||||
'sum': 14.969999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659405600.0,
|
||||
'start': 1659402000.0,
|
||||
'state': 0.63,
|
||||
'sum': 15.6,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659409200.0,
|
||||
'start': 1659405600.0,
|
||||
'state': 0.54,
|
||||
'sum': 16.14,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659412800.0,
|
||||
'start': 1659409200.0,
|
||||
'state': 0.51,
|
||||
'sum': 16.650000000000002,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659416400.0,
|
||||
'start': 1659412800.0,
|
||||
'state': 0.4,
|
||||
'sum': 17.05,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659420000.0,
|
||||
'start': 1659416400.0,
|
||||
'state': 0.49,
|
||||
'sum': 17.54,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659423600.0,
|
||||
'start': 1659420000.0,
|
||||
'state': 0.41,
|
||||
'sum': 17.95,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_coordinator_subsequent_run
|
||||
defaultdict({
|
||||
'srp_energy:123456789_energy_consumption': list([
|
||||
dict({
|
||||
'end': 1659315600.0,
|
||||
'start': 1659312000.0,
|
||||
'state': 2.6,
|
||||
'sum': 2.6,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659319200.0,
|
||||
'start': 1659315600.0,
|
||||
'state': 4.5,
|
||||
'sum': 7.1,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659322800.0,
|
||||
'start': 1659319200.0,
|
||||
'state': 2.5,
|
||||
'sum': 9.6,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659326400.0,
|
||||
'start': 1659322800.0,
|
||||
'state': 2.9,
|
||||
'sum': 12.5,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659330000.0,
|
||||
'start': 1659326400.0,
|
||||
'state': 2.2,
|
||||
'sum': 14.7,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659333600.0,
|
||||
'start': 1659330000.0,
|
||||
'state': 2.1,
|
||||
'sum': 16.8,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659337200.0,
|
||||
'start': 1659333600.0,
|
||||
'state': 2.0,
|
||||
'sum': 18.8,
|
||||
}),
|
||||
]),
|
||||
'srp_energy:123456789_energy_cost': list([
|
||||
dict({
|
||||
'end': 1659315600.0,
|
||||
'start': 1659312000.0,
|
||||
'state': 0.37,
|
||||
'sum': 0.37,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659319200.0,
|
||||
'start': 1659315600.0,
|
||||
'state': 0.64,
|
||||
'sum': 1.01,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659322800.0,
|
||||
'start': 1659319200.0,
|
||||
'state': 0.35,
|
||||
'sum': 1.3599999999999999,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659326400.0,
|
||||
'start': 1659322800.0,
|
||||
'state': 0.42,
|
||||
'sum': 1.7799999999999998,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659330000.0,
|
||||
'start': 1659326400.0,
|
||||
'state': 0.32,
|
||||
'sum': 2.0999999999999996,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659333600.0,
|
||||
'start': 1659330000.0,
|
||||
'state': 0.3,
|
||||
'sum': 2.3999999999999995,
|
||||
}),
|
||||
dict({
|
||||
'end': 1659337200.0,
|
||||
'start': 1659333600.0,
|
||||
'state': 0.28,
|
||||
'sum': 2.6799999999999997,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
@@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.components.srp_energy.const import CONF_IS_TOU, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
@@ -33,7 +34,7 @@ from tests.common import MockConfigEntry
|
||||
|
||||
@pytest.mark.usefixtures("mock_srp_energy_config_flow")
|
||||
async def test_show_form(
|
||||
hass: HomeAssistant, capsys: pytest.CaptureFixture[str]
|
||||
recorder_mock: Recorder, hass: HomeAssistant, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""Test show configuration form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -69,6 +70,7 @@ async def test_show_form(
|
||||
|
||||
|
||||
async def test_form_invalid_account(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
mock_srp_energy_config_flow: MagicMock,
|
||||
) -> None:
|
||||
@@ -88,6 +90,7 @@ async def test_form_invalid_account(
|
||||
|
||||
|
||||
async def test_form_invalid_auth(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
mock_srp_energy_config_flow: MagicMock,
|
||||
) -> None:
|
||||
@@ -107,6 +110,7 @@ async def test_form_invalid_auth(
|
||||
|
||||
|
||||
async def test_form_unknown_error(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
mock_srp_energy_config_flow: MagicMock,
|
||||
) -> None:
|
||||
@@ -126,7 +130,7 @@ async def test_form_unknown_error(
|
||||
|
||||
|
||||
async def test_flow_entry_already_configured(
|
||||
hass: HomeAssistant, init_integration: MockConfigEntry
|
||||
recorder_mock: Recorder, hass: HomeAssistant, init_integration: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test user input for config_entry that already exists."""
|
||||
# Verify mock config setup from fixture
|
||||
@@ -150,7 +154,7 @@ async def test_flow_entry_already_configured(
|
||||
|
||||
|
||||
async def test_flow_multiple_configs(
|
||||
hass: HomeAssistant, init_integration: MockConfigEntry
|
||||
recorder_mock: Recorder, hass: HomeAssistant, init_integration: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test multiple config entries."""
|
||||
# Verify mock config setup from fixture
|
||||
@@ -183,7 +187,7 @@ async def test_flow_multiple_configs(
|
||||
|
||||
|
||||
async def test_reconfigure(
|
||||
hass: HomeAssistant, init_integration: MockConfigEntry
|
||||
recorder_mock: Recorder, hass: HomeAssistant, init_integration: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test reconfiguring an existing entry."""
|
||||
|
||||
@@ -216,6 +220,7 @@ async def test_reconfigure(
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_reconfigure_error(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_srp_energy_config_flow: MagicMock,
|
||||
@@ -262,6 +267,7 @@ async def test_reconfigure_error(
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_reconfigure_unknown_error(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_srp_energy_config_flow: MagicMock,
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
"""Tests for Srp Energy component coordinator."""
|
||||
|
||||
from datetime import datetime as dt
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import freeze_time
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.components.recorder.statistics import (
|
||||
StatisticsRow,
|
||||
statistics_during_period,
|
||||
)
|
||||
from homeassistant.components.srp_energy import SRPEnergyDataUpdateCoordinator
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import PHOENIX_ZONE_INFO
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.recorder.common import async_wait_recording_done
|
||||
|
||||
|
||||
@freeze_time("2022-08-02T12:00:00-07:00")
|
||||
async def test_coordinator_first_run(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_srp_energy: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the coordinator on its first run with no existing statistics.
|
||||
|
||||
Should import multiple days of statistics.
|
||||
"""
|
||||
coordinator = SRPEnergyDataUpdateCoordinator(
|
||||
hass, mock_config_entry, mock_srp_energy
|
||||
)
|
||||
|
||||
await coordinator._async_update_data()
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
# Check stats for electric account '123456789'
|
||||
stats = await get_stats(hass)
|
||||
assert stats == snapshot
|
||||
|
||||
|
||||
async def test_coordinator_subsequent_run(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_srp_energy: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the coordinator correctly updates statistics on subsequent runs."""
|
||||
coordinator = SRPEnergyDataUpdateCoordinator(
|
||||
hass, mock_config_entry, mock_srp_energy
|
||||
)
|
||||
|
||||
mock_srp_energy.usage.side_effect = None
|
||||
mock_srp_energy.usage.return_value = [
|
||||
("7/31/2022", "05:00 PM", "2022-07-31T17:00:00", 2.6, 0.37),
|
||||
("7/31/2022", "06:00 PM", "2022-07-31T18:00:00", 4.5, 0.64),
|
||||
("7/31/2022", "07:00 PM", "2022-07-31T19:00:00", 2.2, 0.32),
|
||||
("7/31/2022", "08:00 PM", "2022-07-31T20:00:00", 0.0, 0.0),
|
||||
("7/31/2022", "09:00 PM", "2022-07-31T21:00:00", 0.0, 0.0),
|
||||
("7/31/2022", "10:00 PM", "2022-07-31T22:00:00", 0.0, 0.0),
|
||||
("7/31/2022", "11:00 PM", "2022-07-31T23:00:00", 0.0, 0.0),
|
||||
]
|
||||
|
||||
# Run aug 2nd at midnight to import through aug 1
|
||||
# BUT tends to be missing last few hours
|
||||
with freeze_time("2022-08-02T00:00:00-07:00"):
|
||||
await coordinator._async_update_data()
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
stats = await get_stats(hass)
|
||||
last_consumption = stats["srp_energy:123456789_energy_consumption"][-1]
|
||||
assert_stat(last_consumption, "2022-07-31T19:00:00-07:00", 2.2, 9.3)
|
||||
last_cost = stats["srp_energy:123456789_energy_cost"][-1]
|
||||
assert_stat(last_cost, "2022-07-31T19:00:00-07:00", 0.32, 1.33)
|
||||
|
||||
mock_srp_energy.usage.return_value = [
|
||||
("7/31/2022", "05:00 PM", "2022-07-31T17:00:00", 2.6, 0.37),
|
||||
("7/31/2022", "06:00 PM", "2022-07-31T18:00:00", 4.5, 0.64),
|
||||
("7/31/2022", "07:00 PM", "2022-07-31T19:00:00", 2.5, 0.35), # was 2.2 / 0.32
|
||||
("7/31/2022", "08:00 PM", "2022-07-31T20:00:00", 2.9, 0.42), # was 0 / 0
|
||||
("7/31/2022", "09:00 PM", "2022-07-31T21:00:00", 2.2, 0.32), # was 0 / 0
|
||||
("7/31/2022", "10:00 PM", "2022-07-31T22:00:00", 2.1, 0.30), # was 0 / 0
|
||||
("7/31/2022", "11:00 PM", "2022-07-31T23:00:00", 2.0, 0.28), # was 0 / 0
|
||||
]
|
||||
# Run aug 2nd at 5am to get the updated data for aug 1
|
||||
with freeze_time("2022-08-02T05:00:00-07:00"):
|
||||
await coordinator._async_update_data()
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
stats = await get_stats(hass)
|
||||
last_consumption = stats["srp_energy:123456789_energy_consumption"][-1]
|
||||
assert_stat(last_consumption, "2022-07-31T23:00:00-07:00", 2.0, 18.8)
|
||||
last_cost = stats["srp_energy:123456789_energy_cost"][-1]
|
||||
assert_stat(last_cost, "2022-07-31T23:00:00-07:00", 0.28, 2.68)
|
||||
assert stats == snapshot
|
||||
|
||||
|
||||
def assert_stat(
|
||||
stat: StatisticsRow, expected_start: str, expected_state: float, expected_sum: float
|
||||
) -> None:
|
||||
"""Helper function to assert a single statistics row."""
|
||||
assert (
|
||||
dt.fromtimestamp(stat["start"], PHOENIX_ZONE_INFO).isoformat() == expected_start
|
||||
)
|
||||
assert round(stat["state"], 2) == expected_state
|
||||
assert round(stat["sum"], 2) == expected_sum
|
||||
|
||||
|
||||
async def test_coordinator_statistics_gap(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_srp_energy: MagicMock,
|
||||
) -> None:
|
||||
"""Test coordinator handles a gap between stored stats and SRP's available data.
|
||||
|
||||
When SRP's 30-day API window rolls past the period covered by stored statistics,
|
||||
statistics_during_period finds nothing at hourly_usage[0].start_time. The else
|
||||
branch should fall back to the last known running sums and continue the totals
|
||||
from there rather than restarting from zero.
|
||||
"""
|
||||
coordinator = SRPEnergyDataUpdateCoordinator(
|
||||
hass, mock_config_entry, mock_srp_energy
|
||||
)
|
||||
|
||||
# First run: establish two stat entries so get_last_statistics returns 2 records.
|
||||
mock_srp_energy.usage.side_effect = None
|
||||
mock_srp_energy.usage.return_value = [
|
||||
("7/31/2022", "05:00 PM", "2022-07-31T17:00:00", 2.0, 0.20),
|
||||
("7/31/2022", "06:00 PM", "2022-07-31T18:00:00", 3.0, 0.30),
|
||||
]
|
||||
|
||||
with freeze_time("2022-08-01T12:00:00-07:00"):
|
||||
await coordinator._async_update_data()
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
stats = await get_stats(hass)
|
||||
assert_stat(
|
||||
stats["srp_energy:123456789_energy_consumption"][-1],
|
||||
"2022-07-31T18:00:00-07:00",
|
||||
3.0,
|
||||
5.0,
|
||||
)
|
||||
assert_stat(
|
||||
stats["srp_energy:123456789_energy_cost"][-1],
|
||||
"2022-07-31T18:00:00-07:00",
|
||||
0.30,
|
||||
0.50,
|
||||
)
|
||||
|
||||
# Second run: SRP's API window has advanced so it only returns data starting
|
||||
# AFTER the most recent stored stat (18:00). statistics_during_period will find
|
||||
# nothing at 20:00 or later, triggering the else branch.
|
||||
mock_srp_energy.usage.return_value = [
|
||||
("7/31/2022", "08:00 PM", "2022-07-31T20:00:00", 4.0, 0.40),
|
||||
("7/31/2022", "09:00 PM", "2022-07-31T21:00:00", 5.0, 0.50),
|
||||
]
|
||||
|
||||
with freeze_time("2022-09-01T12:00:00-07:00"):
|
||||
await coordinator._async_update_data()
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
stats = await get_stats(hass)
|
||||
# Sums must continue from the last stored values (5.0 kWh / $0.50), not restart.
|
||||
assert_stat(
|
||||
stats["srp_energy:123456789_energy_consumption"][-2],
|
||||
"2022-07-31T20:00:00-07:00",
|
||||
4.0,
|
||||
9.0,
|
||||
)
|
||||
assert_stat(
|
||||
stats["srp_energy:123456789_energy_consumption"][-1],
|
||||
"2022-07-31T21:00:00-07:00",
|
||||
5.0,
|
||||
14.0,
|
||||
)
|
||||
assert_stat(
|
||||
stats["srp_energy:123456789_energy_cost"][-2],
|
||||
"2022-07-31T20:00:00-07:00",
|
||||
0.40,
|
||||
0.90,
|
||||
)
|
||||
assert_stat(
|
||||
stats["srp_energy:123456789_energy_cost"][-1],
|
||||
"2022-07-31T21:00:00-07:00",
|
||||
0.50,
|
||||
1.40,
|
||||
)
|
||||
|
||||
|
||||
async def test_coordinator_subsequent_run_no_energy_data(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_srp_energy: MagicMock,
|
||||
) -> None:
|
||||
"""Test the coordinator handles no recent usage/cost data."""
|
||||
coordinator = SRPEnergyDataUpdateCoordinator(
|
||||
hass, mock_config_entry, mock_srp_energy
|
||||
)
|
||||
await coordinator._async_update_data()
|
||||
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
|
||||
async def get_stats(hass: HomeAssistant) -> dict[str, list[StatisticsRow]]:
|
||||
"""Helper function to get the latest statistics."""
|
||||
return await hass.async_add_executor_job(
|
||||
statistics_during_period,
|
||||
hass,
|
||||
dt_util.utc_from_timestamp(0),
|
||||
None,
|
||||
{
|
||||
"srp_energy:123456789_energy_consumption",
|
||||
"srp_energy:123456789_energy_cost",
|
||||
},
|
||||
"hour",
|
||||
None,
|
||||
{"state", "sum"},
|
||||
)
|
||||
@@ -1,15 +1,20 @@
|
||||
"""Tests for Srp Energy component Init."""
|
||||
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def test_setup_entry(hass: HomeAssistant, init_integration) -> None:
|
||||
async def test_setup_entry(
|
||||
recorder_mock: Recorder, hass: HomeAssistant, init_integration
|
||||
) -> None:
|
||||
"""Test setup entry."""
|
||||
assert init_integration.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_unload_entry(hass: HomeAssistant, init_integration) -> None:
|
||||
async def test_unload_entry(
|
||||
recorder_mock: Recorder, hass: HomeAssistant, init_integration
|
||||
) -> None:
|
||||
"""Test being able to unload an entry."""
|
||||
assert init_integration.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from unittest.mock import patch
|
||||
|
||||
from requests.models import HTTPError
|
||||
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
@@ -17,7 +18,9 @@ from homeassistant.core import HomeAssistant
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_loading_sensors(hass: HomeAssistant, init_integration) -> None:
|
||||
async def test_loading_sensors(
|
||||
recorder_mock: Recorder, hass: HomeAssistant, init_integration
|
||||
) -> None:
|
||||
"""Test the srp energy sensors."""
|
||||
# Validate the Config Entry was initialized
|
||||
assert init_integration.state is ConfigEntryState.LOADED
|
||||
@@ -26,10 +29,12 @@ async def test_loading_sensors(hass: HomeAssistant, init_integration) -> None:
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
|
||||
async def test_srp_entity(hass: HomeAssistant, init_integration) -> None:
|
||||
async def test_srp_entity(
|
||||
recorder_mock: Recorder, hass: HomeAssistant, init_integration
|
||||
) -> None:
|
||||
"""Test the SrpEntity."""
|
||||
usage_state = hass.states.get("sensor.srp_energy_mock_title_energy_usage")
|
||||
assert usage_state.state == "150.8"
|
||||
assert usage_state.state == "67.4"
|
||||
|
||||
# Validate attributions
|
||||
assert (
|
||||
@@ -45,6 +50,7 @@ async def test_srp_entity(hass: HomeAssistant, init_integration) -> None:
|
||||
|
||||
|
||||
async def test_srp_entity_update_failed(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
@@ -66,6 +72,7 @@ async def test_srp_entity_update_failed(
|
||||
|
||||
|
||||
async def test_srp_entity_timeout(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user