APsystems APS ECU R local inverters data pull

adriaanh, seems like that the ECU software is very instable or disabling port 8899 in case of misuse.
the 8897 to the portal stays open without any problems, also the HTTP function is available. Mine is also now upgraded to 2.0.1, without any improvement.

the error is on me… the top graph is energy usage only. Need to recheck against produced energy :slight_smile:

I am testing now with scan_interval: 360 which is now functioning properly for 24 hours. Maybe this will help, I’ll keep you posted.

The ECU only updates every 5 minutes anyway, so I’m not sure a 6 minute interval will really impact the data. The ECU device is really pretty bad, and certainly has stability issues if you push it too hard. I’d assume that you have a newer ECU that would be better, but if anything sounds like yours maybe less stable than my ~ 1 year old one.

Kyle, why sometimes after restart a scond set of ecu entities are created with _2. You need then to disable apsystems_ecur integration and remove entities and add integration again.

i;ve seen that mostly happen when communcation was unstable… now with lately some updates on HA and restarts, it dont happen as it was behaving like good boy. But yes i’ve had those _2 entitie too before… some delete and other restarts did the trick (never disabled the integration for it)

Maybe can be changed if the integration is already exists and otherwise not create new one? Hardly people using more then one ECU.

I don’t think I have any control over how and when home assistant loads the integration. HA is responsible for instantiating it, and I just produce the sensors according to their API. HA decides what to name them and will automatically make an _2 one if it already exists with the same name. As far as I can tell there is no way for an integration to actually pull data from HA to be able to determine if there are ones already made (I could be wrong, but in all the sample code I looked at this was never shown).

Kyle, I agree. But seems I have only problems with the duplicate entities of apsystems_ecur. I dont know yet what causing it. Looking to the source code also. But is does it regularly when restart/updating HA. And he doesnt need to do it, when entitie already exist. SO something is different for HA to treat it as a different entity, while actually all information is the same, except timestamp info. Is the timestamp the trigger maybe? Strange thing is that he only doing it for the sensor.ecu_ not for the inverter entities, they stay on unavailable after reload.

My ECU-C has a problem with empty queries after couple hours of normal work.
Response on first query is empty, on second (with ECU ID) I have APS110018000201END. I think about rework script to read html page instead of faulty
socket on 8899 port.

Roman, Let us know your development stages.

