from time import time from datetime import datetime, timedelta from typing import Optional from collections import namedtuple from .clickhouse import ClickhouseDatabase IntervalList = list[list[Optional[datetime]]] class InverterDatabase(ClickhouseDatabase): def __init__(self): super().__init__('solarmon') def add_generation(self, home_id: int, client_time: int, watts: int) -> None: self.db.execute( 'INSERT INTO generation (ClientTime, ReceivedTime, HomeID, Watts) VALUES', [[client_time, round(time()), home_id, watts]] ) def add_status(self, home_id: int, client_time: int, grid_voltage: int, grid_freq: int, ac_output_voltage: int, ac_output_freq: int, ac_output_apparent_power: int, ac_output_active_power: int, output_load_percent: int, battery_voltage: int, battery_voltage_scc: int, battery_voltage_scc2: int, battery_discharge_current: int, battery_charge_current: int, battery_capacity: int, inverter_heat_sink_temp: int, mppt1_charger_temp: int, mppt2_charger_temp: int, pv1_input_power: int, pv2_input_power: int, pv1_input_voltage: int, pv2_input_voltage: int, mppt1_charger_status: int, mppt2_charger_status: int, battery_power_direction: int, dc_ac_power_direction: int, line_power_direction: int, load_connected: int) -> None: self.db.execute("""INSERT INTO status ( ClientTime, ReceivedTime, HomeID, GridVoltage, GridFrequency, ACOutputVoltage, ACOutputFrequency, ACOutputApparentPower, ACOutputActivePower, OutputLoadPercent, BatteryVoltage, BatteryVoltageSCC, BatteryVoltageSCC2, BatteryDischargingCurrent, BatteryChargingCurrent, BatteryCapacity, HeatSinkTemp, MPPT1ChargerTemp, MPPT2ChargerTemp, PV1InputPower, PV2InputPower, PV1InputVoltage, PV2InputVoltage, MPPT1ChargerStatus, MPPT2ChargerStatus, BatteryPowerDirection, DCACPowerDirection, LinePowerDirection, LoadConnected) VALUES""", [[ client_time, round(time()), home_id, grid_voltage, grid_freq, ac_output_voltage, ac_output_freq, ac_output_apparent_power, ac_output_active_power, output_load_percent, battery_voltage, battery_voltage_scc, battery_voltage_scc2, battery_discharge_current, battery_charge_current, battery_capacity, inverter_heat_sink_temp, mppt1_charger_temp, mppt2_charger_temp, pv1_input_power, pv2_input_power, pv1_input_voltage, pv2_input_voltage, mppt1_charger_status, mppt2_charger_status, battery_power_direction, dc_ac_power_direction, 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() elif ranges[len(ranges)-1][1] is None: ranges[len(ranges)-1][1] = dt_to - timedelta(seconds=1) 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