Modify DVCC's code so it account DC current even if it's not directly measured

I want to modify Victron’s DVCC feature’s code to make it use (take) and account “DC system’s consuming current” calculated by difference if there is no “DC meter”, for the DVCC’s feature “Limit Charing Current”.

I realize that most possibly this modification would not be welcomed at all by Victron’s developers, but at least I really want to understand the REASON why they didn’t make it this way or why it is just NOT possible maybe?

From the documentation: https://www.victronenergy.com/media/pg/CCGX/en/dvcc—distributed-voltage-and-current-control.html

11.4. DVCC features for all systems
11.4.1. Limit charge current: in short: “Limit charge current is a user-configurable maximum charge current setting”. Supposed to be the settable “maximum current” which can go into battery.
But, DC Loads may not be accounted for, unless a SmartShunt or BMV-712 is installed and correctly configured as a DC meter. For example, without the DC load monitor a configured maximum charge current of 50A and DC Loads drawing 20A, the battery will be charged with 30A, not with the full allowed 50A. With the SmartShunt configured as a DC meter, maximum charge current configured at 50A and DC system shunt reports a draw of 25A, then the chargers are set to charge with 50 + 25 = 75A.

Although, in the settings in GX device / Venus OS, If we set “has DC system” even if we don’t have DC meter then it anyway shows “DC power” calculating as remaining current after deducting from current of “PV power” the current consumed by battery (measured by shunt) and measured power of inverter (if any). So, the “DC loads current” still IS KNOWN. But, by some reason, the implementation of the DVCC’s feature “Limit charge current” they made it that way so that despite the current consumed by “DC system” is known, but it may not be accounted for, unless it is directly measured by a SmartShunt or BMV-712 configured as a “DC Meter”.

I found the following code (with comments) in the file: /opt/victronenergy/dbus-systemcalc-py/delegates/DVCC.py


 delegates/DVCC.py
 
 def dcsyscurrent(self):
   """ Return non-zero DC system current, if it is based on       
a real measurement. If an estimate/calculation, we cannot use it.   
"""   
if self._dbusservice['/Dc/System/MeasurementType'] == 1:       
try:           
v = self._dbusservice['/Dc/Battery/Voltage']          
 return self._dcsyscurrent.update(               
float(self._dbusservice['/Dc/System/Power'])/v)      
 except (TypeError, ZeroDivisionError):          
 pass 
  return 0.0

Is there some particular reason WHY it is impossible to use DC system current if it is “estimated/calculated” for the purpose to “compensate for DC loads” in DVCC’s “limit charge current” feature ?

What if I change the code in a following way, will it work ?

Details and modified code below:

So, I want to modify the behavior of the dcsyscurrent property in the Dvcc class so that It use a calculated DC current value (if no direct measurement is available).
The current calculation will be: [ “pv_current” = “/Dc/Pv/Power” divided by “/Dc/Battery/Voltage” ; If “battery_current” (measured by shunt as battery meter) is positive, then use dcsyscurrent = (pv_current - battery_current), if negative, then dcsyscurrent = (pv_current + abs(battery_current) ) ]

Currently, “dcsyscurrent” returns a value only if MeasurementType == 1. I want it to fall back to the calculated current if it’s not directly measured, and use it to account for (compensate for) in the DVCC’s “limit charge current” feature the same way as it originally uses directly measured DC loads.


 modified part of "delegates/DVCC.py"

def dcsyscurrent(self):
    """
    Return DC system current in Amps.

    Prefer real measurement if available.
    If not, calculate it as:
        (PV power / Battery voltage) +/- battery current
    depending on whether battery current is charging or discharging.
    """
    try:
        # Use directly measured value if available
        if self._dbusservice['/Dc/System/MeasurementType'] == 1:
            v = self._dbusservice['/Dc/Battery/Voltage']
            return self._dcsyscurrent.update(
                float(self._dbusservice['/Dc/System/Power']) / v)
    except (TypeError, ZeroDivisionError, KeyError):
        pass

    # Fallback: Calculate PV current and compute dcsyscurrent
    try:
        voltage = self._dbusservice['/Dc/Battery/Voltage']
        pv_power = self._dbusservice['/Dc/Pv/Power']
        battery_current = self._dbusservice['/Dc/Battery/Current']

        if None in (voltage, pv_power, battery_current):
            return 0.0

        # Calculate PV current
        pv_current = float(pv_power) / float(voltage)

        # Adjust based on battery current sign
        battery_current = float(battery_current)
        if battery_current < 0:
            dcsys = pv_current + abs(battery_current)
        else:
            dcsys = pv_current - battery_current

        return self._dcsyscurrent.update(dcsys)

    except (TypeError, ZeroDivisionError, KeyError, ValueError):
        return 0.0

