Solar PV intregration

Has anyone had any success with integrating a Solis 4G inverter in to their system.
Data is sent from the inverter every 5 mins or so to the server at the manufacturers Ginlong in China.
I know a few had tried to setup a seconadary server running MQTT, but getting the data out from the
packets sent is not easy.
Ginlong provide a website you can log into to get the generation data from your system and there is also an android app to do the same, but cant find any API info.
It may be something on the Chinese pages/

The only parameter I’m interested in is the power being generated, but there maybe an alternative to
pulling data from the inverter.
I have a device which measures the power level and once a threshold is reached switches power to the electric water heater, the load power to the heater is balanced using PWM so that nothing is imported from the grid to heat the water.
If I could find a low cost solution for a AC RMS voltmeter with a wireless <2.4 GHz> interface I could measure the voltage and if below a given level when its known hot water will be needed turn on the gas boiler to get the water up to temperature.
Hence the reason I’ve been playing with temperature sensors and HA.
In the summer its easy, but as we approach winter you cant rely on solar generated electricty to heat the water, so the idea is to use both gas and electricty and use a computer with HA to minimise the gas usage. the Sun is free, the gas is not

Maybe this can helpe u with.

Many thanks.
I’ve had a look at that, I ried to use PIP to load some of the python modules, but the way i have HA installed wont allow super user to use PIP, strange ???, I’m also not sure how well running two versions of python will work.
There may be an even simpler way to get the info I need.
From HA history graph I can see the rate the hot water is cooling overnight, and getting weather info
from darksky I should get a very good idea how much light there will be in the morning.
ie. if there is enough hot water needed for a shower at 0900 and sunrise is at 0730, I know how long 100 ltrs takes to lose heat , so by watching the graphs over a week or so I should be able to see or calculate the increase in temperature due to sloar heating.
I have historical data on how much power is generated in the first two hours from sunrise, its never much more than 300W even in summer.
I suspect unless its raining or overcast , on a clear day the energy generated thoughout the year will be similar.
IF the data from darksky is accurate , it should possible to simply run an algorithm to determine the point where the gas boiler needs to be started to boost the water temperature.
Sorta loop:-
If pressure high AND not raining AND clear sky AND sun is up , goto temp loop, else turn on boiler
temp loop
get water temperature
wait 5 mins
get water temp
has water temp increased by n degrees IF not turn on boiler
Read Met data again go to start of main loop
Also need to take account of the time and when the water is need
It will al ldepend on IF the data from DarkSky is accurate enough and how many degrees the water has to be heated by.
Should be possible

Maybe u can use emoncms.org like this guy.

I dont know what you finally settle with but I am using scrape component its been working well so far. below is the sample (in sensor yaml) code for posterity. replace your pid in the url. you can also create sensors for other values like today, total, etc.

- platform: scrape
  name: live_watts
  resource: http://ginlongmonitoring.com/Terminal/TerminalMain.aspx?pid=xxxxx
  select: "#ctl00_childPanel_lblNow"
  scan_interval: 300

hi did you bypass the login, i’m having errors: Unable to extract data from HTML


the answer is to make account public.

I moved over to the below solution since then. But to answer your question, you need to make your ginlong site to public in order to access with out a password.

I just created a command-line sensor to execute the python script.

2 Likes

I am currently creating an addon for Inverter-Data-Logger. In this case I am running the InverterServer, and my inverter is pushing values to that server. I may extend it to also allow polling for other inverter types. I am using pv output as intermediate, but maybe I will also add direct integration.

I published my initial version of the add-on here: https://github.com/silvester747/hassio-inverter-data-logger

2 Likes

Thanks for this option! Just one question: where can I find the correct naming for the other sensors?

Hi All, I see a couple of different options but I’m not sure how to implement any of these. I’m new to Home Assistant. I tried the published add-on from silvestor747 but i can’t get it to work. What step am i missing here?

The XtheOne solution seems to be for domoticz and need to install phyton on home assistant. Or is it not possible to run this directly from HomeAssitant? (I’m running hassos_rpi4-3.13.img.gz)

Scraping the website is also unclear. It seems that you need to disable security on the website and is only capable of scraping one value?

What is the best way to proceed and can anybody point me in the right direction. I just want the values avaialble in HA for dashboarding/trending purposes for now.

I altready found out the infropmation can also be scraped using json:
https://m.ginlong.com/cpro/epc/plantview/view/doPlantList.json
https://m.ginlong.com/cpro/epc/plantDevice/inverterListAjax.json?orderBy=updateDate&orderType=2&pageIndex=1&plantId= &sequenceNum=9&showAddFlg=1
However again, unknow how to get these values in HA. Probably only works after authentication also.

