Skip to content

Commit

Permalink
feat: use historical sensor for cost and usage
Browse files Browse the repository at this point in the history
  • Loading branch information
root0x committed Jan 23, 2025
1 parent 98cc0e4 commit d186c64
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 46 deletions.
3 changes: 2 additions & 1 deletion custom_components/hildebrandglow_dcc/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/HandyHat/ha-hildebrandglow-dcc/issues",
"requirements": [
"pyglowmarkt==0.5.5"
"pyglowmarkt==0.5.5",
"homeassistant-historical-sensor==2.0.0rc4"
],
"version": "1.0.3"
}
204 changes: 159 additions & 45 deletions custom_components/hildebrandglow_dcc/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@
DataUpdateCoordinator,
)


from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfEnergy
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dtutil

from homeassistant_historical_sensor import (
HistoricalSensor,
HistoricalState,
PollUpdateMixin,
)

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -143,15 +157,10 @@ async def should_update() -> bool:

async def daily_data(hass: HomeAssistant, resource) -> float:
"""Get daily usage from the API."""
# If it's before 01:06, we need to fetch yesterday's data
# Should only need to be before 00:36 but gas data can be 30 minutes behind electricity data
if datetime.now().time() <= time(1, 5):
_LOGGER.debug("Fetching yesterday's data")
now = datetime.now() - timedelta(days=1)
else:
now = datetime.now()
# Always pull down the last 6 hours of readings
now = datetime.now()
# Round to the day to set time to 00:00:00
t_from = await hass.async_add_executor_job(resource.round, now, "P1D")
t_from = await hass.async_add_executor_job(resource.round, datetime.now() - timedelta(hours=6), "P1D")
# Round to the minute
t_to = await hass.async_add_executor_job(resource.round, now, "PT1M")

Expand All @@ -176,20 +185,11 @@ async def daily_data(hass: HomeAssistant, resource) -> float:
_LOGGER.exception("Unexpected exception: %s. Please open an issue", ex)

try:
_LOGGER.debug(
"Get readings from %s to %s for %s", t_from, t_to, resource.classifier
)
readings = await hass.async_add_executor_job(
resource.get_readings, t_from, t_to, "P1D", "sum", True
resource.get_readings, t_from, t_to, "PT30M", "sum", True
)
_LOGGER.debug("Successfully got daily usage for resource id %s", resource.id)
_LOGGER.debug(
"Readings for %s has %s entries", resource.classifier, len(readings)
)
v = readings[0][1].value
if len(readings) > 1:
v += readings[1][1].value
return v
return readings
except requests.Timeout as ex:
_LOGGER.error("Timeout: %s", ex)
except requests.exceptions.ConnectionError as ex:
Expand Down Expand Up @@ -236,14 +236,13 @@ async def tariff_data(hass: HomeAssistant, resource) -> float:
return None


class Usage(SensorEntity):
class Usage(PollUpdateMixin, HistoricalSensor, SensorEntity):
"""Sensor object for daily usage."""

_attr_device_class = SensorDeviceClass.ENERGY
_attr_has_entity_name = True
_attr_name = "Usage (today)"
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING

def __init__(self, hass: HomeAssistant, resource, virtual_entity) -> None:
"""Initialize the sensor."""
Expand All @@ -254,6 +253,9 @@ def __init__(self, hass: HomeAssistant, resource, virtual_entity) -> None:
self.resource = resource
self.virtual_entity = virtual_entity

async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()

@property
def device_info(self) -> DeviceInfo:
"""Return device information."""
Expand All @@ -271,30 +273,84 @@ def icon(self) -> str | None:
if self.resource.classifier == "gas.consumption":
return "mdi:fire"

