ngardiner / TWCManager

Control power delivered by a Tesla Wall Charger using two wires screwed into its RS-485 terminals.
The Unlicense
127 stars 55 forks source link

Querying 2 inverters (Fronius) #380

Open raffiniert opened 2 years ago

raffiniert commented 2 years ago

Hey

we have a big PV system on the roof (22kWp) and use 2 inverters for it:

  1. Fronius Symo 7.0
  2. Fronius Symo Gen24

As I understand it, when I query the API (v1/GetPowerFlowRealtimeData.fcgi), I only get data from one of them. Currently, I get this:

{ "Body" : { "Data" : { "Inverters" : { "1" : { "DT" : 105, "E_Day" : 24256, "E_Total" : 9652380, "E_Year" : 8519768, "P" : 863 } }, "Site" : { "E_Day" : 24256, "E_Total" : 9652380, "E_Year" : 8519768, "Meter_Location" : "grid", "Mode" : "meter", "P_Akku" : null, "P_Grid" : -1691.22, "P_Load" : 828.22000000000003, "P_PV" : 863, "rel_Autonomy" : 100, "rel_SelfConsumption" : 0 }, "Smartloads" : { "Ohmpilots" : { "720896" : { "P_AC_Total" : 0, "State" : "normal", "Temperature" : 65.799999999999997 } } }, "Version" : "12" } }, "Head" : { "RequestArguments" : {}, "Status" : { "Code" : 0, "Reason" : "", "UserMessage" : "" }, "Timestamp" : "2021-10-28T17:23:57+02:00" } }

Is there a way from the API-side or from TWC-side to query both inverters?

Generally, both inverters produce almost exactly the same energy, as east- and westside of the cells is split evenly across them.

Thanks! Raphi

ngardiner commented 2 years ago

Not having a dual inverter setup myself I am not entirely sure how they work in a Fronius context but it sounds like it is a matter of two independent inverters which both have the Fronius API enabled, but each need to be queried separately and the values added together to get the overall values?

Would you be able to query each of the two inverters in the way you have above and check that the output looks effectively the same? The way TWCManager is structured this would actually be doable out of the box if the inverters were two separate makes/models (as our default way of dealing with multiple EMS modules enabled is to AND them together) but we do not instantiate multiple instances of the same inverter.

That said, it wouldn't be hard to make the module accept multiple inverters and AND them internally, however... we need to be sure first that they both interface the same way because if the APIs aren't identical it might need a different module anyway

raffiniert commented 2 years ago

thank you so much!

So, I found the API of the 2nd inverter, which returns the following:

{ "Body" : { "Data" : { "Inverters" : { "1" : { "DT" : 1, "P" : 434 } }, "Site" : { "BatteryStandby" : false, "E_Day" : null, "E_Total" : null, "E_Year" : null, "Meter_Location" : "unknown", "Mode" : "produce-only", "P_Akku" : null, "P_Grid" : null, "P_Load" : null, "P_PV" : 472.55364990234375, "rel_Autonomy" : null, "rel_SelfConsumption" : null }, "Version" : "12" } }, "Head" : { "RequestArguments" : {}, "Status" : { "Code" : 0, "Reason" : "", "UserMessage" : "" }, "Timestamp" : "2021-10-29T07:22:26+00:00" } }

So I think this looks exactly the same as the first for P_PV, but P_Load is only provided by the first inverter (I suspect only the 1st is connected to the smart meter), thus only one of them provides P_LOAD.

Does that help already?

raffiniert commented 2 years ago

@ngardiner any info missing from my side? Thanks! :-)

blach commented 2 years ago

I have a similar PV installation with 24.79 kWp:

I adapted the Fronius.py file as follows:

This has been working very reliably for about 10 days here.

You can find my modified Fronius.py file here:

# Fronius Datamanager Solar.API Integration (Inverter Web Interface)
import logging
import requests
import time

logger = logging.getLogger(__name__.rsplit(".")[-1])

