Skip to content

Instantly share code, notes, and snippets.

@mojo2012
Created April 18, 2024 13:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mojo2012/0842639b0ce564568b8599293f70749c to your computer and use it in GitHub Desktop.
Save mojo2012/0842639b0ce564568b8599293f70749c to your computer and use it in GitHub Desktop.
#!/usr/bin/python -u
# -*- coding: utf-8 -*-
"""
GENERAL PRINCIPLES ON kWh deltas
Requirements:
For the 'Solar yield' and 'Consumption' tabs on the VRM Portal, we want to get the amount of energy
generated and used. It needs to be split up in the following categories:
- Energy from grid to consumers
- Energy from grid to battery
- Energy from genset to consumers
- Energy from genset to battery
- Energy from PV to consumers
- Energy from PV to battery
- Energy from PV to grid
- Energy from Battery to consumers
- Energy from Battery to grid
When the hour passes, the amount of energy used cq. generated in that hour needs to be sent to the
VRM Database. And during the actual hour, the amount of energy since the start of the hour is
required, imagine looking at the graph and pressing F5 all the time: you will slowly see the bars
growing during that hour.
Input of this process:
- The kWh counters (= energy) of all relevant devices (Multi, BMV, PV Inverters and Solar chargers).
Note that some of these do not store there counters in flash. So every time, for example, a Multi
starts up, it starts counting from 0.
- And later, when adding DC consumption or hub-3, it is possibly also necessary also look at the
direction of DC or AC currents. See hfstore code.
MEASUREMENT INACCURACIES
Assume, for example, you have 3 counters, and since how they are connected electrically, they
relate like a = b + c. But in reality, since all those measurements are not 100% accurate, this
sum will never be 100%.
In below calculations we face the same issue. The chosen approach is to prefer the counters that are
also visible to the user elsewhere, for example to solar yield from the MPPT, or the kWh from the BMV.
The counters in the Multi are not visible to the user.
CALCULATION STATUS:
This code calculates for dc-coupled and ac-coupled systems. It looks at AC consumption for consumption,
DC consumption is computed when the DC-system is enabled in the settings (/Settings/SystemSetup/HasDcSystem).
VEBUS DEVICES:
The kWh values from ve.bus devices are based to AC power measurements. Multi's and Quatros measure AcIn
power and inverter power (just before AC/DC conversion). AcOut power is computed from AcIn and AC inverter
power. The power consumption of the ve.bus device itself is ignored.
USAGE:
vrmlogger makes an instance of this class. And calls getdeltas() everytime it sends data to the vrm-portal.
Exactly on the hour, vrmlogger calls the same function, including a request to refresh the baseline.
IMPLEMENTATION:
This class, KwhDeltas, uses the DbusDeltas class to store the baselines and calculate the raw deltas. Those
raw deltas are then converted into the required parameters by KwhDeltas.getdeltas(...)
"""
import argparse
import logging
import os
import pprint
import sys
import time
from collections import namedtuple
from datetime import datetime
from dbus.exceptions import DBusException
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
# Victron imports
sys.path.insert(1, os.path.join(os.path.dirname(__file__), './ext/velib_python'))
from dbusdeltas import DbusDeltas
from dbusmonitor import DbusMonitor
from vedbus import VeDbusItemImport
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
BatteryService = namedtuple('BatteryService', ('service', 'instance'))
def no_battery_service(reason):
logging.info("battery service not used #{}".format(reason))
class KwhDeltas:
def __init__(self):
self._battery_service = None
self._store = {
'vebus': {
'services': [], 'class': 'com.victronenergy.vebus',
'paths': [
'/Energy/AcIn1ToInverter',
'/Energy/AcIn2ToInverter',
'/Energy/AcIn1ToAcOut',
'/Energy/AcIn2ToAcOut',
'/Energy/InverterToAcIn1',
'/Energy/InverterToAcIn2',
'/Energy/AcOutToAcIn1',
'/Energy/AcOutToAcIn2',
'/Energy/InverterToAcOut',
'/Energy/OutToInverter']},
'pvac.genset': {
'services': [], 'class': 'com.victronenergy.pvinverter',
'paths': [
'/Ac/Energy/Forward']},
'pvac.output': {
'services': [], 'class': 'com.victronenergy.pvinverter',
'paths': [
'/Ac/Energy/Forward']},
'pvac.grid': {
'services': [], 'class': 'com.victronenergy.pvinverter',
'paths': [
'/Ac/Energy/Forward']},
'battery': {
'services': [], 'class': 'com.victronenergy.battery',
'paths': [
'/History/DischargedEnergy',
'/History/ChargedEnergy']},
'solarcharger': {
'services': [], 'class': 'com.victronenergy.solarcharger',
'paths': [
'/Yield/User']},
'grid': {
'services': [], 'class': 'com.victronenergy.grid',
'paths': [
'/Ac/Energy/Forward',
'/Ac/Energy/Reverse']},
'genset': {
'services': [], 'class': 'com.victronenergy.genset',
'paths': [
'/Ac/Energy/Forward']},
'dcmeter': {
'services': [], 'class': 'com.victronenergy.dcmeter',
'paths': [
'/Dc/0/Energy']},
'inverter': {
'services': [], 'class': 'com.victronenergy.inverter',
'paths': [
'/Energy/InverterToAcOut',
'/Energy/OutToInverter',
'/Energy/SolarToBattery',
'/Energy/SolarToAcOut']},
'multi.grid': {
'services': [], 'class': 'com.victronenergy.multi',
'paths': [
'/Energy/AcIn1ToInverter',
'/Energy/AcIn1ToAcOut',
'/Energy/InverterToAcIn1',
'/Energy/AcOutToAcIn1',
'/Energy/InverterToAcOut',
'/Energy/OutToInverter',
'/Energy/SolarToAcOut',
'/Energy/SolarToBattery',
'/Energy/SolarToAcIn1']},
'multi.genset': {
'services': [], 'class': 'com.victronenergy.multi',
'paths': [
'/Energy/AcIn1ToInverter',
'/Energy/AcIn1ToAcOut',
'/Energy/InverterToAcIn1',
'/Energy/AcOutToAcIn1',
'/Energy/InverterToAcOut',
'/Energy/OutToInverter',
'/Energy/SolarToAcOut',
'/Energy/SolarToBattery',
'/Energy/SolarToAcIn1']},
'system': {
'services': [], 'class': 'com.victronenergy.system',
'paths':[
'/Timers/TimeOnInverter',
'/Timers/TimeOnGrid',
'/Timers/TimeOnGenerator',
'/Timers/TimeOff']},
'generator': {
'services': [], 'class': 'com.victronenergy.generator',
'paths': [
'/AccumulatedRuntime']},
'evcharger': {
'services': [], 'class': 'com.victronenergy.evcharger',
'paths': [
'/Ac/Energy/Forward']}}
# note that these paths need to be present in self._store (see above) as well.
self._rawdeltas = {
'com.victronenergy.battery': {
'/History/DischargedEnergy': 'dH21',
'/History/ChargedEnergy': 'dH22'
},
'com.victronenergy.solarcharger': {
'/Yield/User': 'dYU'
},
'com.victronenergy.pvinverter': {
'/Ac/Energy/Forward': 'dpE'
},
'com.victronenergy.grid': {
'/Ac/Energy/Forward': 'dgb',
'/Ac/Energy/Reverse': 'dgs'
},
'com.victronenergy.dcmeter': {
'/Dc/0/Energy': 'ddE'
},
'com.victronenergy.evcharger': {
'/Ac/Energy/Forward': 'evE'
}
}
# Items we watch, but we don't calculate deltas for them
self._non_store = {
'com.victronenergy.pvinverter': ['/Position'],
'com.victronenergy.multi': ['/Ac/In/1/Type', '/Ac/In/2/Type'],
}
def start(self, dbusmonitor, ve_item_gen = None):
self._dbusmonitor = dbusmonitor
self._ve_item_gen = ve_item_gen
logger.debug(
"KwhDeltas.start(): starting up, with self._store: \n%s\n" % pprint.pformat(self._store))
self._dbusdeltas = DbusDeltas(self._dbusmonitor, self._store)
# init all instances already existing on the dbus
services = self._dbusmonitor.get_service_list()
for name, instance in services.items():
self.handle_new_service(name, instance)
self._setup_active_battery_monitor()
def get_dbusmonitortree(self):
# Get all unique serviceclasses from self._store, and all paths.
dbusmonitoritems = {}
# dummy data since DbusMonitor wants it:
dummy = {'code': None, 'whenToLog': None, 'accessLevel': None}
for d in self._store.values():
if d['class'] not in dbusmonitoritems:
dbusmonitoritems[d['class']] = {}
for path in d['paths']:
dbusmonitoritems[d['class']][path] = dummy
for k, li in self._non_store.items():
if k not in dbusmonitoritems:
dbusmonitoritems[k] = {}
for p in li:
dbusmonitoritems[k][p] = dummy
return dbusmonitoritems
def handle_new_service(self, servicename, instance):
logger.debug("handle_new_service: %s" % servicename)
serviceclass = servicename.split('.')
if len(serviceclass) < 3 or serviceclass[1] != 'victronenergy':
return
serviceclass = serviceclass[2]
if serviceclass == 'pvinverter':
self._add_pvinverter_service(servicename, instance)
elif serviceclass == 'battery':
# Battery services handling is based on the active battery service
# provided by systemcalc, which also changes when active battery
# service disappears from the D-Bus. There is however a race
# condition where vrmlogger sees the new battery after
# systemcalc, causing the battery to be ignored. To ensure that
# the battery is always added, add the battery (again) if it matches
# the service name specified by systemcalc.
if self._battery_service is not None and self._battery_service.service == servicename:
self._add_battery_service(servicename, instance)
elif serviceclass == 'system':
# If systemcalc is somehow restarted we need to reconnect to the /ActiveBatteryService item.
self._dbusdeltas.add_service(servicename, instance, 'system')
self._setup_active_battery_monitor()
elif serviceclass == 'multi':
self._add_multirs_service(servicename, instance)
elif serviceclass in ('vebus', 'solarcharger', 'grid', 'genset', 'dcmeter', 'inverter', 'generator', 'evcharger'):
self._dbusdeltas.add_service(servicename, instance, serviceclass)
def handle_removed_service(self, servicename, instance):
self._dbusdeltas.remove_service(servicename)
def handle_acinput_config_changed(self):
services = set(self._dbusdeltas.get_all_services('pvac.grid') + \
self._dbusdeltas.get_all_services('pvac.genset') + \
self._dbusdeltas.get_all_services('pvac.output'))
# Clear deltas conforming to the old config
self._dbusdeltas.remove_all_services('pvac.grid')
self._dbusdeltas.remove_all_services('pvac.genset')
self._dbusdeltas.remove_all_services('pvac.output')
# Add pv-inverters back, using new config
for service, instance in services:
self._add_pvinverter_service(service, instance)
# Core of this class. Performs calculations and returns all kwhdeltas in a dict which is formatted to
# be sent straight to VRM.
def getdeltas(self, refreshbaseline):
# Make sure the ActiveBatteryService is added to dbusdeltas. Due to
# the posibility of race conditions in the availability of energy
# data during startup/boot, the service may have been skipped at
# that time. If the ActiveBatteryService is not added to dbusdeltas,
# then try again.
if self._battery_service is not None and not self._dbusdeltas.has_service(self._battery_service.service):
self._add_battery_service(self._battery_service.service, self._battery_service.instance, no=(lambda s: None))
baselinetimestamp, deltas, rawdeltas, servicecounts = self._dbusdeltas.get_deltas(refreshbaseline)
grid_in = deltas['grid']['/Ac/Energy/Forward']
grid_out = deltas['grid']['/Ac/Energy/Reverse']
# patch to fix ETH-340
grid_net = grid_in - grid_out
if grid_net > 0:
grid_in = grid_net
grid_out = 0
else:
grid_in = 0
grid_out = -grid_net
# end patch
genset_in = deltas['genset']['/Ac/Energy/Forward']
pvac_grid = deltas['pvac.grid']['/Ac/Energy/Forward']
pvac_genset = deltas['pvac.genset']['/Ac/Energy/Forward']
pvac_out = deltas['pvac.output']['/Ac/Energy/Forward']
pvdc = deltas['solarcharger']['/Yield/User']
vebus = deltas['vebus']
inverter = deltas['inverter']
multi_grid = deltas['multi.grid'] # Values for Multi-RS
multi_genset = deltas['multi.genset']
acgrid_to_dc = vebus['/Energy/GridToDc'] + multi_grid['/Energy/AcIn1ToInverter']
acgrid_to_acout = vebus['/Energy/GridToAcOut'] + multi_grid['/Energy/AcIn1ToAcOut']
acgenset_to_dc = vebus['/Energy/GensetToDc'] + multi_genset['/Energy/AcIn1ToInverter']
acgenset_to_acout = vebus['/Energy/GensetToAcOut'] + multi_genset['/Energy/AcIn1ToAcOut']
acout_to_dc = vebus['/Energy/AcOutToDc'] + \
inverter['/Energy/OutToInverter'] + \
multi_grid['/Energy/OutToInverter'] + \
multi_genset['/Energy/OutToInverter']
acout_to_acgrid = vebus['/Energy/AcOutToGrid'] + multi_grid['/Energy/AcOutToAcIn1']
acout_to_acgenset = vebus['/Energy/AcOutToGenset'] + multi_genset['/Energy/AcOutToAcIn1']
dc_to_acgrid = vebus['/Energy/DcToGrid'] + multi_grid['/Energy/InverterToAcIn1']
dc_to_acout = vebus['/Energy/DcToAcOut'] + \
inverter['/Energy/InverterToAcOut'] + \
multi_grid['/Energy/InverterToAcOut'] + \
multi_genset['/Energy/InverterToAcOut']
grid_to_consumption = 0
pv_to_consumption = inverter['/Energy/SolarToAcOut'] + \
multi_grid['/Energy/SolarToAcOut'] + \
multi_genset['/Energy/SolarToAcOut']
bat_to_consumption = 0
genset_to_consumption = 0
pv_to_bat = inverter['/Energy/SolarToBattery'] + \
multi_grid['/Energy/SolarToBattery'] + \
multi_genset['/Energy/SolarToBattery']
pv_to_grid = 0
has_dc_system = self._dbusmonitor.get_value('com.victronenergy.settings',
'/Settings/SystemSetup/HasDcSystem')
# Due to a bug in the BMV 70x firmware (v3.07 and earlier), the ChargedEnergy and DischargedEnergy
# are not reliable, so we use these values only when we really need them: if a DC system is present.
# In that case we need the Charged/Discharged energy to compute the DC consumption.
# You can find the old algorithm in vrmlogger v2.42. This version would scale the vebus kWh values
# using the battery kWh values.
if servicecounts['battery']['/History/DischargedEnergy'] > 0 and has_dc_system:
bat_in = deltas['battery']['/History/DischargedEnergy']
bat_out = deltas['battery']['/History/ChargedEnergy']
pvdc, bat_out, pvdc_to_bat = _distribute(pvdc, bat_out)
else:
dc_to_ac = dc_to_acgrid + dc_to_acout
pvdc_to_bat = max(0, pvdc - dc_to_ac)
bat_in = max(0, dc_to_ac - pvdc)
# Note that in the if-branch above, bat_out is already distributed to pvdc_to_bat. In this case
# we can compute pvdc_to_bat directly. We skip the call to _distribute and do not add
# pvdc_to_bat to bat_out, so bat_out will be the charged battery energy minus pvdc_to_bat.
# Also we reduce pvdc with pvdc_to_bat like distribute does. After that bat_out and pvdc have
# the same meaning as they have when leaving the if-branch.
bat_out = acgrid_to_dc + acgenset_to_dc + acout_to_dc
pvdc -= pvdc_to_bat
if servicecounts['grid']['/Ac/Energy/Forward'] == 0:
grid_in = max(0, acgrid_to_dc + acgrid_to_acout - pvac_grid)
grid_out = dc_to_acgrid + acout_to_acgrid + multi_grid['/Energy/SolarToAcIn1']
if servicecounts['genset']['/Ac/Energy/Forward'] == 0:
genset_in = max(0, acgenset_to_dc + acgenset_to_acout - pvac_genset)
# This is how the code below works: we take a measured kWh value (eg. pvac_grid) and distribute it
# over a list of possible destinations (in case below acgrid_to_dc, grid_out, ac_grid_to_acout).
# The destinations are sorted by priority. So if acgrid_to_dc > pvac_grid, it is assumes that all
# energy from pvac_grid has flown to acgrid_to_dc, and there is nothing left for the others (in case
# below grid_out and ac_grid_to_acout).
# The distributed values are stored in pvac_in_to_dc, pvac_in_to_grid, pvac_in_to_acout. The energy
# in pvac_in_to_dc will be distributed later to construct PV to battery and PV to (DC) consumption.
# What is left of pvac_grid in the end is assumed to be part of the consumption.
pvac_grid, acgrid_to_dc, pvac_in_to_dc = _distribute(pvac_grid, acgrid_to_dc)
pvac_grid, grid_out, pvac_in_to_grid = _distribute(pvac_grid, grid_out)
pvac_grid, acgrid_to_acout, pvac_in_to_acout = _distribute(pvac_grid, acgrid_to_acout)
# Addition January 2022: The MultiRS has PV that ties onto the AC bus,
# like a PV-inverter, but it has separate energy counters for energy
# going to Ac-Out or the battery. Hence we bring in PV from the
# Multi-RS only after distributing pvac_grid to AC-Out and the battery.
# If there is any of grid_out left, we assume this comes from the
# Multi-RS. What remains must be part of consumption then.
pv_multirs_grid = multi_grid['/Energy/SolarToAcIn1']
pv_multirs_grid, grid_out, pv_multirs_in_to_grid = _distribute(pv_multirs_grid, grid_out)
# Now we can estimate how much of the PV went to consumption, and what
# ended up in the grid
pv_to_consumption += pvac_grid + pvac_in_to_acout + pv_multirs_grid
pv_to_grid += pvac_in_to_grid + pv_multirs_in_to_grid
# Now we distribute grid_in (energy taken from grid). Energy from grid is usually fed to the inverter
# (AC_In), but may also be consumed directly (Hub-4). The inverter knows which part is sent to AC_Out
# or the DC out.
grid_in, acgrid_to_dc, grid_to_dc = _distribute(grid_in, acgrid_to_dc)
grid_in, acgrid_to_acout, grid_to_acout = _distribute(grid_in, acgrid_to_acout)
grid_to_consumption += grid_in + grid_to_acout
pvac_genset, acgenset_to_dc, pvac_genset_to_dc = _distribute(pvac_genset, acgenset_to_dc)
pvac_genset, acgenset_to_acout, pvac_genset_to_acout = _distribute(pvac_genset, acgenset_to_acout)
pv_to_consumption += pvac_genset + pvac_genset_to_acout
genset_in, acgenset_to_dc, genset_to_dc = _distribute(genset_in, acgenset_to_dc)
genset_in, acgenset_to_acout, genset_to_acout = _distribute(genset_in, acgenset_to_acout)
genset_to_consumption += genset_in + genset_to_acout
pvac_out, acout_to_dc, pvac_out_to_dc = _distribute(pvac_out, acout_to_dc)
pvac_out, acout_to_acgrid, pvac_out_to_acgrid = _distribute(pvac_out, acout_to_acgrid)
pvac_out, acout_to_acgenset, pvac_out_to_acgenset = _distribute(pvac_out, acout_to_acgenset)
pv_to_consumption += pvac_out + pvac_out_to_acgenset
grid_to_dc, bat_out, grid_to_bat = _distribute(grid_to_dc, bat_out)
grid_to_consumption += grid_to_dc
genset_to_dc, bat_out, genset_to_bat = _distribute(genset_to_dc, bat_out)
genset_to_consumption += genset_to_dc
pvac_in_to_dc, bat_out, pvac_in_to_bat = _distribute(pvac_in_to_dc, bat_out)
pv_to_consumption += pvac_in_to_dc
pv_to_bat += pvac_in_to_bat
pvac_genset_to_dc, bat_out, pvac_genset_to_bat = _distribute(pvac_genset_to_dc, bat_out)
pv_to_consumption += pvac_genset_to_dc
pv_to_bat += pvac_genset_to_bat
pvac_out_to_dc, bat_out, pvac_out_to_bat = _distribute(pvac_out_to_dc, bat_out)
pv_to_consumption += pvac_out_to_dc
pv_to_bat += pvac_out_to_bat
bat_in, dc_to_acout, bat_to_acout = _distribute(bat_in, dc_to_acout)
bat_in, dc_to_acgrid, bat_to_acin = _distribute(bat_in, dc_to_acgrid)
bat_to_consumption += bat_to_acout
if has_dc_system:
bat_to_consumption += bat_in
pvdc, dc_to_acout, pvdc_to_acout = _distribute(pvdc, dc_to_acout)
pvdc, dc_to_acgrid, pvdc_to_acin = _distribute(pvdc, dc_to_acgrid)
pv_to_consumption += pvdc_to_acout
if has_dc_system:
pv_to_consumption += pvdc
pv_to_bat += pvdc_to_bat
pvdc_to_acin, grid_out, pvdc_to_grid = _distribute(pvdc_to_acin, grid_out)
pv_to_grid += pvdc_to_grid
pv_to_consumption += pvdc_to_acin
pvac_out_to_acgrid, grid_out, pvac_out_to_grid = _distribute(pvac_out_to_acgrid, grid_out)
pv_to_consumption += pvac_out_to_acgrid
pv_to_grid += pvac_out_to_grid
bat_to_acin, grid_out, bat_to_grid = _distribute(bat_to_acin, grid_out)
bat_to_consumption += bat_to_acin
result = {}
result['Gc'] = grid_to_consumption
result['Gb'] = grid_to_bat
result['gc'] = genset_to_consumption
result['gb'] = genset_to_bat
result['Pb'] = pv_to_bat
result['Pc'] = pv_to_consumption
result['Pg'] = pv_to_grid
result['Bc'] = bat_to_consumption
result['Bg'] = bat_to_grid
# Timers
system = deltas['system']
result['To'] = system['/Timers/TimeOff']
result['Tgs'] = system['/Timers/TimeOnGenerator']
result['Tg'] = system['/Timers/TimeOnGrid']
result['Ti'] = system['/Timers/TimeOnInverter']
# Generator run time
result['Tga'] = deltas['generator']['/AccumulatedRuntime']
for servicename, details in rawdeltas.items():
for path, tuple in details.items():
serviceclass = '.'.join(servicename.split('.')[0:3])
code = self._rawdeltas.get(serviceclass, {}).get(path)
logger.debug("service: %s, path: %s, code: %s" % (servicename, path, code))
if code:
result[code + "[" + str(tuple[0]) + "]"] = tuple[1]
# The timestamp sent with the kWh data is specified in minutes, but rounded to 15 minutes.
# Note that we use UTC timestamps here, because the old hourly kwhDeltas did so as well.
# The old implementation used time.gmtime.
date = datetime.utcfromtimestamp(baselinetimestamp)
date = date.replace(minute = date.minute - date.minute % 15, second=0, microsecond=0)
result['Et'] = date.strftime("%Y%m%d%H%M")
logger.debug("Calculated deltas, with baseline-timestamp: %s, age (m): %s" % (
baselinetimestamp,
(time.time() - baselinetimestamp) / 60))
return result
def _get_import_item(self, service_name, path, eventCallback=None):
if self._ve_item_gen is None:
return VeDbusItemImport(self._dbusmonitor.dbusConn, service_name, path, eventCallback)
else:
return self._ve_item_gen(service_name, path, eventCallback)
def _setup_active_battery_monitor(self):
""" This is called at startup, and when systemcalc comes up. """
try:
self._active_battery_item = self._get_import_item('com.victronenergy.system',
'/ActiveBatteryService',
lambda x,y,z: self._update_active_battery_service())
except DBusException:
self._dbusdeltas.remove_all_services('battery')
else:
self._update_active_battery_service()
def _update_active_battery_service(self):
battery_service = self._active_battery_item.get_value()
self._dbusdeltas.remove_all_services('battery')
self._battery_service = None
if battery_service is None:
no_battery_service(1); return
instance = battery_service.split('/')[1]
battery_service = battery_service.replace('.', '_').replace('/', '_')
service_mapping = self._get_import_item('com.victronenergy.system',
'/ServiceMapping/' + battery_service)
battery_service = service_mapping.get_value()
if battery_service is None:
no_battery_service(2); return
# Selected battery service may also be a vebus device, which should not be added as a battery
spl = battery_service.split('.')
if len(spl) < 3 or spl[2] != 'battery':
no_battery_service(3); return
# Store the battery service to work around a race condition. If the
# battery shows up later than systemcalc, we can use this information
# to call _add_battery_service later.
self._battery_service = BatteryService(battery_service, instance)
self._add_battery_service(battery_service, instance)
def _add_battery_service(self, battery_service, instance, no=no_battery_service):
# Avoid adding it twice
if self._dbusdeltas.has_service(battery_service):
no(9); return
# Skip battery services that don't have the counters we need. This will also filter out the
# BMV-60x series
if self._dbusmonitor.get_value(battery_service, '/History/DischargedEnergy') is None:
no(4); return
if self._dbusmonitor.get_value(battery_service, '/History/ChargedEnergy') is None:
no(5); return
# Filter out all battery monitors with bugs in their kwh counter implementation
fw_version = self._dbusmonitor.get_value(battery_service, '/FirmwareVersion')
product_id = self._dbusmonitor.get_value(battery_service, '/ProductId')
if product_id in (0x0203, 0x0204, 0x0205) and fw_version < 0x0308:
logging.info("ignoring bmv-70x with fw_version %x" % (fw_version if fw_version else 0))
no(6); return
if product_id == 0x0141 and fw_version < 0x0110:
logging.info("ignoring Lynx Shunt VE.Can with fw_version %x" % (fw_version if fw_version else 0))
no(7); return
if product_id == 0xA130 and fw_version < 0x0131:
logging.info("ignoring Lynx Ion + Shunt with fw_version %x" % (fw_version if fw_version else 0))
no(8); return
# The other used productids are:
# 0xB000 = Valence XP
# 0xB00* = Redflow ZBM, etc. etc.
logger.info('active battery service: %s' % battery_service)
self._dbusdeltas.add_service(battery_service, instance, 'battery')
def _add_pvinverter_service(self, servicename, instance):
# Split pv inverters in three groups: grid, genset and mains
# First get the position, then lookup the AC input type to
# find out what that means.
position = self._dbusmonitor.get_value(servicename, '/Position')
if position == 1:
pvtype = 'pvac.output'
else:
if self._dbusmonitor.get_value('com.victronenergy.settings',
'/Settings/SystemSetup/AcInput{}'.format(
2 if position == 2 else 1)) == 2:
pvtype = 'pvac.genset'
else:
pvtype = 'pvac.grid'
logger.debug("%s, p:%s = %s" % (servicename, position, pvtype))
self._dbusdeltas.add_service(servicename, instance, pvtype)
def _add_multirs_service(self, servicename, instance, count=5):
# FIXME: Only one input supported for now! No support for a future Quattro-RS yet
source = self._dbusmonitor.get_value(servicename, '/Ac/In/1/Type')
# If the source is not known yet, try again later.
if source is None and count > 0:
GLib.timeout_add(1000, self._add_multirs_service, servicename, instance, count-1)
return False
self._dbusdeltas.add_service(servicename, instance,
'multi.genset' if source == 2 else 'multi.grid')
return False
def _distribute(src, dest):
v = min(src, dest)
src -= v
dest -= v
return src, dest, v
# === All code below is to debug this class without needing to run the the full vrmlogger ===
kwhdeltas = None
def handle_timer():
try:
deltas = kwhdeltas.getdeltas(refreshbaseline=False)
return True
except:
import traceback
traceback.print_exc()
sys.exit(1)
# Called from the dbusmonitor
def handle_new_service(servicename, deviceinstance):
kwhdeltas.handle_new_service(servicename, deviceinstance)
# Called from the dbusmonitor
def handle_removed_service(servicename, deviceinstance):
kwhdeltas.handle_removed_service(servicename, deviceinstance)
def main():
global kwhdeltas
logging.basicConfig(level=logging.DEBUG)
# Have a mainloop, so we can send/receive asynchronous calls to and from dbus
DBusGMainLoop(set_as_default=True)
kwhdeltas = KwhDeltas()
kwhdeltas.start(
DbusMonitor(
dbusTree=kwhdeltas.get_dbusmonitortree(),
deviceAddedCallback=handle_new_service,
deviceRemovedCallback=handle_removed_service)
)
GLib.timeout_add(3000, handle_timer)
# Start and run the mainloop
logging.info("Starting mainloop, responding on only events from now on. Press ctrl-Z to see the deltas")
mainloop = GLib.MainLoop()
mainloop.run()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment