Compare commits
1
Commits
0b17034b68
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
651676afda |
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -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.
|
||||||
+6
-3
@@ -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:
|
||||||
update()
|
try:
|
||||||
|
update()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Update failed, retrying next cycle")
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
cd /data
|
|
||||||
/usr/bin/python dbustest.py > /var/log/chargeman.log
|
|
||||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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
@@ -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
@@ -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"))
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
paho-mqtt<2
|
||||||
|
pyTibber
|
||||||
|
# dbus-python is provided by Venus OS
|
||||||
+6
-7
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user