(Actually , instead of calculating that pv_current via “/Dc/Pv/Power” / “/Dc/Battery/Voltage” I could simply take it from “Dc/Pv/Current”, but anyway)

Or, WHY it will NOT work?

They clearly said in their documentation that DVCC’s feature “Limit charge current” will work fully only if “DC loads” current measured physically by the additional shunt configured as “DC system meter”. This is different from that shunt which configured as “battery monitor”. The “battery monitor” measures current which goes in or out the battery, while “DC meter” measures the current consumed by DC loads. So, they wrote in the documentation that “Limit Charge current” will be limiting TOTAL current which goes from outside INTO the system, unless DC loads are measured directly - then it will account for it and limit only that current which goes into battery. (Victron’s inverters reporting their own consumption, so that current will be accounted for as well).
But, indeed, why not take that calculated “DC current” and use it for compensation logic ? I realize that maybe that way it will work not on ALL possible configurations, but in simple case it seems to me that it will work? Or I’m misunderstanding something here, that’s what I want to find out, first.
“Simple case” is the following:
I have Victron SmartSolar , Smart shunt for battery monitor, a battery and a non-Victron inverter together with some other DC loads. I want to set limit on battery charging current but in the same time I don’t want to limit the power which can come from SmartSolar to the loads. That DVCC looked as great feature for that purpose. With their (looking to me artificial) limitation it is not possible; unless I need an another shunt which will measure DC loads.
In their code of DVCC features they clearly wrote this comment: " Return non-zero DC system current, if it is based on
a real measurement. If an estimate/calculation, we cannot use it. " WHY ?
I want to find out if it’s possible to implement it this way…

Because if you have ever watched the calculated DC current then it can be erratic and it can be wrong. There are a couple of problems, one is that the sampling / polling times from different equipment is not consistent and secondly, the accuracy of the current measurement in different equipment varies. Making charger current respond to these fluctuations is not ideal.

I am having the same issue described here: Charging bugs with new Orion XS 1400 and Pylontech RT12100 - probably due to DVCC - #8 by alexpescaru

I am using managed BMS-Can battery which reports current with high accuracy so this should be comparable to SmartShunt. And in my case installing additional SmartShunt to measure DC loads is also highly impractical as loads’ negative pole is connected to chassis in different parts of the vehicle so I would have to install multiple Shunts or run additional negative wire to all loads from common Shunt.

1 Like

Thank you ! OK, that’s natural and perfectly understandable ! So , anyway, if I accept and agree to all those not precise and not stable “estimated measurements”, then will it anyway work if I changed that logic in the code ? I mean, is it going to work , if we “forget” about any possible wrong / unprecise “calculated” measurement ? And is it, to modify that logic accordingly, enough only change that parts what I changed in that code showed here or is it still need to change something else somewhere else also ? Thank you very much for help !

When are you using your Shunt as battery monitor, it doesn’t work properly? I understand that you don’t have additional Shunt for the DC loads, but when using Shunt as Battery Monitor, the Venus OS should have “real measurement” value of all the current leaving the battery and so it should compensate for that. Maybe I am mistaken and you need another Shunt.

Well… in my situation, I suppose, they are probably not considering the data coming from battery BMS as “real measurement” on par with their SmartShunt measurements, but in your case I would assume this works.

Btw. do you have to compile the whole OS if modifying the source code mentioned? or how would you go about testing it?

I am not expert on programming but I did once forked a code from GitHub and created fake Victron MPPT driver for EPEVER charger. It has been working for couple of years without hiccup… the only thing I don’t do in this one install is to update Venus OS as this deletes the driver and it has to be reinstalled and I can’t be bothered to check compatibility with each new version of the OS. Creating fake SmartShunt driver with your calculations in place so it shows as proper DC load meter system might be easier than modifying the OS directly.

