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.
|
||||
+5
-2
@@ -51,13 +51,13 @@ mqtt = mqtthandler.handler(cm)
|
||||
if 'battery_total' in config:
|
||||
try:
|
||||
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']))
|
||||
exit()
|
||||
|
||||
try:
|
||||
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']))
|
||||
exit()
|
||||
|
||||
@@ -113,7 +113,10 @@ def update():
|
||||
|
||||
def main():
|
||||
while True:
|
||||
try:
|
||||
update()
|
||||
except Exception:
|
||||
logger.exception("Update failed, retrying next cycle")
|
||||
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 threading
|
||||
import time
|
||||
import functools
|
||||
import victronclient
|
||||
import mqttpublisher
|
||||
import tibberclient
|
||||
@@ -7,6 +10,15 @@ 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
|
||||
@@ -27,6 +39,8 @@ class Chargemode(IntEnum):
|
||||
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
|
||||
@@ -57,17 +71,20 @@ class chargeman_controller:
|
||||
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):
|
||||
|
||||
|
||||
@@ -86,7 +103,12 @@ class chargeman_controller:
|
||||
#logger.info("Update P1 usage: {}".format(netp1usage))
|
||||
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
|
||||
|
||||
@@ -101,22 +123,25 @@ class chargeman_controller:
|
||||
if self.setpoint > MAX_CHARGE_POWER:
|
||||
self.setpoint = MAX_CHARGE_POWER
|
||||
elif self.setpoint < -MAX_POWER:
|
||||
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
|
||||
|
||||
@@ -142,6 +167,7 @@ class chargeman_controller:
|
||||
self.valuekwh = value
|
||||
|
||||
|
||||
@_locked
|
||||
def update(self):
|
||||
|
||||
logger.debug("Run controller update")
|
||||
@@ -150,10 +176,17 @@ class chargeman_controller:
|
||||
|
||||
logger.debug("Controller update mqtt loop done")
|
||||
|
||||
if self.victronconn.get_power_usage() > 0:
|
||||
self.battery_total_net_usage += self.victronconn.get_power_usage() /60/60*10
|
||||
elif self.victronconn.get_power_usage() < 0:
|
||||
self.battery_total_net_delivery += (self.victronconn.get_power_usage() * -1 ) /60/60*10
|
||||
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()
|
||||
|
||||
@@ -175,7 +208,7 @@ class chargeman_controller:
|
||||
self.netp1usage,
|
||||
self.victronconn.get_power_usage(),
|
||||
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_delivery,
|
||||
self.tibber.getPrice() ))
|
||||
@@ -187,6 +220,7 @@ class chargeman_controller:
|
||||
|
||||
|
||||
|
||||
@_locked
|
||||
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.loop_start()
|
||||
break
|
||||
except:
|
||||
logger.info("Unable to connect to mqtt")
|
||||
except Exception as e:
|
||||
logger.info("Unable to connect to mqtt: {!r}".format(e))
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -26,8 +26,8 @@ class publisher:
|
||||
self.client.connect(broker,port, 60)
|
||||
self.client.loop_start()
|
||||
break
|
||||
except:
|
||||
logger.info("Unable to connect to mqtt")
|
||||
except Exception as e:
|
||||
logger.info("Unable to connect to mqtt: {!r}".format(e))
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
|
||||
+3
-2
@@ -20,5 +20,6 @@ topic_bat_consumption = topic_base + 'actual_consumption'
|
||||
#battery packs
|
||||
topic_batpack_soc = topic_base + '{}_soc' + topic_state_postfix
|
||||
|
||||
broker="10.2.0.15"
|
||||
port=1883
|
||||
import os
|
||||
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("Return from setpoint: {}".format(self.session_bus.call_blocking(self.device, path_setpoint ,None,'SetValue', 'v', [self.setpoint])))
|
||||
break
|
||||
except:
|
||||
|
||||
logger.info("Unable to connect to dbus {}".format(path_setpoint))
|
||||
except Exception as e:
|
||||
logger.info("Unable to connect to dbus {}: {!r}".format(path_setpoint, e))
|
||||
time.sleep(5)
|
||||
|
||||
def get_power_usage(self):
|
||||
@@ -129,9 +128,9 @@ class client:
|
||||
try:
|
||||
self.session_bus.call_blocking(self.device, path_setpoint ,None,'SetValue', 'v', [setpoint])
|
||||
self.setpoint = setpoint
|
||||
except:
|
||||
logger.info("Unable to connect to dbus {}".format(path_setpoint))
|
||||
self.setpoint = None
|
||||
except Exception as e:
|
||||
# keep the last known setpoint; None would break later comparisons
|
||||
logger.error("Unable to set dbus {}: {!r}".format(path_setpoint, e))
|
||||
|
||||
|
||||
def get_battery_current(self):
|
||||
@@ -193,7 +192,7 @@ class battery:
|
||||
#self.paths = victron.listPaths(device)
|
||||
|
||||
def get_name(self):
|
||||
return self.snameoc
|
||||
return self.name
|
||||
|
||||
|
||||
def get_soc(self):
|
||||
|
||||
Reference in New Issue
Block a user