Hi
I started the thread and have kept an eye on its progression.
I have found problems with the ginlong site, it is not 100% reliable as well as the problem everything depends on conectivity in both directions.
For me the simplest solution was to use one of the Shelly EM monitors and with two sensors
you can easily monitor the amount generated , and whats coming in from the grid.
So any appliance use can be quickly controlled to maximise the generated power.
I power a hotwater heater off the solar power, the power consumption reduced from 3KW to 1.5KW by adding a series diode, by local monitoring I can very quickly disconnect the water heater if the power generated drops to less than a set threshold.
With monitoring the power taken from the grid if excess power is taken, ie. if the kettle is plugged in, the water heater can be disconnected until the kettle or other appliance has finished use. The time lag on the ginlong site is too long for me to use for rapid monitoring.
Thats just what I ended up doing.

I had a domoticz script running that scraped the internal website of the wifi stick. However I just switched to HA and searching for a way to intergrate these values. I believe I could your scrape but I have no clue how to start using this.

Can you share how you enabled this script in HA?
It mentions output to Domoticz but not HA.

Thank you so much for these pointers, do you happen to know a good way to not only display the info from the scrape but also how I would add this to the InfluxDB I have setup?

I upload my solar data to pvoutput.org. so I have added some custom code to PVoutputOutput.py file in “outputs” folder to create a few http sensors in HA. here is the full file for your reference.

import PluginLoader
from datetime import datetime
from pytz import timezone
import sys
#added by custom
import requests
import json
import sqlite3
#import MySQLdb
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
#end

if sys.version[:1] == '2':
    import urllib
    import urllib2
else:
    import urllib.request, urllib.parse, urllib.error