class Fronius:

    cacheTime = 10
    config = None
    configConfig = None
    configFronius = None
    consumedW = 0
    fetchFailed = False
    generatedW = 0
    generatedW2 = 0
    akku = 0
    importW = 0
    exportW = 0
    lastFetch = 0
    master = None
    serverIP = None
    serverPort = 80
    serverIP2 = None
    serverPort2 = 80
    status = False
    timeout = 5 
    voltage = 0

    def __init__(self, master):
        self.master = master
        self.config = master.config
        try:
            self.configConfig = master.config["config"]
        except KeyError:
            self.configConfig = {}
        try:
            self.configFronius = master.config["sources"]["Fronius"]
        except KeyError:
            self.configFronius = {}
        self.status = self.configFronius.get("enabled", False)
        self.serverIP = self.configFronius.get("serverIP", None)
        self.serverPort = self.configFronius.get("serverPort", "80")
        self.serverIP2 = self.configFronius.get("serverIP2", None)
        self.serverPort2 = self.configFronius.get("serverPort2", "80")

        # Unload if this module is disabled or misconfigured
        if (not self.status) or (not self.serverIP) or (int(self.serverPort) < 1):
            self.master.releaseModule("lib.TWCManager.EMS", "Fronius")
            return None

    def getConsumption(self):

        if not self.status:
            logger.debug("Fronius EMS Module Disabled. Skipping getConsumption")
            return 0

        # Perform updates if necessary
        #self.update()

        generated = self.getGeneration()

        if not self.akku:
            self.akku = 0

        consumed = generated + float(self.consumedW) + float(self.akku)

        return consumed

    def getGeneration(self):

        if not self.status:
            logger.debug("Fronius EMS Module Disabled. Skipping getGeneration")
            return 0

        # Perform updates if necessary
        self.update()

        # Return generation value
        if not self.generatedW:
            self.generatedW = 0
        if not self.generatedW2:
            self.generatedW2 = 0

        return float(self.generatedW) + float(self.generatedW2)

    def getInverterData(self):
        url = "http://" + self.serverIP + ":" + self.serverPort
        url = (
            url
            + "/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&DeviceID=1&DataCollection=CommonInverterData"
        )

        return self.getInverterValue(url, False)

    def getInverterValue(self, url, shouldSetFailed):

        # Fetch the specified URL from the Fronius Inverter and return the data

        try:
            r = requests.get(url, timeout=self.timeout)
        except requests.exceptions.ConnectionError as e:
            logger.log(
                logging.INFO4,
                "Error connecting to Fronius Inverter to fetch sensor value",
            )
            logger.debug(str(e))
            if shouldSetFailed:
                self.fetchFailed = True
            return False

        r.raise_for_status()
        jsondata = r.json()
        return jsondata

    def getMeterData(self, ip, port, shouldSetFailed):
        url = "http://" + ip + ":" + port
        url = url + "/solar_api/v1/GetPowerFlowRealtimeData.fcgi?Scope=System"

        return self.getInverterValue(url, shouldSetFailed)

    def getSmartMeterData(self):
        url = "http://" + self.serverIP + ":" + self.serverPort
        url = url + "/solar_api/v1/GetMeterRealtimeData.cgi?Scope=System"

        return self.getInverterValue(url, False)

    def update(self):

        if (int(time.time()) - self.lastFetch) > self.cacheTime:
            # Cache has expired. Fetch values from Fronius inverter.

            self.fetchFailed = False

            #inverterData = self.getInverterData()
            #if inverterData:
            #    try:
            #        if "UAC" in inverterData["Body"]["Data"]:
            #            self.voltage = inverterData["Body"]["Data"]["UAC"]["Value"]
            #    except (KeyError, TypeError) as e:
            #        logger.log(
            #            logging.INFO4, "Exception during parsing Inveter Data (UAC)"
            #        )
            #        logger.debug(e)

            meterData = self.getMeterData(self.serverIP, self.serverPort, True)
            if meterData:
                try:
                    self.generatedW = meterData["Body"]["Data"]["Site"]["P_PV"]
                    self.akku = meterData["Body"]["Data"]["Site"]["P_Akku"]
                except (KeyError, TypeError) as e:
                    self.generatedW = 0
                    self.akku = 0
                    logger.log(
                        logging.INFO4,
                        "Exception during parsing Meter Data (Generation)",
                    )
                    logger.debug(e)

            meterData2 = self.getMeterData(self.serverIP2, self.serverPort2, False)
            if meterData2:
                try:
                    self.generatedW2 = meterData2["Body"]["Data"]["Site"]["P_PV"]
                except (KeyError, TypeError) as e:
                    self.generatedW2 = 0
                    logger.log(
                        logging.INFO4,
                        "Exception during parsing Meter Data 2 (Generation)",
                    )
                    logger.debug(e)

            smartMeterData = self.getSmartMeterData()
            if smartMeterData:
                try:
                    self.consumedW = smartMeterData["Body"]["Data"]["0"]["PowerReal_P_Sum"]
                    self.voltage = smartMeterData["Body"]["Data"]["0"]["Voltage_AC_Phase_1"] 
                except (KeyError, TypeError) as e:
                    self.consumedW = 0
                    logger.log(
                        logging.INFO4,
                        "Exception during parsing Smart Meter Data (Consumption)",
                    )
                    logger.debug(e)

            # Update last fetch time
            if self.fetchFailed is not True:
                self.lastFetch = int(time.time())

            return True
        else:
            # Cache time has not elapsed since last fetch, serve from cache.
            return False