Yes, the shunt as battery monitor itself is working properly. But DVCC is not accounting calculated DC loads to compensate and demand from solar charger to give more current than it is set as “limit charging current”, so when loads demands more , the charger will still give not more than that limit and current splits between battery and loads. Just because in the code it is explicitly said “report DC loads 0 if not directly measured” :slight_smile:
And no, that “directly measured” should be the another shunt connected before DC loads.

No, it seems not need to recompile the whole OS. DVCC.py is separate file and it’s Python - just can directly modify the code and it should work. I just want to make sure that it is all what’s need to modify, i.e no other “module” also contains other dependencies.

I was also thinking about some kind of “fake SmartShunt” and configure it as DC loads monitor. But I just have no idea how to create it. But then I found that part of code in DVCC.py - looks like it is enough to change that : “IF not directly measured - report 0.0” ! But may be in reality it’s not so simple. That’s what I want to find out, before testing my changes. I don’t want whole system crash:)

In that case you should just try it and report back if that works… if that will be the case I would gladly test this too.. My setup is not anything critical so I test this for sure

1 Like
1 Like

I will try ! :slight_smile:

I’m thinking I will go ahead and test if my modified simplified code will work as intended (Modify the following section of DVCC.py: )

just simply this way:

in the file /delegates/DVCC.py in the “def dcsyscurrent(self):” replace “return 0.0” with:

: return self._dcsyscurrent.update( float(self._dbusservice['/Dc/System/Power'])/v) except (TypeError, ZeroDivisionError):
(“v” was v = self._dbusservice[‘/Dc/Battery/Voltage’ (move it up out of “if” section)
(correct?)

But that’s just for test. Although by my opinion it’s anyway better than nothing, but I would like for real usage make it more advanced way: make it calculate 4-5 samples of “dc loads” and average and then use that for compensation. All that will be processed in the same DVCC.py module. The question is, how often that DVCC.py code runs? It will be need to code a following logic:
If “direct measurement” then return dc loads current as originally, If not direct, then:
1st run: return 0.0 or previous calculated value, but get at that moment dc loads and remember calculated value for future averaging in 5th run,
2nd run: return previous calculated value but get at that moment dc loads and add into remembered value for future averaging,
3rd, 4th run: the same,
5th run: average previous 4 runs together with this time: return averaged value, remember for next runs, and so on.
Unfortunately I’m not a programmer at all to implement this complicated logic and yet I don’t know many details of how the whole system of python modules work there, and at least how to / where to “remember” “between runs” of DVCC.py those values and number of “runs” needed for averaging. I hope that someone with knowledge could (and would) help me with this.

Because myself I don’t really know Python, I asked an AI to write a code:) It needs checking of course, that’s why I’m publishing it here: (I hope they will not delete this post like it is generated by AI, no, it is not, I’m myself writing it now here in my own words!).
Idea was:
If /Dc/System/MeasurementType == 1: then Use directly measured current (as originally) ,
Else: Calculate current as /Dc/System/Power ÷ /Dc/Battery/Voltage , Average this value over 5 cycles, returning the last average until the next full 5-point average is ready.
So far correct logic ?
If yes, then there is how we are going to code it:
We’ll add

  1. A list to store up to 5 recent calculated current values,
  2. A counter to track how many times we’ve stored values,
  3. A cached averaged value to return in between full recalculations.
    In other words:
    Until 5 samples are collected, it returns the last full average or 0.0 initially;
    After the 5th sample, it continuously averages the next 5;
    It resets the ring buffer and index.
    So far correct logic ?

Then the code: (really needs to re-check everything if all is correct by syntax and by logic !)

@property
def dcsyscurrent(self):
    """
    Return DC system current.
    If MeasurementType == 1, use directly measured value.
    Otherwise, average calculated value over 5 cycles.
    """
    try:
        if self._dbusservice['/Dc/System/MeasurementType'] == 1:
            v = self._dbusservice['/Dc/Battery/Voltage']
            return self._dcsyscurrent.update(
                float(self._dbusservice['/Dc/System/Power']) / v)
    except (TypeError, ZeroDivisionError, KeyError):
        return 0.0

    # Initialize state on first use
    if not hasattr(self, '_dcsyscurrent_samples'):
        self._dcsyscurrent_samples = []
        self._dcsyscurrent_avg = 0.0
        self._dcsyscurrent_index = 0

    try:
        v = self._dbusservice['/Dc/Battery/Voltage']
        p = self._dbusservice['/Dc/System/Power']
        sample = float(p) / float(v)
    except (TypeError, ZeroDivisionError, KeyError):
        return self._dcsyscurrent_avg

    # Add sample
    if len(self._dcsyscurrent_samples) < 5:
        self._dcsyscurrent_samples.append(sample)
        return self._dcsyscurrent_avg if self._dcsyscurrent_samples[:-1] else 0.0
    else:
        self._dcsyscurrent_samples[self._dcsyscurrent_index] = sample
        self._dcsyscurrent_index = (self._dcsyscurrent_index + 1) % 5
        self._dcsyscurrent_avg = sum(self._dcsyscurrent_samples) / 5
        return self._dcsyscurrent_avg

Hello ! Good news:)
Simple modified code works.
I tested it, it now limiting charge current TO BATTERY only, while yet rumps up charge controller with load demands. If then limit to battery reduced, it reduces current so no more than limit goes to battery. So, as intended.
Dvcc.py runs every 3 seconds. So there will be some delay. Also couple of seconds delay can be between loads changes while it retrieves calculated correct value with load demands changes. But overall very satisfactory.
At least it won’t last long when battery allowed to discharge due to limits with load increase.
The only part I modified in the DVCC.py is the following:

