Skip to content

Commit

Permalink
Merge pull request #28 from EuleMitKeule/develop
Browse files Browse the repository at this point in the history
feat: improve glow energy calculation
  • Loading branch information
EuleMitKeule authored May 4, 2023
2 parents b4cd00c + f4b626a commit efb4b94
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 246 deletions.
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"python.linting.pylintArgs": [
"--extension-pkg-whitelist=pydantic",
],
"python.formatting.provider": "black",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
Expand All @@ -31,4 +30,7 @@
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.defaultProfile.osx": "zsh",
"terminal.integrated.defaultProfile.windows": "PowerShell",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
}
63 changes: 25 additions & 38 deletions estimenergy/devices/base_device.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Abstract class for all devices."""
from abc import ABC, abstractmethod
import datetime
from typing import Optional, Type

from estimenergy.const import Metric
from estimenergy.models.config.config import Config
from estimenergy.models.device_config import DeviceConfig, DeviceConfigRead
from estimenergy.models.device_config import DeviceConfig
from estimenergy.services.data_service import DataService
from estimenergy.services.influx_service import InfluxService
from estimenergy.services.prometheus_service import PrometheusService
Expand Down Expand Up @@ -33,66 +34,52 @@ def __init__(self, device_config: DeviceConfig, config: Config):
influx_service = InfluxService(self.device_config, config)
self.data_services.append(influx_service)

@property
@abstractmethod
def provided_metrics(self) -> list[Metric]:
"""Return a list of metrics provided by this device."""
async def start(self):
"""Start the device."""

@abstractmethod
async def stop(self):
"""Stop the device."""

@abstractmethod
async def start(self):
"""Start the device."""

async def increment(
async def write(
self,
metric: Metric,
value: float,
value_dt: datetime.datetime,
):
"""Increment a metric in the database."""

if metric not in self.provided_metrics:
raise ValueError(f"Metric {metric} not provided by this device.")
"""Write a metric to the database."""

for data_service in self.data_services:
await data_service.increment(metric, value, value_dt)
await data_service.write(metric, value, value_dt)

async def decrement(
await self.update(value_dt)

async def update(
self,
metric: Metric,
value: float,
value_dt: datetime.datetime,
):
"""Decrement a metric in the database."""

if metric not in self.provided_metrics:
raise ValueError(f"Metric {metric} not provided by this device.")
"""Calculate metrics based on other metrics."""

for data_service in self.data_services:
await data_service.decrement(metric, value, value_dt)
await data_service.update(value_dt)

async def write(
async def last(
self,
metric: Metric,
value: float,
value_dt: datetime.datetime,
):
"""Write a metric to the database."""
) -> Optional[float]:
"""Get the last value of a metric."""

if metric not in self.provided_metrics:
raise ValueError(f"Metric {metric} not provided by this device.")
sql_service: SqlService = next(
(
data_service
for data_service in self.data_services
if isinstance(data_service, SqlService)
),
)

for data_service in self.data_services:
await data_service.write(metric, value, value_dt)
if sql_service:
return await sql_service.last(metric, value_dt)

async def update(
self,
value_dt: datetime.datetime,
):
"""Calculate metrics based on other metrics."""

for data_service in self.data_services:
await data_service.update(value_dt)
return None
129 changes: 91 additions & 38 deletions estimenergy/devices/glow_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from aioesphomeapi import (
APIClient,
APIConnectionError,
DeviceInfo,
InvalidEncryptionKeyAPIError,
ReconnectLogic,
RequiresEncryptionAPIError,
Expand All @@ -25,14 +24,31 @@
from estimenergy.models.device_config import DeviceConfig


class EnergySplit:
energy_start: float
energy_last: float
time_start: datetime.datetime
time_last: datetime.datetime

def __init__(self, energy_start: float, time_start: datetime.datetime):
self.energy_start = energy_start
self.energy_last = energy_start
self.time_start = time_start
self.time_last = time_start

def update(self, energy: float, time: datetime.datetime):
self.energy_last = energy
self.time_last = time


class GlowDevice(BaseDevice):
"""Home Assistant Glow device."""

zeroconf: Zeroconf
api: APIClient
reconnect_logic: Optional[ReconnectLogic] = None
last_kwh: Optional[float] = None
last_time: Optional[datetime.datetime] = None
reconnect_logic: Optional[ReconnectLogic]
energy_splits: list[EnergySplit]
current_split: Optional[EnergySplit]

def __init__(self, device_config: DeviceConfig, config: Config):
"""Initialize the Glow device."""
Expand All @@ -46,17 +62,13 @@ def __init__(self, device_config: DeviceConfig, config: Config):
self.device_config.password,
zeroconf_instance=self.zeroconf,
)

@property
def provided_metrics(self) -> list[Metric]:
return [
Metric(MetricType.ENERGY, MetricPeriod.DAY, False, False),
Metric(MetricType.ACCURACY, MetricPeriod.DAY, False, False),
Metric(MetricType.POWER, MetricPeriod.TOTAL, False, False),
]
self.reconnect_logic = None
self.energy_splits = []
self.current_split = None

async def start(self):
"""Start the device."""

with Session(db_engine) as session:
self.device_config.is_active = True
session.add(self.device_config)
Expand All @@ -79,6 +91,8 @@ async def on_connect():
session.commit()
session.refresh(self.device_config)

await self.__initialize_data_splits()

await self.api.subscribe_states(self.__state_changed)

async def on_connect_error(error: Exception):
Expand Down Expand Up @@ -142,6 +156,29 @@ async def can_connect(self) -> bool:
finally:
await self.api.disconnect(force=True)

async def __initialize_data_splits(self):
current_timezone = datetime.datetime.now().astimezone().tzinfo
current_dt = datetime.datetime.now(tz=current_timezone)

time_start: datetime.datetime = datetime.datetime.now().replace(
hour=0, minute=0, second=0, microsecond=0
)
energy_start: float = 0
energy_split: EnergySplit = EnergySplit(energy_start, time_start)

last_energy: float = await self.last(
Metric(MetricType.ENERGY, MetricPeriod.DAY, False, False), current_dt
)
last_accuracy: float = await self.last(
Metric(MetricType.ACCURACY, MetricPeriod.DAY, False, False), current_dt
)
last_time = time_start + datetime.timedelta(
seconds=last_accuracy * 60 * 60 * 24
)
energy_split.update(last_energy, last_time)

self.energy_splits.append(energy_split)

def __state_changed(self, state: SensorState):
loop = asyncio.get_event_loop()

Expand All @@ -150,47 +187,63 @@ def __state_changed(self, state: SensorState):
return

if state.key == 2690257735:
loop.create_task(self.__on_total_kwh_changed(state.state))
loop.create_task(self.__on_total_energy_changed(state.state))

async def __on_total_kwh_changed(self, value: float):
async def __on_total_energy_changed(self, value: float):
current_timezone = datetime.datetime.now().astimezone().tzinfo
current_dt = datetime.datetime.now(tz=current_timezone)

if self.last_kwh is None or self.last_time is None:
self.last_kwh = value
self.last_time = datetime.datetime.now(tz=current_timezone)
if self.current_split is None:
self.current_split = EnergySplit(value, current_dt)
return

if value < self.last_kwh:
self.last_kwh = value
logger.warning("Detected a reset of the total kWh counter.")
if value < self.current_split.energy_last:
self.energy_splits.append(self.current_split)
self.current_split = EnergySplit(value, current_dt)
return

value_dt = datetime.datetime.now(tz=current_timezone)

kwh_increase = value - self.last_kwh
time_increase_us = (value_dt - self.last_time).microseconds
us_per_day = 1000 * 1000 * 60 * 60 * 24
accuracy_increase = time_increase_us / us_per_day
if self.current_split.time_last.date() != current_dt.date():
self.energy_splits = []
self.current_split = EnergySplit(value, current_dt)
return

logger.debug(
f"Detected {kwh_increase} kWh increase in {time_increase_us} us for device {self.device_config.name}."
self.current_split.update(value, current_dt)

energy_daily_total: float = (
sum(
[
energy_data_split.energy_last - energy_data_split.energy_start
for energy_data_split in self.energy_splits
]
)
+ self.current_split.energy_last
- self.current_split.energy_start
)
seconds_daily_total: float = (
sum(
[
(
energy_data_split.time_last - energy_data_split.time_start
).total_seconds()
for energy_data_split in self.energy_splits
]
)
) + (
self.current_split.time_last - self.current_split.time_start
).total_seconds()
accuracy_daily_total: float = seconds_daily_total / (60 * 60 * 24)

await self.increment(
await self.write(
Metric(MetricType.ENERGY, MetricPeriod.DAY, False, False),
kwh_increase,
value_dt,
energy_daily_total,
current_dt,
)
await self.increment(
await self.write(
Metric(MetricType.ACCURACY, MetricPeriod.DAY, False, False),
accuracy_increase,
value_dt,
accuracy_daily_total,
current_dt,
)

await self.update(value_dt)

self.last_kwh = value

async def __on_power_changed(self, value: float):
current_timezone = datetime.datetime.now().astimezone().tzinfo
value_dt = datetime.datetime.now(tz=current_timezone)
Expand Down
1 change: 1 addition & 0 deletions estimenergy/models/total.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ class Total(SQLModel, table=True):
device_name: str = Field(default=None, index=True)
energy: float = Field(default=0)
cost: float = Field(default=0)
power: float = Field(default=0)
70 changes: 1 addition & 69 deletions estimenergy/services/data_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,76 +17,8 @@ def __init__(self, device_config: DeviceConfig, config: Config):
self.device_config = device_config
self.config = config

async def last(
self,
metric: Metric,
value_dt: datetime.datetime,
) -> float:
"""Return the last value for a metric."""

if metric not in self.supported_metrics:
return 0

return await self._last(metric, value_dt)

async def write(
self,
metric: Metric,
value: float,
value_dt: datetime.datetime,
):
"""Write a metric to the database."""

if metric not in self.supported_metrics:
return

await self._write(metric, value, value_dt)

async def increment(
self,
metric: Metric,
value: float,
value_dt: datetime.datetime,
):
"""Increment a metric in the database."""

if metric not in self.supported_metrics:
return

last_value = await self.last(metric, value_dt)

await self.write(metric, last_value + value, value_dt)

async def decrement(
self,
metric: Metric,
value: float,
value_dt: datetime.datetime,
):
"""Decrement a metric in the database."""

if metric not in self.supported_metrics:
return

last_value = await self.last(metric, value_dt)

await self.write(metric, last_value - value, value_dt)

@property
@abstractmethod
def supported_metrics(self) -> list[Metric]:
"""Return a list of metrics supported by this service."""

@abstractmethod
async def _last(
self,
metric: Metric,
value_dt: datetime.datetime,
) -> float:
"""Get the last value for a metric."""

@abstractmethod
async def _write(
async def write(
self,
metric: Metric,
value: float,
Expand Down
Loading

0 comments on commit efb4b94

Please sign in to comment.