Ok, so I have first stage - pre-component code. Next I’ll enumerate second page (http://ecu_internal_ip/index.php/realtimedata) to fetch detailed info about inverters.
Next I’ll recode original HA component apsystems_ecur to make my first HA component ever :slight_smile:

import pandas as pd
import re
url = r'http://192.168.1.220'
tables = pd.read_html(url)
pve_table = tables[0]
# to see all table just print it out :
# print (pve_table)
lifetime_generation = float(re.search(r'\d+\.\d+', pve_table[1][1]).group())
current_day = float(re.search(r'\d+\.\d+', pve_table[1][3]).group())
current_power = int(re.search(r'\d+', pve_table[1][2]).group())
print (f"Current power {current_power} W. Today's production {current_day} kWh.")
print (f"Lifetime generation : {lifetime_generation} kWh")

For those who don’t own ECU-C, little explanation - at http://internalurl/ in source html we have only one table : (same thing on second page with detailed data)

image

Page without any login/pass procedure, advanced anti-robot things etc. So data fetching is so simple.

I really wish the ECU-R had the web page (or just an http json dump), seems like such a silly thing to limit to only the “commercial” monitor, and would be a very simple addition to the firmware.

If you create an APSystemsECUC class with similar methods and data structure to the existing APSsystemsECUR class we should be able to make one integration supporting both ECUs. Then just add a flag to the yaml, or try to autodetect based on a response to the http request.

I don’t have a “C” device to test, but I’d be happy to do what I can to help integrate this into the existing custom component if you want.

1 Like

Sure ! I’ll let you know when I complete my code.

The ECU-R 2.0.x had also a webpage. Only the Home is not working, but the /index.php/realtimedata, power_graph, energy_graph and Administration (Id maangement, Grid profile, Data-Time-TimeZone, Laguage, Network Connectivity, WLAN & Firmware Update)
But strange thing is, that I disabled all integration and even then last night the ECU-R was offline (cloud LED was not blinking) but the APS portal and ECU-R communication was still working. Really a weird device and not stable, even think about replacement.

Hi,
HTTP webpage doesn’t provide signal info but everything else is working.
I wrote new class, after switching to the my one, integration work fine.
Class :

class APSystemsECUHttp:

    def __init__(self, ipaddr, port=8899, raw_ecu=None, raw_inverter=None):
        self.ipaddr = ipaddr
# port is not needed but can be safetly deleted ?
# i don't wanna as a noob to touch other files like __init or so
        self.port = port
        self.summary_url = f"http://{self.ipaddr}"
        self.details_url = f"http://{self.ipaddr}/index.php/realtimedata"

        self.qs1_ids = [ "802", "801", "804", "806" ]
        self.yc600_ids = [ "406", "407", "408", "409" ]
        self.yc1000_ids = [ "501", "502", "503", "504" ]

        self.ecu_id = None
        self.qty_of_inverters = 0
        self.lifetime_energy = 0
        self.current_power = 0
        self.today_energy = 0
        self.inverters = []
        self.firmware = None
        self.timezone = None
        self.last_update = None

        self.ecu_raw_data = raw_ecu
        self.inverter_raw_data = raw_inverter
        self.inverter_raw_signal = None

    def dump(self):
        print(f"ECU : {self.ecu_id}")
        print(f"Firmware : {self.firmware}")
        print(f"TZ : {self.timezone}")
        print(f"Qty of inverters : {self.qty_of_inverters}")

    async def read_url(self, session, url):
        async with session.get(url) as resp:
            return await resp.read()

    async def async_query_ecu(self):
        tasks = []
        async with aiohttp.ClientSession() as session:
            _LOGGER.info(f"Asking page {self.summary_url} for a ECU data")
            for url in [self.summary_url, self.details_url]:
                tasks.append(asyncio.create_task(self.read_url(session, url)))
            html_pages = await asyncio.gather(*tasks)
            summary_table = HTMLTableParser()
            summary_table.feed(html_pages[0].decode('utf-8'))
            self.process_ecu_data(summary_table.tables[0])

            details_table = HTMLTableParser()
            details_table.feed(html_pages[1].decode('utf-8'))
            data = self.process_inverter_data(details_table.tables[0])

            data["ecu_id"] = self.ecu_id
            data["today_energy"] = self.today_energy
            data["lifetime_energy"] = self.lifetime_energy
            data["current_power"] = self.current_power

            return(data)

    def process_ecu_data(self, data=None):
        # if not data:
        #     data = self.ecu_raw_data
        self.ecu_id = data[0][1]
        self.qty_of_inverters = data[5][1]
        self.firmware = data[7][1]
        self.timezone = data[8][1]
        self.lifetime_energy = float(re.search(r'\d+\.\d+', data[1][1]).group())
        self.today_energy = float(re.search(r'\d+\.\d+', data[3][1]).group())
        self.current_power = int(re.search(r'\d+', data[2][1]).group())

    def process_inverter_data(self, data=None):
        # if not data:
        #     data = self.inverter_raw_data
        output = {}
        output["inverter_qty"] = self.qty_of_inverters
        output["inverters"] = {}
        inverters = {}
        for index in range(1, len(data)):
            row = data[index]
            inverter_uid = str(row[0].split('-')[0])
            if not inverter_uid in inverters:
                inverters[inverter_uid] = {}
                inverters[inverter_uid]["uid"] = inverter_uid
                inverters[inverter_uid]["online"] = True
                inverters[inverter_uid]["frequency"] = float(re.search(r'\d+\.\d+', row[2]).group()) if row[1] != '-' else 0
                inverters[inverter_uid]["temperature"] = int(re.search(r'\d+', row[4]).group()) if row[1] != '-' else 0
                inverters[inverter_uid]["signal"] = 100
                inv_model_id = inverter_uid[0:3]
                if inv_model_id in self.qs1_ids:
                    inv_model = "QS1"
                elif inv_model_id in self.yc600_ids:
                    inv_model = "YC600"
                elif inv_model_id in self.yc1000_ids:
                    inv_model = "YC1000"
                else:
                    inv_model = "Unkown"
                inverters[inverter_uid]["model"] = inv_model
                inverters[inverter_uid]["channel_qty"] = 0
                inverters[inverter_uid]["power"] = []
                inverters[inverter_uid]["voltage"] = []
                output["timestamp"] = str(row[5])
                self.last_update = output["timestamp"]
            panel_id = int(row[0].split('-')[1])
            inverters[inverter_uid]["channel_qty"] += 1
            inverters[inverter_uid]["power"].append(int(re.search(r'\d+', row[1]).group()) if row[1] != '-' else 0)
            if panel_id == 1:
                inverters[inverter_uid]["voltage"].append(int(re.search(r'\d+', row[3]).group()) if row[1] != '-' else 0)
            else:
                inverters[inverter_uid]["voltage"].append(int(re.search(r'\d+', row[2]).group()) if row[1] != '-' else 0)
        output["inverters"] = inverters
        return (output)

To proper run requires to import :

import asyncio
import aiohttp
import logging
from .table_parser import HTMLTableParser
import re

table_parser is a file from html-table-parser-python3/parser.py at 4c09d60eca655653c8cb064fa3c84a518024d932 · schmijos/html-table-parser-python3 · GitHub I just put this file in components directory
aiohttp can be added to requirements in manifest.json file (If I understand it correctly becouse i just do pip3 install aiohttp on my HA machine).

Previous “draft” version based on “Pandas” library wasn’t working correctly becouse it doesn’t support asyncio work required by HA. So I switched to above solution.

To test this out outside HA i just add at the end :

ecu = APSystemsECUR("192.168.1.220")
data = asyncio.run(ecu.async_query_ecu())
print (data)

and just have this output : (formated by me to be pretty and IDs was hidden)

 {
   "inverter_qty":"6",
   "inverters":{
      "HIDDEN":{
         "uid":"HIDDEN",
         "online":true,
         "frequency":50.0,
         "temperature":25,
         "signal":100,
         "model":"QS1",
         "channel_qty":4,
         "power":[
            48, 48, 52, 52
         ],
         "voltage":[
            235, 235, 235, 235
         ]
      },
      "HIDDEN":{
         "uid":"HIDDEN",
         "online":true,
         "frequency":50.0,
         "temperature":28,
         "signal":100,
         "model":"QS1",
         "channel_qty":4,
         "power":[
            45, 46, 50, 50
         ],
         "voltage":[
            234, 234, 234, 234
         ]
      },
      "HIDDEN":{
         "uid":"HIDDEN",
         "online":true,
         "frequency":50.0,
         "temperature":29,
         "signal":100,
         "model":"QS1",
         "channel_qty":4,
         "power":[
            46, 47, 50, 50
         ],
         "voltage":[
            232, 232, 232, 232
         ]
      },
      "HIDDEN":{
         "uid":"HIDDEN",
         "online":true,
         "frequency":50.0,
         "temperature":27,
         "signal":100,
         "model":"QS1",
         "channel_qty":4,
         "power":[
            46, 46, 0, 0
         ],
         "voltage":[
            234, 234, 234, 234
         ]
      },
      "HIDDEN":{
         "uid":"HIDDEN",
         "online":true,
         "frequency":50.0,
         "temperature":28,
         "signal":100,
         "model":"QS1",
         "channel_qty":4,
         "power":[
            46, 46, 49, 50
         ],
         "voltage":[
            232, 232, 232, 232
         ]
      },
      "HIDDEN":{
         "uid":"HIDDEN",
         "online":true,
         "frequency":50.0,
         "temperature":29,
         "signal":100,
         "model":"QS1",
         "channel_qty":4,
         "power":[
            46, 47, 50, 49
         ],
         "voltage":[
            233, 233, 233, 233
         ]
      }
   },
   "timestamp":"2021-08-19 17:28:38",
   "ecu_id":"HIDDEN",
   "today_energy":22.94,
   "lifetime_energy":396.95,
   "current_power":1059
}

To be more compilant with original component and to proper handle temperatures below 0 C we must change 1 line and add another one :

inverters[inverter_uid]["temperature"] = int(re.search(r'\d+', row[4]).group()) if row[1] != '-' else 100
inverters[inverter_uid]["temperature"] *= -1 if row[4] == '-' else inverters[inverter_uid]["temperature"]

First line sets 100 when inverter goes down (after sunset) and we have ‘-’ in table.In next line we change a sign of int when first char of “temperature” is ‘-’. Simple but works in both scenarios (minus temperatures or ‘-’ on turned off inverter)

How is the HTTP integration option going?

The current version of the integration will probably cause issues when the November version of Home Assistant is released. In the logfile of HA, you can see what the problem is:

Entity sensor.ecu_lifetime_energy (<class 'custom_components.apsystems_ecur.sensor.APSystemsECUSensor'>) with state_class measurement has set last_reset. Setting last_reset for entities with state_class other than 'total' is deprecated and will be removed from Home Assistant Core 2021.11. Please update your configuration if state_class is manually configured, otherwise report it to the custom component author.

I already opened a ‘issue’ for this on Github a while ago, but haven’t seen any update yet.
Please fix this in advance, before the HA version 2021.11 will be released.