Originally this part is:

        @property
        def dcsyscurrent(self):
                """ Return non-zero DC system current, if it is based on
                    a real measurement. If an estimate/calculation, we cannot use it.
                """
                if self._dbusservice['/Dc/System/MeasurementType'] == 1:
                        try:
                                v = self._dbusservice['/Dc/Battery/Voltage']
                                return self._dcsyscurrent.update(
                                        float(self._dbusservice['/Dc/System/Power'])/v)
                        except (TypeError, ZeroDivisionError):
                                pass
                return 0.0

My changed code:

        @property
        def dcsyscurrent(self):
                """ Return non-zero DC system current, if it is based on
                    a real measurement. If an estimate/calculation, we cannot use it.
                """
# Vav_Tonga
                batt_volt = self._dbusservice['/Dc/Battery/Voltage']

                Dc_system_power_now = self._dbusservice['/Dc/System/Power']
                Dc_loads_current_now = Dc_system_power_now/batt_volt

                if self._dbusservice['/Dc/System/MeasurementType'] == 1:
                        try:
                                return self._dcsyscurrent.update(
                                        float(Dc_system_power_now)/batt_volt)
                        except (TypeError, ZeroDivisionError):
                                pass
# Vav_Tonga     return 0.0
                return self._dcsyscurrent.update(
                        float(Dc_system_power_now)/batt_volt)
# Vav_Tonga changed code ends

I want yet to implement averaged calculations of dcsyscurrent before update it. Previous code was not correct. And the idea can be even better: let’s assume samples are 1 2 3 4 5 6 7 8 9 10, we could average them taking (1+2+3+4+5) / 5, then (6+7+8+9+10) / 5. But can do it differently, first 1 2 3 4 5, then 2 3 4 5 6, then 3 4 5 6 7, then 4 5 6 7 8 and so on. Now, to implement it, as we found out, DVCC.py runs every 3 seconds, so with every run we still need to make it return current averaged value. But somewhere it have to “remember” previous 5 values between runs . That’s what I don’t know how to do.

P.S. Actually, even those complications above are in reality not necessary. Original code confused me - why they done this: “self._dbusservice[‘/Dc/System/Power’])/v” if there is possible this: self._dbusservice[‘/Dc/System/Current’]) ? Additional significant operation is not division by “v”, but retrieving /Dc/Battery/Voltage takes time.
Use the following code for changes. Replace that section in original dvcc.py file into this:

        @property
        def dcsyscurrent(self):
                """ Return non-zero DC system current, if it is based on
                    a real measurement. If an estimate/calculation, we cannot use it.
                """
# Vav_Tonga
                if self._dbusservice['/Dc/System/MeasurementType'] == 1:
                        try:
                                v = self._dbusservice['/Dc/Battery/Voltage']
                                return self._dcsyscurrent.update(
                                        float(self._dbusservice['/Dc/System/Power'])/v)
                        except (TypeError, ZeroDivisionError):
                                pass
# Vav_Tonga     return 0.0
                return self._dcsyscurrent.update(
                        float(self._dbusservice['/Dc/System/Current']))
# Vav_Tonga changed code ends 

I am traveling at the moment, but will be back next week and will try this as soon as possible

1 Like