class PVoutputOutput(PluginLoader.Plugin):
    """Sends the data from the inverter logger to PVoutput.org"""

    def process_message(self, msg):
        """Send the information from the inverter to PVoutput.org.

        Args:
            msg (InverterMsg.InverterMsg): Message to process

        """

        timezoner = timezone('Australia/Adelaide')
        now = datetime.now(timezoner)

        ##commented by custom
        #if (now.minute % 5) == 0:  # Only run at every 5 minute interval

        sys_id = 'sysid-'+msg.id
        if not self.config.has_option('pvout', sys_id):
            self.logger.error('no sysid in configuration file for inverter with ID: {0}, skipping.'.format(msg.id))
            return []

        api_key = 'apikey-'+msg.id
        if not self.config.has_option('pvout', api_key):
            self.logger.error('no apikey in configuration file for inverter with ID: {0}, skipping.'.format(msg.id))
            return []

        self.logger.info('Uploading data from inverter with ID: {0} to PVoutput'.format(msg.id))

        url = "http://pvoutput.org/service/r2/addstatus.jsp"

        self.logger.debug('temperature: '+str(msg.temp)) # err:514,7
        self.logger.debug('AC1 voltage: '+str(msg.v_ac(1)))
        self.logger.debug('PV1 voltage: '+str(msg.v_pv(1)))
        self.logger.debug('e_today    : '+str(msg.e_today))
        self.logger.debug('e_total    : '+str(msg.e_total))
        self.logger.debug('total E    : '+str(((((msg.e_today*10)-(int(msg.e_today*10)))/10)+msg.e_total)))

        get_data = {
            'key': self.config.get('pvout', api_key),
            'sid': self.config.get('pvout', sys_id),
            'd': now.strftime('%Y%m%d'),
            't': now.strftime('%H:%M'),
            'v1': round(msg.e_today * 1000), #rounded by rgogada
            'v2': round(msg.p_ac(1)),        #rouned by rgogada
        }
        if self.config.getboolean('general', 'use_temperature'):
            # sometimes the inverter gives 514,7 as temperature, don't send temp then!
            if msg.temp < 300:
                get_data.update ({
                    'v5': msg.temp,
                })
            else: self.logger.error('temperature out of range: '+str(msg.temp))

        get_data.update ({
            'v6': msg.v_pv(1)
        })

        self.logger.debug('get_data....done')

        ####################################################################################
        ## added by custom
        ####################################################################################

        headers = {
            'Authorization': 'Bearer xxxxxxxxxxxxxxxxx',
            'Content-Type': 'application/json'
        }
       
        solar_power_prod =  0 if round(msg.p_ac(1)) < 200 else round(msg.p_ac(1))
        
        ## create http sensor solar_power_prod
        url_hass = 'http://192.168.1.130:8123/api/states/sensor.solar_power_prod'
        data = json.dumps({'state': solar_power_prod, 'attributes': {'unit_of_measurement': 'W', 'friendly_name': 'solar_power_prod'}})
        response = requests.post(url_hass, verify=False, headers=headers, data=data)

        self.logger.debug('solar_power_prod: ' + str(solar_power_prod))

        ## create http sensor solar_energy_prod
        url_hass = "http://192.168.1.130:8123/api/states/sensor.solar_energy_prod"
        data = json.dumps({"state":round(msg.e_today,2), 'attributes': {'unit_of_measurement': 'kWh', 'friendly_name': 'solar_energy_prod'}})
        response = requests.post(url_hass, verify=False, headers=headers, data=data)

        self.logger.debug('solar_energy_prod: ' + str(round(msg.e_today,2)))

        if (now.minute % 5) == 0: ## export to pvoutput only at 5 minute interval

            # add solar_energy_consumption (v3) to pvoutput
            # solar_energy_consumption equals to solar_energy_prod minus solar_energy_feed
            
            url_hass = "http://192.168.1.130:8123/api/states/sensor.solar_energy_feed"
            response = requests.get(url_hass, verify=False, headers=headers)
            solar_energy_feed = float(response.json()['state'])

            solar_energy_consumption = round((msg.e_today - solar_energy_feed)*1000) #no decimals
            get_data.update ({
                'v3': solar_energy_consumption 
            })

            self.logger.debug('solar_energy_consumption: ' + str(solar_energy_consumption))

            # # add solar_power_consumption (v4) to pvoutput
            # # solar_power_consumption equals to solar_power_prod minus solar_power_feed

            url_hass = "http://192.168.1.130:8123/api/states/sensor.solar_power_feed"
            response = requests.get(url_hass, verify=False, headers=headers)
            solar_power_feed = int(response.json()['state'])

            solar_power_consumption = round(msg.p_ac(1) - solar_power_feed) #no decimals
            get_data.update ({
                'v4': solar_power_consumption 
            })

            self.logger.debug('solar_power_consumption: ' + str(solar_power_consumption))

        ####################################################################################
        ## end
        ####################################################################################

            if sys.version[:1] == '2':
                get_data_encoded = urllib.urlencode(get_data)
                self.logger.debug(url + '?' + get_data_encoded)
                request_object = urllib2.Request(url + '?' + get_data_encoded)

                try:
                    response = urllib2.urlopen(request_object)
                except urllib2.HTTPError as e:
                    self.logger.error('HTTP error : '+str(e.code)+' Reason: '+str(e.reason))
                    return []
                except urllib2.URLError as e:
                    self.logger.error('URL error : '+str(e.args)+' Reason: '+str(e.reason))
                    return []
                else:
                    self.logger.debug(response.read())  # Show the response
            else:
                get_data_encoded = urllib.parse.urlencode(get_data)
                self.logger.debug(url + '?' + get_data_encoded)
                request_object = urllib.request.Request(url + '?' + get_data_encoded)

                try:
                    response = urllib.request.urlopen(request_object)
                except urllib.error.HTTPError as e:
                    self.logger.error('HTTP error : '+str(e.code)+' Reason: '+str(e.reason))
                    return []
                except urllib.error.URLError as e:
                    self.logger.error('URL error : '+str(e.args)+' Reason: '+str(e.reason))
                    return []
                else:
                    self.logger.debug(response.read())  # Show the response

        else:
            self.logger.debug('Not sending to PVoutput, not within 5 minutes interval.')

1 Like

Thanks for sharing, better late then never :slight_smile:

In the mean time I found an alternatieve way to retrieve the values from the internal webinterface using the HA node red add-on.

The scrape node does:
curl ‘http://:@192.168.x.x/status.html’

Then the grab nodes use regex to grab the values.

Example for the grab current power value:

var parts = msg.payload.match(/webdata_now_p\s=\s"(\d+)"/);
msg.payload = {
    value: parts[1],
};
return msg;
1 Like

Is it possible to share this flow in Nodered, looks like exactly what I need to read from my inverter.

1 Like

Sure.
I only repalced the URL containing username, password and IP in the SCRAPE node.

