Skip to content

Commit

Permalink
Merge branch 'main' of github.com:ldotlopez/ha-ideenergy
Browse files Browse the repository at this point in the history
  • Loading branch information
ldotlopez committed Jul 13, 2023
2 parents b1b0acb + b41f588 commit c5c814d
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 67 deletions.
3 changes: 2 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ name = "pypi"
[packages]

[dev-packages]
homeassistant = ">= 2023.6.1"
ipython = "*"
ipdb = "*"
homeassistant-historical-sensor = {editable = true, path = "./../ha-historical-sensor"}
sqlalchemy = "*"
pre-commit = "*"
homeassistant = ">= 2023.6.0"

[requires]
python_version = "3"
3 changes: 2 additions & 1 deletion config/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ logger:
custom_components.ideenergy.datacoordinator: debug
custom_components.ideenergy.sensor: debug
custom_components.ideenergy.updates: debug
homeassistant_historical_sensor: debug
custom_components.ideenergy.fixes: debug
homeassistant_historical_sensor: debug
3 changes: 1 addition & 2 deletions custom_components/ideenergy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,14 @@
import math
from datetime import timedelta

import ideenergy
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceInfo

import ideenergy

from .barrier import TimeDeltaBarrier, TimeWindowBarrier # NoopBarrier,
from .const import (
API_USER_SESSION_TIMEOUT,
Expand Down
3 changes: 1 addition & 2 deletions custom_components/ideenergy/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@
import os
from typing import Any

import ideenergy
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback # noqa: F401
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_create_clientsession

import ideenergy

from . import _LOGGER
from .const import CONF_CONTRACT, CONFIG_ENTRY_VERSION, DOMAIN

Expand Down
3 changes: 1 addition & 2 deletions custom_components/ideenergy/datacoordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@
from datetime import datetime, timedelta, timezone
from typing import Any

import ideenergy
from homeassistant.core import dt_util
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

import ideenergy

from .barrier import Barrier, BarrierDeniedError
from .const import (
DATA_ATTR_HISTORICAL_CONSUMPTION,
Expand Down
16 changes: 10 additions & 6 deletions custom_components/ideenergy/manifest.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
{
"domain": "ideenergy",
"name": "i-DE Energy Monitor",
"codeowners": ["@ldotlopez"],
"codeowners": [
"@ldotlopez"
],
"config_flow": true,
"dependencies": ["recorder"],
"dependencies": [
"recorder"
],
"documentation": "https://github.com/ldotlopez/ha-ideenergy",
"integration_type": "device",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/ldotlopez/ha-ideenergy/issues",
"requirements": [
"ideenergy==1.0.0",
"homeassistant-historical-sensor==1.0.1"
],
"ideenergy==1.0.0",
"homeassistant-historical-sensor==2.0.0b0"
],
"version": "2.0.1"
}
}
128 changes: 77 additions & 51 deletions custom_components/ideenergy/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,44 +27,34 @@

import itertools
import logging

# import statistics
from collections.abc import Callable
from datetime import datetime, timedelta
from typing import Any, Callable
from typing import Any

from homeassistant.components import recorder
from homeassistant.components.recorder import statistics
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.components.recorder.models import (StatisticData,
StatisticMetaData)
from homeassistant.components.sensor import (SensorDeviceClass, SensorEntity,
SensorStateClass)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfEnergy,
UnitOfPower,
)
from homeassistant.const import (STATE_UNAVAILABLE, STATE_UNKNOWN,
UnitOfEnergy, UnitOfPower)
from homeassistant.core import HomeAssistant, callback, dt_util
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import DiscoveryInfoType
from homeassistant.util import dt as dtutil
from homeassistant_historical_sensor import HistoricalSensor, HistoricalState