async def async_update(self) -> None:
async def async_update_historical(self) -> None:
"""Fetch new data for the sensor."""
# Get data on initial startup
if not self.initialised:
value = await daily_data(self.hass, self.resource)
if value:
self._attr_native_value = round(value, 2)
self.initialised = True
else:
# Only update the sensor if it's between 0-5 or 30-35 minutes past the hour
if await should_update():
value = await daily_data(self.hass, self.resource)
if value:
self._attr_native_value = round(value, 2)
readings = await daily_data(self.hass, self.resource)
hist_states = []
for reading in readings:
hist_states.append(HistoricalState( # noqa: PERF401
state = reading[1].value,
dt = dtutil.as_local(reading[0])
))
self._attr_historical_states = hist_states
self.initialised = True
elif await should_update():
# Only update the sensor if it's between 1-5 or 31-35minutes past the hour
readings = await daily_data(self.hass, self.resource)
hist_states = []
for reading in readings:
hist_states.append(HistoricalState( # noqa: PERF401
state = reading[1].value,
dt = reading[0]
))
self._attr_historical_states = hist_states

def get_statistic_metadata(self) -> StatisticMetaData:
meta = super().get_statistic_metadata()
meta["has_sum"] = True
meta["has_mean"] = True

return meta

async def async_calculate_statistic_data(
self, hist_states: list[HistoricalState], *, latest: dict | None = None
) -> list[StatisticData]:
#
# Group historical states by hour
# Calculate sum, mean, etc...
#

accumulated = latest["sum"] if latest else 0

def hour_block_for_hist_state(hist_state: HistoricalState) -> datetime:
# XX:00:00 states belongs to previous hour block
if hist_state.dt.minute == 0 and hist_state.dt.second == 0:
dt = hist_state.dt - timedelta(hours=1)
return dt.replace(minute=0, second=0, microsecond=0)

else:
return hist_state.dt.replace(minute=0, second=0, microsecond=0)

ret = []
for dt, collection_it in itertools.groupby(
hist_states, key=hour_block_for_hist_state
):
collection = list(collection_it)
mean = statistics.mean([x.state for x in collection])
partial_sum = sum([x.state for x in collection])
accumulated = accumulated + partial_sum

ret.append(
StatisticData(
start=dt,
state=partial_sum,
mean=mean,
sum=accumulated,
)
)

class Cost(SensorEntity):
return ret


class Cost(PollUpdateMixin, HistoricalSensor, SensorEntity):
"""Sensor usage for daily cost."""

_attr_device_class = SensorDeviceClass.MONETARY
_attr_has_entity_name = True
_attr_name = "Cost (today)"
_attr_native_unit_of_measurement = "GBP"
_attr_state_class = SensorStateClass.TOTAL_INCREASING

def __init__(self, hass: HomeAssistant, resource, virtual_entity) -> None:
"""Initialize the sensor."""
Expand All @@ -306,6 +362,9 @@ def __init__(self, hass: HomeAssistant, resource, virtual_entity) -> None:
self.resource = resource
self.virtual_entity = virtual_entity

async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()

@property
def device_info(self) -> DeviceInfo:
"""Return device information."""
Expand All @@ -317,20 +376,75 @@ def device_info(self) -> DeviceInfo:
name=device_name(self.resource, self.virtual_entity),
)

async def async_update(self) -> None:
async def async_update_historical(self) -> None:
"""Fetch new data for the sensor."""
# Get data on initial startup
if not self.initialised:
value = await daily_data(self.hass, self.resource)
if value:
self._attr_native_value = round(value / 100, 2)
self.initialised = True
else:
# Only update the sensor if it's between 0-5 or 30-35 minutes past the hour
if await should_update():
value = await daily_data(self.hass, self.resource)
if value:
self._attr_native_value = round(value / 100, 2)
readings = await daily_data(self.hass, self.resource)
hist_states = []
for reading in readings:
hist_states.append(HistoricalState( # noqa: PERF401
state = reading[1].value,
dt = dtutil.as_local(reading[0])
))
self._attr_historical_states = hist_states
self.initialised = True
elif await should_update():
# Only update the sensor if it's between 1-5 or 31-35minutes past the hour
readings = await daily_data(self.hass, self.resource)
hist_states = []
for reading in readings:
hist_states.append(HistoricalState( # noqa: PERF401
state = reading[1].value,
dt = reading[0]
))
self._attr_historical_states = hist_states