deece commented 2 years ago

As an alternative, I just sent a PR to add support for Open Energy Monitor (openenergymonitor.org). You can pull your inverter data into that, then combine the generation feeds into a new feed aggregating production for both, before passing the data to TWCManager.

I have a complex setup with 4 inverters + 2 standalone chargers, and this works well.

raffiniert commented 2 years ago

thanks @blach & @deece ! Will look into it these days. @ngardiner no more updates from your side? Did I not send all the information you need?

raffiniert commented 2 years ago

@blach I was finally able to install it correctly, I edited the config.json to include both IPs of both inverters, and I replaced the Fronius.py with your new code. Thank you so much so far!

However, I get a plain "0" on all meters on the web-dashboard. While it's already late and only some fraction of power comes in, it's still around 100W in total.

I checked the following responses:

http://[ip]/solar_api/v1/GetPowerFlowRealtimeData.fcgi http://[ip]/solar_api/v1/GetPowerFlowRealtimeData.fcgi http://[ip]/solar_api/v1/GetMeterRealtimeData.cgi?Scope=System

and the corresponding attributes in the JSONs, and they return what I would expect.

Any idea where I could look?

Oh, I also dropped the loglevel to 17 - where is it supposed to log? To console?

Maybe I have to add that it is still not connected to my TWC - is that the culprit?

raffiniert commented 2 years ago

Seems I get an update on the numbers when I press the buttons on the bottom of the page, like "charge now"... looked in the code, there is a cache, but it's only set to 10s?

blach commented 2 years ago

Did you connect the software to your Tesla wall connector yet? If I remember correctly, it didn't query the inverters for me before it successfully communicated with the TWCs.

It's also necessary that the policy is set to "Solar Surplus". If it isn't, the inverters aren't queried either.

I assume you have a Fronius Smart Meter connected to your Gen24? Make sure that serverIP points to the Gen24 and serverIP2 points to the Symo 7.0.

If that still doesn't work, one difference might be that I have a battery in my system and you don't. Not sure if that would cause different inverter outputs.

blach commented 2 years ago

Maybe I have to add that it is still not connected to my TWC - is that the culprit?

Oh, I just saw that. Yes, that is most likely the problem.

raffiniert commented 2 years ago

awesome. Sounds like I'll be ready to connect it tomorrow... thanks so much!

raffiniert commented 2 years ago

@blach I will go outside and connect it now for the first time, though I still suspect something is off. I added both inverter-IPs to config.json, I exchanged Fronius.py with your file, but I only see the wattage of the first inverter on the web-dashboard. Don't understand this yet...

raffiniert commented 2 years ago

Oh, and should I be able to set the policy to "solar surplus" on the web dashboard? I cannot change anything there on those 3 tabs, it's on "fixed rate/amps" and the others are unclickable.

raffiniert commented 2 years ago

this is my Fronius-entry in the /etc/TWCManager/config.json:

` "Fronius": { "enabled": true, "serverIP": "10.0.x.x", "serverIP2": "10.0.x.y" },

`

and yes, the IPs are correct...