[{"id":"a6c69280.aefa","type":"tab","label":"Solis","disabled":false,"info":""},{"id":"8d3fd5ff.a575d8","type":"mqtt out","z":"a6c69280.aefa","name":"Publish MQTT Message","topic":"","qos":"","retain":"true","broker":"36d42be1.7a03e4","x":1150,"y":500,"wires":[]},{"id":"6315810.eba6a8","type":"ping","z":"a6c69280.aefa","mode":"timed","name":"Ping Solis Convertor","host":"192.168.2.25","timer":"60","inputs":0,"x":110,"y":280,"wires":[[]]},{"id":"9977c825.5888d8","type":"switch","z":"a6c69280.aefa","name":"Check if convertor is online","property":"payload","propertyType":"msg","rules":[{"t":"false"},{"t":"gt","v":"0","vt":"num"}],"checkall":"true","repair":false,"outputs":2,"x":200,"y":400,"wires":[["71244c20.72e364","b51741e5.39a3d"],["5af351cf.e8d92"]]},{"id":"24252177.a3d88e","type":"function","z":"a6c69280.aefa","name":"Grab current power value","func":"var parts = msg.payload.match(/webdata_now_p\\s=\\s\"(\\d+)\"/);\nmsg.payload = {\n    value: parts[1],\n};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":470,"y":480,"wires":[["e7aaad99.13a0c"]]},{"id":"5af351cf.e8d92","type":"exec","z":"a6c69280.aefa","command":"curl 'http://<USER>:<PASS>@192.168.X.X/status.html'","addpay":true,"append":"","useSpawn":"false","timer":"10","oldrc":false,"name":"Scrape web interface","x":180,"y":480,"wires":[["24252177.a3d88e","35f76747.bda408","10bd3393.9a038c"],[],[]]},{"id":"71244c20.72e364","type":"change","z":"a6c69280.aefa","name":"Prepare message","rules":[{"t":"set","p":"topic","pt":"msg","to":"Solis_Power_Current","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":340,"wires":[["8d3fd5ff.a575d8"]]},{"id":"e7aaad99.13a0c","type":"change","z":"a6c69280.aefa","name":"Prepare message","rules":[{"t":"set","p":"topic","pt":"msg","to":"Solis_Power_Current","tot":"str"},{"t":"move","p":"payload.value","pt":"msg","to":"payload","tot":"msg"},{"t":"delete","p":"rc","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":480,"wires":[["a54e4318.609bb"]]},{"id":"35f76747.bda408","type":"function","z":"a6c69280.aefa","name":"Grab total power today value","func":"var parts = msg.payload.match(/webdata_today_e\\s=\\s\"(\\S+)\"/);\nmsg.payload = {\n    value: parts[1],\n};\nreturn msg;","outputs":1,"noerr":0,"x":480,"y":520,"wires":[["ec1f587c.99d7a8"]]},{"id":"ec1f587c.99d7a8","type":"change","z":"a6c69280.aefa","name":"Prepare message","rules":[{"t":"set","p":"topic","pt":"msg","to":"Solis_Power_Today","tot":"str"},{"t":"move","p":"payload.value","pt":"msg","to":"payload","tot":"msg"},{"t":"delete","p":"rc","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":520,"wires":[["8d3fd5ff.a575d8"]]},{"id":"10bd3393.9a038c","type":"function","z":"a6c69280.aefa","name":"Grab total power value","func":"var parts = msg.payload.match(/webdata_total_e\\s=\\s\"(\\S+)\"/);\nmsg.payload = {\n    value: parts[1],\n};\nreturn msg;","outputs":1,"noerr":0,"x":460,"y":560,"wires":[["8a30c6ed.9b75d8"]]},{"id":"8a30c6ed.9b75d8","type":"change","z":"a6c69280.aefa","name":"Prepare message","rules":[{"t":"set","p":"topic","pt":"msg","to":"Solis_Power_Total","tot":"str"},{"t":"move","p":"payload.value","pt":"msg","to":"payload","tot":"msg"},{"t":"delete","p":"rc","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":560,"wires":[["8d3fd5ff.a575d8"]]},{"id":"b51741e5.39a3d","type":"timecheck","z":"a6c69280.aefa","name":"Midnight","time":"23:58","x":500,"y":400,"wires":[["4869bc6c.1cf2c4"],[]]},{"id":"4869bc6c.1cf2c4","type":"change","z":"a6c69280.aefa","name":"Prepare message","rules":[{"t":"set","p":"topic","pt":"msg","to":"Solis_Power_Today","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":380,"wires":[["8d3fd5ff.a575d8"]]},{"id":"a54e4318.609bb","type":"function","z":"a6c69280.aefa","name":"to kW","func":"msg.payload = Number(msg.payload);\nmsg.payload = (msg.payload)/1000;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":890,"y":480,"wires":[["8d3fd5ff.a575d8"]]},{"id":"d3756928.2025d8","type":"ping","z":"a6c69280.aefa","mode":"triggered","name":"Ping Solis Convertor","host":"","timer":"20","inputs":1,"x":180,"y":240,"wires":[["9977c825.5888d8"]]},{"id":"2311af6d.9be32","type":"inject","z":"a6c69280.aefa","name":"Ping trigger","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"60","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"host\":\"192.168.2.25\",\"timeout\":15000}]","payloadType":"json","x":130,"y":160,"wires":[["d3756928.2025d8"]]},{"id":"36d42be1.7a03e4","type":"mqtt-broker","z":"","name":"mqtt_broker","broker":"localhost","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]
1 Like

Thank you @Frank_R , this works perfectly for me