def get_statistic_metadata(self) -> StatisticMetaData:
meta = super().get_statistic_metadata()
meta["has_sum"] = True
meta["has_mean"] = True

return meta

async def async_calculate_statistic_data(
self, hist_states: list[HistoricalState], *, latest: dict | None = None
) -> list[StatisticData]:
#
# Group historical states by hour
# Calculate sum, mean, etc...
#

accumulated = latest["sum"] if latest else 0

def hour_block_for_hist_state(hist_state: HistoricalState) -> datetime:
# XX:00:00 states belongs to previous hour block
if hist_state.dt.minute == 0 and hist_state.dt.second == 0:
dt = hist_state.dt - timedelta(hours=1)
return dt.replace(minute=0, second=0, microsecond=0)

else:
return hist_state.dt.replace(minute=0, second=0, microsecond=0)

ret = []
for dt, collection_it in itertools.groupby(
hist_states, key=hour_block_for_hist_state
):
collection = list(collection_it)
mean = statistics.mean([x.state for x in collection])
partial_sum = sum([x.state for x in collection])
accumulated = accumulated + partial_sum

ret.append(
StatisticData(
start=dt,
state=partial_sum,
mean=mean,
sum=accumulated,
)
)

return ret

class TariffCoordinator(DataUpdateCoordinator):
"""Data update coordinator for the tariff sensors."""
Expand Down

10 comments on commit d186c64

@jonandel
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this ! I tried it yesterday, but today Im getting no data whatso ever (11am).
The Hildbrand API hasnt been consistent at returning data lately...particularly for the Gas data... but Im judging from lack of data in HA.

Logs seem to indicate that all the API GET's are being returned, but somehow its not getting into the HA sensor... hmmm

@root0x
Copy link
Author

@root0x root0x commented on d186c64 Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible you send me some of your logs? Also if you can also add a log statement for the readings variable in the daily_data method that would also be useful

@RMMTSLLP
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi root0x,

Haven't done much trouble shooting before could you give me a idiots guide to sending you the logs required. Also what do you mean by adding log statement.

Sorry .....

@root0x
Copy link
Author

@root0x root0x commented on d186c64 Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give this branch a go https://github.com/root0x/ha-hildebrandglow-dcc/tree/feat/use-historical-sensor it contains some of the other fixes that other people have made like which times the script runs.

@RMMTSLLP
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certainly I'll load it up later and report back. Many thanks.

@RMMTSLLP
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Daniel,

I loaded up this latest version as requested. Unfortunately for ; Electricity Used Today.Electricity Cost Today, Gas Used Today & Gas Cost Today Sensor all I see is "Unknown" Prior to uploading this version I tried your version "5 Commits ahead of" the sensors were reporting for that version, there still were issues as initially described in #412 the usage sensors "utility meter helpers" have have not updated since the latest vesion was uploaded due to no data appearing.

Let me know if you require further information, I'm quite happy to carry on testing. Screenshots from the intergration/device screen below

....... and thankyou for asssisting.

Are there not many of us using this integration ?

Image
Image

@root0x
Copy link
Author

@root0x root0x commented on d186c64 Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, so those will unfortunatley have to show as unknown because of the ha-historial-sensor that is used, the graphs should be correct though. I just use the energy dashboard and that isn't a problem for me. Please see this limitation https://github.com/ldotlopez/ha-historical-sensor

@RMMTSLLP
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update I'll monitor over the next few days many thanks.

@RMMTSLLP
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Daniel, Data seems to be coming in much better. There seemed to be a couple of hours where the data was incorrect, I'll monitor this and report back.

A new issue has been found that in the energy dashboard it no longer reports the Energy Cost looks like its missing the "Energy Used Today" mentioned above which looks to be used in the calculation. Do you see the same? Is there a work around?

Image
Image

@RMMTSLLP
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Daniel, Please disregard the last email, I have reloaded the integration all of the sensors now show. I'll update you once some more data has been imported. Sorry ......... :-(

Please sign in to comment.