blach commented 2 years ago

Oh, and should I be able to set the policy to "solar surplus" on the web dashboard? I cannot change anything there on those 3 tabs, it's on "fixed rate/amps" and the others are unclickable.

No, it's selected according to your settings. I have set "Non-Scheduled power action" to "Track Green Energy".

raffiniert commented 2 years ago

I just saw this, thanks! This part seems okay now.

Solar tracking is still only working for one of the inverters... :-(

blach commented 2 years ago

@blach I will go outside and connect it now for the first time, though I still suspect something is off. I added both inverter-IPs to config.json, I exchanged Fronius.py with your file, but I only see the wattage of the first inverter on the web-dashboard. Don't understand this yet...

Are you sure it is using the modified file? Did you install it using setup.py or how are you running it?

Maybe you can add a log statement to Fronius.py and see it if is printed to the log to make sure it using the right file.

raffiniert commented 2 years ago

I seem to be having some issues with basic UNIX :-) sorry. Basically, I mounted the raspi-"disk" via afp to my Mac to modify files. I did so by using netatalk.

I am now ssh'ing into the file system to look at the file.

Do I need to run setup.py again after the file is changed?! :-o

blach commented 2 years ago

I don't know. I don't use Docker.

While developing I ran it directly from the directory using sudo -u twcmanager python -m TWCManager.

raffiniert commented 2 years ago

I uninstalled docker as well and am running a manual install now! I am running it using the service, like:

sudo cp contrib/twcmanager.service /etc/systemd/system/twcmanager.service sudo systemctl enable twcmanager.service --now

blach commented 2 years ago

That runs the package installed in the system using setup.py.

To debug the problem, try to stop the service and run it directly from the terminal from the twcmanager directory using the command I posted above.

Something like:

sudo systemctl stop twcmanager
cd TWCManager # change to the directory containing TWCManager.py
sudo -u twcmanager python -m TWCManager

After a moment you'll see the log output in the terminal.

You can then quit the program using Ctrl-C.

raffiniert commented 2 years ago

I did that, thanks. I've set the loglvl to 16, added a log-stmt to see the values, and now the whole thing is bricked and doesn't start up. git diff doesn't show anything that looks wrong.

Really upset about myself I don't seem to be able to handle this.

raffiniert commented 2 years ago

getting this on startup:

` Traceback (most recent call last): File "/usr/local/lib/python3.7/dist-packages/commentjson-0.9.0-py3.7.egg/commentjson/commentjson.py", line 180, in loads parsed = _remove_trailing_commas(parser.parse(text)) File "/usr/local/lib/python3.7/dist-packages/lark_parser-0.7.8-py3.7.egg/lark/lark.py", line 311, in parse return self.parser.parse(text, start=start) File "/usr/local/lib/python3.7/dist-packages/lark_parser-0.7.8-py3.7.egg/lark/parser_frontends.py", line 89, in parse return self._parse(token_stream, start, [sps] if sps is not NotImplemented else []) File "/usr/local/lib/python3.7/dist-packages/lark_parser-0.7.8-py3.7.egg/lark/parser_frontends.py", line 54, in _parse return self.parser.parse(input, start, args) File "/usr/local/lib/python3.7/dist-packages/lark_parser-0.7.8-py3.7.egg/lark/parsers/lalr_parser.py", line 36, in parse return self.parser.parse(*args) File "/usr/local/lib/python3.7/dist-packages/lark_parser-0.7.8-py3.7.egg/lark/parsers/lalr_parser.py", line 84, in parse for token in stream: File "/usr/local/lib/python3.7/dist-packages/lark_parser-0.7.8-py3.7.egg/lark/lexer.py", line 373, in lex for x in l.lex(stream, self.root_lexer.newline_types, self.root_lexer.ignore_types): File "/usr/local/lib/python3.7/dist-packages/lark_parser-0.7.8-py3.7.egg/lark/lexer.py", line 174, in lex raise UnexpectedCharacters(stream, line_ctr.char_pos, line_ctr.line, line_ctr.column, allowed=allowed, state=self.state, token_history=last_token and [last_token]) lark.exceptions.UnexpectedCharacters: No terminal defined for 'X' at line 224 col 1

X        # a URL such as:
^

Expecting: {'RBRACE', 'RSQB', 'TRAILING_COMMA', 'COLON'}

Previous tokens: Token(ESCAPED_STRING, '"/dev/serial0"')`

