diff options
author | Evgeny Zinoviev <me@ch1p.io> | 2022-10-24 04:38:48 +0300 |
---|---|---|
committer | Evgeny Zinoviev <me@ch1p.io> | 2022-10-24 04:38:55 +0300 |
commit | b66958643466b70011bcd0ed1f03e4938bb97f85 (patch) | |
tree | e0d37ced61134bc251332a1784077cce7e1fd47b /src/home | |
parent | 3bac4b316dc275c1c22b93cde5ebac87df1d3234 (diff) |
add utility that calculates total sum of consumed energy in wh for given period
Diffstat (limited to 'src/home')
-rw-r--r-- | src/home/config/__init__.py | 2 | ||||
-rw-r--r-- | src/home/config/config.py | 4 | ||||
-rw-r--r-- | src/home/database/inverter.py | 133 |
3 files changed, 135 insertions, 4 deletions
diff --git a/src/home/config/__init__.py b/src/home/config/__init__.py index d4b1c27..cc9c091 100644 --- a/src/home/config/__init__.py +++ b/src/home/config/__init__.py @@ -1 +1 @@ -from .config import ConfigStore, config, is_development_mode +from .config import ConfigStore, config, is_development_mode, setup_logging diff --git a/src/home/config/config.py b/src/home/config/config.py index f953c43..9882bfa 100644 --- a/src/home/config/config.py +++ b/src/home/config/config.py @@ -34,7 +34,7 @@ class ConfigStore: data: MutableMapping[str, Any] app_name: Optional[str] - def __int__(self): + def __init__(self): self.data = {} self.app_name = None @@ -57,7 +57,7 @@ class ConfigStore: parser = ArgumentParser() if not no_config: parser.add_argument('-c', '--config', type=str, required=name is None, - help='Path to the config in TOML format') + help='Path to the config in TOML or YAML format') parser.add_argument('-V', '--verbose', action='store_true') parser.add_argument('--log-file', type=str) parser.add_argument('--log-default-fmt', action='store_true') diff --git a/src/home/database/inverter.py b/src/home/database/inverter.py index 6fdf9d7..8ebfae2 100644 --- a/src/home/database/inverter.py +++ b/src/home/database/inverter.py @@ -1,10 +1,40 @@ -from .clickhouse import get_clickhouse +import logging + +from zoneinfo import ZoneInfo from time import time +from datetime import datetime, timedelta +from typing import Optional +from collections import namedtuple + +from ..config import is_development_mode +from .clickhouse import get_clickhouse + + +IntervalList = list[list[Optional[datetime]]] class InverterDatabase: def __init__(self): self.db = get_clickhouse('solarmon') + self.server_timezone = self.query('SELECT timezone()')[0][0] + + self.logger = logging.getLogger(self.__class__.__name__) + + def query(self, *args, **kwargs): + settings = {'use_client_time_zone': True} + kwargs['settings'] = settings + + if 'no_tz_fix' not in kwargs and len(args) > 1 and isinstance(args[1], dict): + for k, v in args[1].items(): + if isinstance(v, datetime): + args[1][k] = v.astimezone(tz=ZoneInfo(self.server_timezone)) + + result = self.db.execute(*args, **kwargs) + + if is_development_mode(): + self.logger.debug(args[0] if len(args) == 1 else args[0] % args[1]) + + return result def add_generation(self, home_id: int, client_time: int, watts: int) -> None: self.db.execute( @@ -100,3 +130,104 @@ class InverterDatabase: line_power_direction, load_connected ]]) + + def get_consumed_energy(self, dt_from: datetime, dt_to: datetime) -> float: + rows = self.query('SELECT ClientTime, ACOutputActivePower FROM status' + ' WHERE ClientTime >= %(from)s AND ClientTime <= %(to)s' + ' ORDER BY ClientTime', {'from': dt_from, 'to': dt_to}) + prev_time = None + prev_wh = 0 + + ws = 0 # watt-seconds + for t, wh in rows: + if prev_time is not None: + n = (t - prev_time).total_seconds() + ws += prev_wh * n + + prev_time = t + prev_wh = wh + + return ws / 3600 # convert to watt-hours + + def get_intervals_by_condition(self, + dt_from: datetime, + dt_to: datetime, + cond_start: str, + cond_end: str) -> IntervalList: + rows = None + ranges = [[None, None]] + + while rows is None or len(rows) > 0: + if ranges[len(ranges) - 1][0] is None: + condition = cond_start + range_idx = 0 + else: + condition = cond_end + range_idx = 1 + + rows = self.query('SELECT ClientTime FROM status ' + f'WHERE ClientTime > %(from)s AND ClientTime <= %(to)s AND {condition}' + ' ORDER BY ClientTime LIMIT 1', + {'from': dt_from, 'to': dt_to}) + if not rows: + break + + row = rows[0] + + ranges[len(ranges) - 1][range_idx] = row[0] + if range_idx == 1: + ranges.append([None, None]) + + dt_from = row[0] + + if ranges[len(ranges)-1][0] is None: + ranges.pop() + + return ranges + + def get_grid_connected_intervals(self, dt_from: datetime, dt_to: datetime) -> IntervalList: + return self.get_intervals_by_condition(dt_from, dt_to, 'GridFrequency > 0', 'GridFrequency = 0') + + def get_grid_used_intervals(self, dt_from: datetime, dt_to: datetime) -> IntervalList: + return self.get_intervals_by_condition(dt_from, + dt_to, + "LinePowerDirection = 'Input'", + "LinePowerDirection != 'Input'") + + def get_grid_consumed_energy(self, dt_from: datetime, dt_to: datetime) -> float: + PrevData = namedtuple('PrevData', 'time, pd, bat_chg, bat_dis, wh') + + ws = 0 # watt-seconds + amps = 0 # amper-seconds + + intervals = self.get_grid_used_intervals(dt_from, dt_to) + for dt_start, dt_end in intervals: + fields = ', '.join([ + 'ClientTime', + 'DCACPowerDirection', + 'BatteryChargingCurrent', + 'BatteryDischargingCurrent', + 'ACOutputActivePower' + ]) + rows = self.query(f'SELECT {fields} FROM status' + ' WHERE ClientTime >= %(from)s AND ClientTime < %(to)s ORDER BY ClientTime', + {'from': dt_start, 'to': dt_end}) + + prev = PrevData(time=None, pd=None, bat_chg=None, bat_dis=None, wh=None) + for ct, pd, bat_chg, bat_dis, wh in rows: + if prev.time is not None: + n = (ct-prev.time).total_seconds() + ws += prev.wh * n + + if pd == 'DC/AC': + amps -= prev.bat_dis * n + elif pd == 'AC/DC': + amps += prev.bat_chg * n + + prev = PrevData(time=ct, pd=pd, bat_chg=bat_chg, bat_dis=bat_dis, wh=wh) + + amps /= 3600 + wh = ws / 3600 + wh += amps*48 + + return wh |