Compare commits

..
1 Commits
12 changed files with 89 additions and 27996 deletions
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
+12
View File
@@ -0,0 +1,12 @@
# chargeman
Home battery controller for Victron (Venus OS). Controls the inverter via D-Bus (AC power setpoint, feed-in/charge enable),
uses P1 meter data and the Tibber hourly price via MQTT, and publishes state to MQTT (Home Assistant discovery).
## Run
- Install on the Venus OS device under `/data/chargeman`, copy `config.ini.example` to `config.ini` and set the Tibber key.
- Init script: `chargeman` (`start|stop|restart|status`), runs `chargeman.py` with `/data` as working directory.
- MQTT broker: env `CHARGEMAN_MQTT_BROKER` (default `10.2.0.15`) and `CHARGEMAN_MQTT_PORT` (default `1883`).
## Chargemodes (`avr95/battery/chargemode/set`)
0 floating, 1 charge manual, 2 charge max, 3 discharge manual, 4 discharge max, 5 charge cheap (Tibber), 6 balance dyn, 7 charge dyn, 8 discharge dyn.
+5 -2
View File
@@ -51,13 +51,13 @@ mqtt = mqtthandler.handler(cm)
if 'battery_total' in config: if 'battery_total' in config:
try: try:
cm.set_battery_total_net_delivery(float(config['battery_total']['net_delivery'])) cm.set_battery_total_net_delivery(float(config['battery_total']['net_delivery']))
except: except Exception:
logger.error("Unable to load net_delivery {}".format(config['battery_total']['net_delivery'])) logger.error("Unable to load net_delivery {}".format(config['battery_total']['net_delivery']))
exit() exit()
try: try:
cm.set_battery_total_net_usage(float(config['battery_total']['net_usage'])) cm.set_battery_total_net_usage(float(config['battery_total']['net_usage']))
except: except Exception:
logger.error("Unable to load net_usage {}".format(config['battery_total']['net_usage'])) logger.error("Unable to load net_usage {}".format(config['battery_total']['net_usage']))
exit() exit()
@@ -113,7 +113,10 @@ def update():
def main(): def main():
while True: while True:
try:
update() update()
except Exception:
logger.exception("Update failed, retrying next cycle")
time.sleep(10) time.sleep(10)
-4
View File
@@ -1,4 +0,0 @@
#!/bin/sh
cd /data
/usr/bin/python dbustest.py > /var/log/chargeman.log
+12
View File
@@ -0,0 +1,12 @@
[battery_total]
net_usage = 229505.52499994054
net_delivery = 135455.02500000477
[tibber]
key = YOUR_TIBBER_API_TOKEN
[status]
chargemode = 8
chargevalue = 0
valuekwh = 0.0
+41 -7
View File
@@ -1,5 +1,8 @@
import logging import logging
import threading
import time
import functools
import victronclient import victronclient
import mqttpublisher import mqttpublisher
import tibberclient import tibberclient
@@ -7,6 +10,15 @@ from enum import IntEnum
logger = logging.getLogger('main') 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 charge_max_price_level = 0.024
MAX_POWER=4000 MAX_POWER=4000
@@ -27,6 +39,8 @@ class Chargemode(IntEnum):
class chargeman_controller: class chargeman_controller:
def __init__(self, victronconn: victronclient.client, tibber: tibberclient.client) -> None: def __init__(self, victronconn: victronclient.client, tibber: tibberclient.client) -> None:
self.lock = threading.RLock()
self.last_update = None
self.chargemode=Chargemode.FLOATING self.chargemode=Chargemode.FLOATING
self.chargevalue=0 self.chargevalue=0
self.consump_updated=False self.consump_updated=False
@@ -57,17 +71,20 @@ class chargeman_controller:
return self.battery_total_net_usage return self.battery_total_net_usage
@_locked
def set_net_consumption(self, value): def set_net_consumption(self, value):
self.consumption = value self.consumption = value
self.consump_updated = True self.consump_updated = True
self.check_net_usage() self.check_net_usage()
@_locked
def set_net_delivery(self, value): def set_net_delivery(self, value):
self.return_delivery = value self.return_delivery = value
self.return_updated = True self.return_updated = True
self.check_net_usage() self.check_net_usage()
@_locked
def check_net_usage(self): def check_net_usage(self):
@@ -86,7 +103,12 @@ class chargeman_controller:
#logger.info("Update P1 usage: {}".format(netp1usage)) #logger.info("Update P1 usage: {}".format(netp1usage))
if self.chargemode in [Chargemode.BALANCE_DYN,Chargemode.CHARGE_DYN,Chargemode.DISCHARGE_DYN]: if self.chargemode in [Chargemode.BALANCE_DYN,Chargemode.CHARGE_DYN,Chargemode.DISCHARGE_DYN]:
setpoint = -1 * self.victronconn.get_power_usage() 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 setpoint = self.netp1usage+setpoint
@@ -101,22 +123,25 @@ class chargeman_controller:
if self.setpoint > MAX_CHARGE_POWER: if self.setpoint > MAX_CHARGE_POWER:
self.setpoint = MAX_CHARGE_POWER self.setpoint = MAX_CHARGE_POWER
elif self.setpoint < -MAX_POWER: elif self.setpoint < -MAX_POWER:
self.setpoint = MAX_POWER self.setpoint = -MAX_POWER
#logger.info ("Dynamic setpoint: {}".format(setpoint)) #logger.info ("Dynamic setpoint: {}".format(setpoint))
self.apply_charge_mode() self.apply_charge_mode()
@_locked
def set_chargemode(self, mode: Chargemode): def set_chargemode(self, mode: Chargemode):
self.chargemode = mode self.chargemode = mode
self.apply_charge_mode() self.apply_charge_mode()
@_locked
def set_chargemode_int(self, mode: int): def set_chargemode_int(self, mode: int):
self.chargemode = Chargemode(mode) self.chargemode = Chargemode(mode)
self.apply_charge_mode() self.apply_charge_mode()
@_locked
def set_chargevalue(self,value): def set_chargevalue(self,value):
self.chargevalue = value self.chargevalue = value
@@ -142,6 +167,7 @@ class chargeman_controller:
self.valuekwh = value self.valuekwh = value
@_locked
def update(self): def update(self):
logger.debug("Run controller update") logger.debug("Run controller update")
@@ -150,10 +176,17 @@ class chargeman_controller:
logger.debug("Controller update mqtt loop done") logger.debug("Controller update mqtt loop done")
if self.victronconn.get_power_usage() > 0: now = time.monotonic()
self.battery_total_net_usage += self.victronconn.get_power_usage() /60/60*10 elapsed = 0.0 if self.last_update is None else now - self.last_update
elif self.victronconn.get_power_usage() < 0: self.last_update = now
self.battery_total_net_delivery += (self.victronconn.get_power_usage() * -1 ) /60/60*10
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() self.apply_charge_mode()
@@ -175,7 +208,7 @@ class chargeman_controller:
self.netp1usage, self.netp1usage,
self.victronconn.get_power_usage(), self.victronconn.get_power_usage(),
self.victronconn.get_soc() , self.victronconn.get_soc() ,
float(self.victronconn.get_battery_current() ), float(self.victronconn.get_battery_current() or 0),
self.battery_total_net_usage, self.battery_total_net_usage,
self.battery_total_net_delivery, self.battery_total_net_delivery,
self.tibber.getPrice() )) self.tibber.getPrice() ))
@@ -187,6 +220,7 @@ class chargeman_controller:
@_locked
def apply_charge_mode(self): def apply_charge_mode(self):
-27969
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -31,8 +31,8 @@ class handler:
self.client.connect(broker,port, 60) self.client.connect(broker,port, 60)
self.client.loop_start() self.client.loop_start()
break break
except: except Exception as e:
logger.info("Unable to connect to mqtt") logger.info("Unable to connect to mqtt: {!r}".format(e))
time.sleep(5) time.sleep(5)
+2 -2
View File
@@ -26,8 +26,8 @@ class publisher:
self.client.connect(broker,port, 60) self.client.connect(broker,port, 60)
self.client.loop_start() self.client.loop_start()
break break
except: except Exception as e:
logger.info("Unable to connect to mqtt") logger.info("Unable to connect to mqtt: {!r}".format(e))
time.sleep(5) time.sleep(5)
+3 -2
View File
@@ -20,5 +20,6 @@ topic_bat_consumption = topic_base + 'actual_consumption'
#battery packs #battery packs
topic_batpack_soc = topic_base + '{}_soc' + topic_state_postfix topic_batpack_soc = topic_base + '{}_soc' + topic_state_postfix
broker="10.2.0.15" import os
port=1883 broker=os.environ.get("CHARGEMAN_MQTT_BROKER", "10.2.0.15")
port=int(os.environ.get("CHARGEMAN_MQTT_PORT", "1883"))
+3
View File
@@ -0,0 +1,3 @@
paho-mqtt<2
pyTibber
# dbus-python is provided by Venus OS
+6 -7
View File
@@ -111,9 +111,8 @@ class client:
logger.info("Connecting to dbus...") logger.info("Connecting to dbus...")
logger.info("Return from setpoint: {}".format(self.session_bus.call_blocking(self.device, path_setpoint ,None,'SetValue', 'v', [self.setpoint]))) logger.info("Return from setpoint: {}".format(self.session_bus.call_blocking(self.device, path_setpoint ,None,'SetValue', 'v', [self.setpoint])))
break break
except: except Exception as e:
logger.info("Unable to connect to dbus {}: {!r}".format(path_setpoint, e))
logger.info("Unable to connect to dbus {}".format(path_setpoint))
time.sleep(5) time.sleep(5)
def get_power_usage(self): def get_power_usage(self):
@@ -129,9 +128,9 @@ class client:
try: try:
self.session_bus.call_blocking(self.device, path_setpoint ,None,'SetValue', 'v', [setpoint]) self.session_bus.call_blocking(self.device, path_setpoint ,None,'SetValue', 'v', [setpoint])
self.setpoint = setpoint self.setpoint = setpoint
except: except Exception as e:
logger.info("Unable to connect to dbus {}".format(path_setpoint)) # keep the last known setpoint; None would break later comparisons
self.setpoint = None logger.error("Unable to set dbus {}: {!r}".format(path_setpoint, e))
def get_battery_current(self): def get_battery_current(self):
@@ -193,7 +192,7 @@ class battery:
#self.paths = victron.listPaths(device) #self.paths = victron.listPaths(device)
def get_name(self): def get_name(self):
return self.snameoc return self.name
def get_soc(self): def get_soc(self):