from . import fixes
from .const import DOMAIN
from .datacoordinator import (
DATA_ATTR_HISTORICAL_CONSUMPTION,
DATA_ATTR_HISTORICAL_GENERATION,
DATA_ATTR_HISTORICAL_POWER_DEMAND,
DATA_ATTR_MEASURE_ACCUMULATED,
DATA_ATTR_MEASURE_INSTANT,
DataSetType,
)
from .datacoordinator import (DATA_ATTR_HISTORICAL_CONSUMPTION,
DATA_ATTR_HISTORICAL_GENERATION,
DATA_ATTR_HISTORICAL_POWER_DEMAND,
DATA_ATTR_MEASURE_ACCUMULATED,
DATA_ATTR_MEASURE_INSTANT, DataSetType)
from .entity import IDeEntity
from .fixes import async_fix_statistics

PLATFORM = "sensor"

Expand Down Expand Up @@ -97,21 +87,28 @@ def async_update_historical(self) -> None:

class StatisticsMixin(HistoricalSensor):
@property
def statatistic_id(self):
def statistic_id(self):
return self.entity_id

def get_statatistic_metadata(self) -> StatisticMetaData:
meta = super().get_statatistic_metadata() | {"has_sum": True}
def get_statistic_metadata(self) -> StatisticMetaData:
meta = super().get_statistic_metadata() | {"has_sum": True}

return meta

async def async_added_to_hass(self):
await super().async_added_to_hass()
await fixes.async_fix_statistics(self.hass, self.get_statatistic_metadata())

#
# In 2.0 branch we f**ked statistiscs.
# Don't set state_class attributes for historical sensors!
#
# FIXME: Remove in future 3.0 series.
#
await async_fix_statistics(self.hass, self.get_statistic_metadata())

async def async_calculate_statistic_data(
self, hist_states: list[HistoricalState], *, latest: dict | None
) -> list[StatisticData]:

#
# Filter out invalid states
#
Expand All @@ -120,7 +117,7 @@ async def async_calculate_statistic_data(
hist_states = [x for x in hist_states if x.state not in (0, None)]
if len(hist_states) != n_original_hist_states:
_LOGGER.warning(
f"{self.statatistic_id}: "
f"{self.statistic_id}: "
+ "found some weird values in historical statistics"
)

Expand All @@ -137,13 +134,6 @@ def hour_block_for_hist_state(hist_state: HistoricalState) -> datetime:
else:
return hist_state.dt.replace(minute=0, second=0, microsecond=0)

#
# Somehow, somewhere, Home Assistant writes invalid statistics
# FIXME: integrate into homeassistant_historical_sensor and remove
#

await fixes.async_fix_statistics(self.hass, self.get_statatistic_metadata())

#
# Ignore supplied 'lastest' and fetch again from recorder
# FIXME: integrate into homeassistant_historical_sensor and remove
Expand All @@ -153,22 +143,31 @@ def get_last_statistics():
ret = statistics.get_last_statistics(
self.hass,
1,
self.statatistic_id,
self.statistic_id,
convert_units=True,
types={"last_reset", "max", "mean", "min", "state", "sum"},
types={"sum"},
)
if ret is None:

# ret can be none or {}
if not ret:
return None

try:
return ret[self.statatistic_id][0]
except (KeyError, IndexError):
_LOGGER.debug(
f"{self.statatistic_id}: [bug] found last statistics but doesn't "
+ f"have matching key or values: {ret!r}"
)
return ret[self.statistic_id][0]

except KeyError:
# No stats found
return None

except IndexError:
# What?
_LOGGER.error(
f"{self.statatistic_id}: "
+ "[bug] found last statistics key but doesn't have any value! "
+ f"({ret!r})"
)
raise

latest = await recorder.get_instance(self.hass).async_add_executor_job(
get_last_statistics
)
Expand All @@ -183,7 +182,7 @@ def extract_last_sum(latest) -> float:
total_accumulated = extract_last_sum(latest)
except (KeyError, ValueError):
_LOGGER.error(
f"{self.statatistic_id}: [bug] statistics broken (lastest={latest!r})"
f"{self.statistic_id}: [bug] statistics broken (lastest={latest!r})"
)
return []

Expand All @@ -192,7 +191,7 @@ def extract_last_sum(latest) -> float:
)