While it is of course true that I had to change the interface from USB to serial (because I'm using a RS485 hat), I don't see anything wrong with that line:

"port": "/dev/serial0"

blach commented 2 years ago

Sounds like there is a stray "X" character in line 224 of config.json after the port setting?

raffiniert commented 2 years ago

no, there isn't... and I removed the whole TMCManager-directory now, checked it out from github again, make install ... same error.

This is driving my crazy :-D

blach commented 2 years ago

Maybe you can attach your /etc/twcmanager/config.json file.

raffiniert commented 2 years ago

hell no...

why is there one config.json at /etc/twcmanager/config.json and one at /TWCManager/etc/twcmanager/config.json?! :-D That maybe could be the issue

raffiniert commented 2 years ago

Running and up again. Anyway, maybe then I'm also editing the wrong Fronius.py?

Btw., I would like to buy you a beer. Or a few of them, tbh.

raffiniert commented 2 years ago

oh man! I think I am onto it... I need to run make install after the Fronius.py is changed, as this was compiled by that process into pyc, correct?

If so, I'm sorted...

blach commented 2 years ago

Yes.

raffiniert commented 2 years ago

still the same. I re-ran make install after changing the file, same result. Only one inverter (the first) is queried.

blach commented 2 years ago

Then I can only recommend to add some logging statements to see what's going on.

raffiniert commented 2 years ago

I think the files are cached somewhere else... if I add log-statements, they aren't fired, so it seems it doesn't matter what I change on the Fronius.py Maybe easiest if I start all over again (empty disk) and change the Fronius.py before the first make install...

blach commented 2 years ago

To debug the problem, try to stop the service and run it directly from the terminal from the twcmanager directory using the command I posted above.

Something like:


sudo systemctl stop twcmanager

cd TWCManager # change to the directory containing TWCManager.py

sudo -u twcmanager python -m TWCManager

After a moment you'll see the log output in the terminal.

You can then quit the program using Ctrl-C.

This should still work. It doesn't used the precompiled package but directly uses the .py files.

raffiniert commented 2 years ago

yes, it does.

What I did:

logger.info( "generatedW", self.generatedW )

&

logger.info( "generatedW2", self.generatedW2 )

this is seemingly not the correct way to log, but the output is still readable:

TypeError: not all arguments converted during string formatting ... Message: 'generatedW' Arguments: (None,)

&

TypeError: not all arguments converted during string formatting ... Message: 'generatedW2' Arguments: (0.0,)

while the json of 1 is returning "P_PV" : null, and 2 is "P_PV" : 0.0,

so I think it might be valid now (it's dark outside) :-D

raffiniert commented 2 years ago

it's working now! All good.

Can I buy you a beer?

blach commented 2 years ago

it's working now! All good.

Can I buy you a beer?

Great to hear that it works now. No worries, I was struggling with very similar issues before it worked for me and I'm just glad that I can help someone else by sharing what I found.

blach commented 2 years ago

I'm curious: what was the problem in the end? Did you have to change something in the config file or Fronius.py?

Willy81 commented 2 years ago

New user here. I just got TWCManager up and going. I have a similar use-case. I have a Fronius Primo (6.6kW of PV) and a Redback battery system (5.8kW PV, 13kWh of batts). I have the Fronius smart meter installer, but obviously the generation is incorrect. Forgive me as I'm very new to TWCManager and how it works. But for use-cases where mine where one system is not metered, would it be possible to make TWCManager use the power at the grid connection instead? For example, you are charging at 6kW, the Fronius smart meter measures 2kW export to the grid, therefore TWCManager can increase the charge power to 8kW. I'm not sure if this is possible. Perhaps it should be split to a separate feature.

Edit: I just had a look at what blach did above. I'm going to try something similar by estimating the generation as 2 * generation (similar sized systems) and back-calculate the consumption from the grid power measured by the meter. I'll trial this for a few days and see how it goes. As soon as I deployed it, clouds rolled over. Haha