Files
chargeman/controller.py
T

258 lines
7.4 KiB
Python

import logging
import threading
import time
import functools
import victronclient
import mqttpublisher
import tibberclient
from enum import IntEnum
logger = logging.getLogger('main')
def _locked(method):
# Serialise access to controller state: MQTT callbacks and the main loop run in different threads
@functools.wraps(method)
def wrapper(self, *args, **kwargs):
with self.lock:
return method(self, *args, **kwargs)
return wrapper
charge_max_price_level = 0.024
MAX_POWER=4000
MAX_CHARGE_POWER=4000
class Chargemode(IntEnum):
FLOATING=0
CHARGING_MANUAL=1
CHARGING_MAX=2
DISCHARGING_MANUAL=3
DISCHARGING_MAX=4
CHARGING_CHEAP=5
BALANCE_DYN=6
CHARGE_DYN=7
DISCHARGE_DYN=8
class chargeman_controller:
def __init__(self, victronconn: victronclient.client, tibber: tibberclient.client) -> None:
self.lock = threading.RLock()
self.last_update = None
self.chargemode=Chargemode.FLOATING
self.chargevalue=0
self.consump_updated=False
self.return_updated=False
self.consumption=0
self.return_delivery=0
self.netp1usage =0
self.valuekwh = 0.0
self.battery_total_net_usage = 0.0
self.battery_total_net_delivery = 0.0
self.setpoint = 0
self.netp1usageupdated = False
self.victronconn = victronconn
self.tibber = tibber
self.mqtt = mqttpublisher.publisher()
def set_battery_total_net_delivery(self, value):
self.battery_total_net_delivery = value
def get_battery_total_net_delivery(self):
return self.battery_total_net_delivery
def set_battery_total_net_usage(self, value):
self.battery_total_net_usage = value
def get_battery_total_net_usage(self):
return self.battery_total_net_usage
@_locked
def set_net_consumption(self, value):
self.consumption = value
self.consump_updated = True
self.check_net_usage()
@_locked
def set_net_delivery(self, value):
self.return_delivery = value
self.return_updated = True
self.check_net_usage()
@_locked
def check_net_usage(self):
if self.return_updated and self.consump_updated:
self.consump_updated=False
self.return_updated=False
if self.netp1usageupdated: # skip till next p1 cycle
self.netp1usageupdated=False
return
self.netp1usage = self.consumption - self.return_delivery
self.netp1usageupdated = True
#logger.info("Update P1 usage: {}".format(netp1usage))
if self.chargemode in [Chargemode.BALANCE_DYN,Chargemode.CHARGE_DYN,Chargemode.DISCHARGE_DYN]:
power_usage = self.victronconn.get_power_usage()
if power_usage is None:
logger.warning("Victron power usage unknown, skipping dynamic setpoint")
return
setpoint = -1 * power_usage
setpoint = self.netp1usage+setpoint
if self.chargemode==Chargemode.CHARGE_DYN and setpoint > 0:
setpoint =0
elif self.chargemode==Chargemode.DISCHARGE_DYN and setpoint < 0:
setpoint =0
self.setpoint = -1 * setpoint
if self.setpoint > MAX_CHARGE_POWER:
self.setpoint = MAX_CHARGE_POWER
elif self.setpoint < -MAX_POWER:
self.setpoint = -MAX_POWER
#logger.info ("Dynamic setpoint: {}".format(setpoint))
self.apply_charge_mode()
@_locked
def set_chargemode(self, mode: Chargemode):
self.chargemode = mode
self.apply_charge_mode()
@_locked
def set_chargemode_int(self, mode: int):
self.chargemode = Chargemode(mode)
self.apply_charge_mode()
@_locked
def set_chargevalue(self,value):
self.chargevalue = value
def get_chargevalue(self):
return self.chargevalue
def get_chargemode(self) -> Chargemode:
return self.chargemode
def get_chargemode_int(self) -> int:
return int(self.chargemode)
def get_chargevaluekwh(self) -> float:
return self.valuekwh
def set_chargevaluekwh(self,value: float):
self.valuekwh = value
@_locked
def update(self):
logger.debug("Run controller update")
self.mqtt.loop()
logger.debug("Controller update mqtt loop done")
now = time.monotonic()
elapsed = 0.0 if self.last_update is None else now - self.last_update
self.last_update = now
power_usage = self.victronconn.get_power_usage()
if power_usage is not None:
# W * s / 3600 = Wh, as kWh counter: elapsed real time instead of a fixed interval
if power_usage > 0:
self.battery_total_net_usage += power_usage / 60 / 60 * elapsed
elif power_usage < 0:
self.battery_total_net_delivery += (power_usage * -1) / 60 / 60 * elapsed
self.apply_charge_mode()
logger.debug("Controller update mqtt update batteries")
for battery in self.victronconn.batteries:
self.mqtt.publish_battery(battery)
logger.debug("Controller update mqtt update done")
logger.info('Mode={} CV={}W L1: SP={}W Net usage={}W Power={}W Battery: SoC={}% Current={:.2f}A TUsage={:.1f}KwH TDelivery{:.1f}KwH Tibber=€{}'.format(
str(self.chargemode),
int(self.chargevalue),
self.victronconn.get_setpoint(),
self.netp1usage,
self.victronconn.get_power_usage(),
self.victronconn.get_soc() ,
float(self.victronconn.get_battery_current() or 0),
self.battery_total_net_usage,
self.battery_total_net_delivery,
self.tibber.getPrice() ))
@_locked
def apply_charge_mode(self):
if self.chargemode in [Chargemode.CHARGING_MANUAL, Chargemode.DISCHARGING_MANUAL]:
self.setpoint = self.chargevalue
elif self.chargemode in [Chargemode.CHARGING_MAX]:
self.setpoint = MAX_CHARGE_POWER
elif self.chargemode in [Chargemode.DISCHARGING_MAX]:
self.setpoint = -MAX_POWER
elif self.chargemode == Chargemode.CHARGING_CHEAP:
if self.tibber.getPrice() == None:
logger.info("Tibber price unknown, doing nothing")
self.setpoint = 0
elif round(self.tibber.getPrice(),2) > charge_max_price_level:
logger.info("Energy prive {} too expensive, no charging".format(round(self.tibber.getPrice(),2)))
self.setpoint = 0
else:
logger.info("Energy prive {} ok, charging".format(round(self.tibber.getPrice(),2)))
self.setpoint = MAX_CHARGE_POWER
elif self.chargemode == Chargemode.FLOATING:
self.setpoint = 0
if self.setpoint > 0:
self.victronconn.set_charging()
elif self.setpoint == 0:
self.victronconn.set_idle()
else:
if self.setpoint < -MAX_POWER:
self.setpoint = -MAX_POWER
self.victronconn.set_discharging()
self.victronconn.set_setpoint(self.setpoint)
self.mqtt.publish_controller(self)