_LOGGER.debug(
f"{self.statatistic_id}: "
f"{self.statistic_id}: "
+ f"calculating statistics using {total_accumulated} as base accumulated "
+ f"(registed at {start_point_local_dt})"
)
Expand Down Expand Up @@ -240,6 +239,10 @@ def __init__(self, *args, **kwargs):
# possible, state class total_increasing or total with last_reset should only be
# used when state class total without last_reset does not work for the sensor.
# https://developers.home-assistant.io/docs/core/entity/sensor/#how-to-choose-state_class-and-last_reset

# The sensor's value never resets, e.g. a lifetime total energy consumption or
# production: state_class total, last_reset not set or set to None

self._attr_state_class = SensorStateClass.TOTAL

@property
Expand Down Expand Up @@ -300,12 +303,24 @@ class HistoricalConsumption(

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self._attr_device_class = SensorDeviceClass.ENERGY
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
self._attr_entity_registry_enabled_default = False
self._attr_state = None

# The sensor's state is reset with every state update, for example a sensor
# updating every minute with the energy consumption during the past minute:
# state class total, last_reset updated every state change.
#
# (*) last_reset is set in states by historical_states_from_historical_api_data
# (*) set only in internal statistics model
#
# DON'T set for HistoricalSensors, you will mess your statistics.
# Keep as reference.
#
# self._attr_state_class = SensorStateClass.TOTAL

@property
def historical_states(self):
ret = historical_states_from_historical_api_data(
Expand All @@ -325,11 +340,23 @@ class HistoricalGeneration(
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._attr_device_class = SensorDeviceClass.ENERGY
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
self._attr_entity_registry_enabled_default = False
self._attr_state = None

# The sensor's state is reset with every state update, for example a sensor
# updating every minute with the energy consumption during the past minute:
# state class total, last_reset updated every state change.
#
# (*) last_reset is set in states by historical_states_from_historical_api_data
# (*) set only in internal statistics model
#
# DON'T set for HistoricalSensors, you will mess your statistics.
#
# Keep as reference.
#
# self._attr_state_class = SensorStateClass.TOTAL

@property
def historical_states(self):
ret = historical_states_from_historical_api_data(
Expand All @@ -347,7 +374,6 @@ class HistoricalPowerDemand(HistoricalSensorMixin, IDeEntity, SensorEntity):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._attr_device_class = SensorDeviceClass.POWER
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = UnitOfPower.WATT
self._attr_entity_registry_enabled_default = False
self._attr_state = None
Expand Down
5 changes: 3 additions & 2 deletions custom_components/ideenergy/updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@

import logging

from custom_components.ideenergy.const import DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry, entity_registry
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.util import slugify

from custom_components.ideenergy.const import DOMAIN

from .entity import IDeEntity
from .entity import _build_entity_unique_id as _build_entity_unique_id_v3
from .sensor import AccumulatedConsumption, HistoricalConsumption
Expand Down Expand Up @@ -150,7 +151,7 @@ def _update_entity_registry_v1(
("historical", HistoricalConsumption),
)

for (old_sensor_type, new_sensor_cls) in migrate:
for old_sensor_type, new_sensor_cls in migrate:
entity_id = er.async_get_entity_id(
"sensor",
"ideenergy",
Expand Down
12 changes: 12 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[build-system]
requires = ["setuptools>=40.8.0", "wheel"]
build-backend = "setuptools.build_meta"

[tool.black]
target-version = ['py310']

[tool.isort]
profile = "black"

[tool.mypy]
files = ["custom_components/ideenergy"]

0 comments on commit c5c814d

Please sign in to comment.