APsystems APS ECU R local inverters data pull

So i’ve been looking around to have solar data inside HA without using cloud api from APSystems. The newer versions of the ECU-R dont have a webinterface anymore, so that was no longer an option. Together with some other folks, I first started to trace the data pull from their own ECU app, which normally only connects when the local WIFI SSID is enabled.
This showed it pulled binary data over TCP layer on port 8899. After some time we found most bytes to be usefull and i created a first python script to fetch and output for HA consumption.
Please bear with me, I have no dev skills at all, so what you see is dirty as it can be. I’m hoping for some support here to clean this up and maybe even a dev can pick it to make it an official integration.

import socket
import struct
import binascii
import json
import datetime
from struct import unpack


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.1.xx", 8899 ))


# datasend = bytes.fromhex('41505331313030323830303032323136303030303631373238454E440A')

mystring ='APS1100280002216000061728END'
datasend = mystring.encode('utf-8')  
s.send(datasend)
dataecu =  s.recv(1024)
#dataecu =  bytes.fromhex('415053313130313736303030323030303100072020112412051040800009401601303101f3006f001400e4001400e440800009562201303101f3006f001300e4001400e440800009182601303101f3006f001400e3001400e340800009293301303101f3006f001400e3001300e340800009191301303101f3006f001500e3001400e340800009243401303101f3006f001400e3001400e340800009184001303101f3006f001400e2001400e2454e440a')



# print (dataecu)
s = binascii.b2a_hex (dataecu)

timestamp =  str(s[38:52])
# print (timestamp)
inverterid1 = str(s[52:64])
#print (inverterid1)
inverterid1online = str(bool(s[64:66]))
#print (inverterid1online)
something1 = str(int(s[66:68],16))
something2 = str(int(s[68:70],16))
#print (something1 +" " + something2)
inverterid1freq = str(int(s[70:74],16)/10)
#print (inverterid1freq + " Hz")
inverterid1temp = str(int(s[76:78],16)-100)
# print (inverterid1temp + " C")
inverterid1powerA = str(int(s[78:82],16))
# print (inverterid1powerA + " Watt")
inverterid1volt = str(int(s[82:86],16))
# print (inverterid1volt + " V")
inverterid1powerB = str(int(s[86:90],16))
# print (inverterid1powerB + " Watt")


# f = open("/home/pi/PI/.homeassistant/custom_components/ecudata.txt", "a")
print("{ \"Time\": \"" + timestamp  + "\", \"inv1\": \"" + inverterid1 + "\", \"inv1on\": \"" + inverterid1online + "\", \"inv1freq\": \"" + inverterid1freq + "\", \"inv1temp\": \"" + inverterid1temp + "\", \"inv1powerA\": \"" + inverterid1powerA +"\", \"inv1volt\": \""+ inverterid1volt + "\", \"inv1powerB\": \"" + inverterid1powerB + "\"} ")

So above will kind of output a json thing (cant really call it json nor object :-)).
This is just for one inverter, i have 7, so I can repeat this for each inverter, as bytes are repeated for other inverters in next parts of hex stream.

in HA some templates

  - platform: command_line
    name: ECUDataInverter1
    command: "python3 /home/pi/PI/.homeassistant/custom_components/apsinv1.py"
    scan_interval: 240
    command_timeout: 5
    json_attributes:
      - Time
      - inv1
      - inv1powerA
      - inv1powerB
      - inv1volt
      - inv1temp

and templates to make right sensors:

    sensors:
      ecu_inverter_1_powera:
        value_template: '{{ states.sensor.ecudatainverter1.attributes["inv1powerA"] | float }}'
        unit_of_measurement: 'W'
      ecu_inverter_1_powerb:
        value_template: '{{ states.sensor.ecudatainverter1.attributes["inv1powerB"] | float }}'
        unit_of_measurement: 'W'
      ecu_inverter_1_temp:
        value_template: '{{ states.sensor.ecudatainverter1.attributes["inv1temp"] | float }}'
        unit_of_measurement: 'C'

note, the Temperature is wrong, still need to fix this.

Resulting in frontend thing like:

So, happy so far with Proof of Concept. But things could be improved:

  • query IP adress to fetch ECU id, so no need for hardcoding
  • based on response of command, it also tells the number of inverters present, so this could help to loop and fetch all inverter data
  • output as real json (way above my grade)
  • handle ‘no update’ scenario as only a new timestamp would give new data
  • create real HA integration component would be very nice

anyone to share thoughts or chip in, i’m open for suggestions

I’m interested in helping. I have an AP systems solar with 7 of the 4 channels QS1 inverters. I currently have integrated data from the API, but only at the entire system level, not at each panel - which I would love to have. I do have quite a bit of python experience, and dabbled in making my own home assistant integrations - and could certainly work on the code some.

A couple of questions on your example, what is this?

mystring ='APS1100280002216000061728END'

Looks like the last part is your ECU ID, but what is the first part?

Does the ECU need to be in the local wifi mode, or does querying port 8899 work even when it’s connected to your standard network?

Do you have a full breakdown of each byte from the binary data that is returned?

In the second screen of the ECUapp this string is being send to the ECU-R, only thing we know until now is that this command retrieves all the inverters and parameters.


This works with all three methods; local AP, Wifi and network cable (make sure IP-adresses are correct and port = TCP 8899). In your situation having the QS1 I expect that Channels ABCD are being returned before the next inverter data follows in the reply string. To be determend, I’m writing a Windows application to resolve this.

I’ll do a little further testing tonight, but as of right now, my ECU-R isn’t listening on tcp port 8899. It is connected to their EMA service via it’s ethernet cable, and sending data correctly. I also know I have the correct IP address. I’ll try putting it in setup mode, by pressing the button on the ECU and seeing if I can get further. If I can get it to listen, and give me data, I’ll send that your way so maybe you’ll have some more info to help parse out the format.

‘isnt listening’, meaning you cant ping the port or you cant have python connect? I noticed a port scan didnt reveal this port to be usable, but just making an tcp connect, it did just work. Also i have no issues having ecu report to EMA site and pulling data together

I can’t connect to port 8899.

$ python apsystems.py
Traceback (most recent call last):
  File "apsystems.py", line 10, in <module>
    s.connect(("192.168.0.192", 8899 ))
ConnectionRefusedError: [Errno 111] Connection refused

same if I try something like netcat

$ netcat -v 192.168.0.192 8899
netcat: connect to 192.168.0.192 port 8899 (tcp) failed: Connection refused

the device is up

$ ping 192.168.0.192
PING 192.168.0.192 (192.168.0.192) 56(84) bytes of data.
64 bytes from 192.168.0.192: icmp_seq=1 ttl=255 time=0.248 ms
64 bytes from 192.168.0.192: icmp_seq=2 ttl=255 time=0.275 ms

that’s messed up then… Do yo have any http call succesfull on 80 or 443?
and maybe a firmware version of your ecu?

Nope, doesn’t listen on 80 or 443 either. I’ll put it in the AP mode and check out the ECU app tonight and see if I can get anything useful like firmware version, etc. My ECU ID number is higher than yours, and it’s a pretty new install (only about 2 months old).

mine is also just few weeks ‘old’ , went live on 14 november :slight_smile:

cant imagine they closed this part too already. You sure there is no local firewall hampering this 8899 port anywhere?

Re-looking at the numbers, mine actually mine might be an older unit, my ecu number is 2216000047832. No firewall between the box i’m querying it from and the ecu - all on the same subnet and switch.

That’s strange, the ECUapp connects to the ECU-R on port 8899. Are you able to connect with the temporary WiFi accesspoint using the ECUapp (SSID=ECU_R_216xxxxxxx)? There’s no doubt this should work because you have used this method to initially setup the ECU-R with this app. Maybe you’ve got a firewall or other software preventing traffic to this port? What firmware version does your ECU-R have (v1.2.13)? What country are you at? A port scan using nmap.org did reveal port 8899 to be open.

I will have to try the temporary access point tonight, but I have accessed it in the past (after pressing the button on the device). It’s currently not broadcasting that SSID. Once I connect again, I’ll pull firmware version, and any other useful details.

I’m in the US, I didn’t set it up initially my contractor did (not sure if that would have a difference). I did a portscan with nmap and 8899 is not open.

Could it be that your contractor closed the port? If nmap did not reveal any port this could be the issue. Do you have a support contract? It is known that registered/offical installers have more options to service the PV set. I’m a self-installer. Only way to find out is indeed try to connect using the button on the ECU-R and find out if the centralized distributed ECUapp wants to connect. Mind that there is also a USB port that might have been used for special purposes like locking the device. Installation manual only says “reserved”. Ask the contractor to re-open the port for the purposes you want. Don’t know if there are special support contracts between official installers and APS that prohibit these actions in the US, local laws might apply. Option I just came up with… when the inverters are online, the ECU-R posts the data to the EMA site. Capture this traffic and use that (reroute the traffic on your LAN). Analysis of data package is needed though.

Notice: Sending commands to the ECU-R can alter the configuration if you don’t know what you’re doing!

i did not self install, service company just setup the ecu and inverterid’s, didnt see any config options to change stuff. I did apply for installer account on EMA, maybe i should try to use that and see if more options come up.

Ok, checked on the device tonight. Here’s what I found. If I power cycle it, it will start broadcasting it’s ECU_R_XXXX SSID. I connect on my phone and the ECU app works, I’m running the 1.2.13 firmware. I double checked in the LAN settings section to make sure the IP address was correct, and it is. Still no response on the 8899 tcp port from my computer with netcat or nmap.

I then proceeded to connect my computer to the ECU_R_XXX SSID and was assigned an IP address in the 10.10.100.X/24 network with a default gateway of 10.10.100.254. I presumed that was the IP of the ECU, and can confirm it is listening on tcp port 8899 on 10.10.100.254 - I get data this way with the sample python program.

But, I’m still getting no response on port 8899 the LAN interface of the ECU. Only it’s internal wifi network in AP mode.

I then used the ECU app to have it join my wifi network, I had been using the ethernet interface only, and then in my router, I see it having 2 IP addresses on my network, one ethernet (192.168.0.198), one wifi (192.168.0.251). If I query the ethernet IP (192.168.0.198) on port 8899 still no go, but if I use the wifi one (192.168.0.251), it does now work!

Are you guys using wifi or ethernet to connect it to your network, maybe it only listens on port 8899 on it’s wifi interface?

Anyway, here’s my raw ascii data from the ECU when sending the command in the first python example.

41505331313031393030303032303030310007202012282040578020001044130030330000006400000000000000000000802000110269003033000000640000000000000000000080200011054900303300000064000000000000000000008020001113140030330000006400000000000000000000802000112342003033000000640000000000000000000080200011330400303300000064000000000000000000008020001135230030330000006400000000000000000000454e440a

There are a lot of zeros since it’s night-time and no sun. I’ll spend some time tomorrow decoding this more, when the sun is out, and I start seeing actual data.

Since I played a lot on the local ssid, I have it in wifi mode connect to network, wanted to switch to ethernet, but if then the port is unavailable, it’s a no go :slight_smile:
Good you have data now

You have 7 inverters? How many panels are installed on those?
Also interested in the 3033 part on each inverter… I have 3031, can’t figure what it is.
THinking you have qs1 inverters, it might be identifier for that.
Or maybe the power grid profile setting. Where do you live?

Also on Wifi, haven’t tried LAN interface on ECU-R (switch full, one broken). Indeed 7 inverters (QS1) makes a total of 28 panels if all channels on the inverters are used. Unfortunately it seems that there are two bytes added per inverter. Logic must be added to serve QS1 owners. Since we haven’t figured out all data yet, it could well be that there is an ID of inverter type (YC600 or QS1) present. For now it seems that panels connected to the YC600 start of with 4080 and panels connected to the QS1 with 8020 but the exact word-length is a wild guess. We simply haven’t enough data examples to prove this. It could well be that Sander is right with 3033(QS1) and 3031(YC600). Also a mix of QS1’s and YC600’s could be connected to the ECU-R, to make things more complicated :slight_smile:

About every 5 minutes data is posted to ecu.apsema.com but I’ve seen ecu2.apsema.com also pass by in the early morning when I was asleep.

@ksheumaker got some better script to work with from other guy that had interest, maybe helps you too.

import socket
import struct
import binascii
import json
import datetime
from struct import unpack
 
myIPadress = "192.168.?.?"
myMACadress = "" #May be search for this if ipadres doesn't react, later
output = {}
 
def APSint(codec,start):
    return int(binascii.b2a_hex(codec[(start):(start+2)]),16)
   
def APSbool(codec,start):
    return bool(binascii.b2a_hex(codec[(start):(start+2)]))
 
def APSuid(codec,start):
    return str(binascii.b2a_hex(codec[(start):(start+12)]))[2:14]
 
def APSstr(codec,start,amount):
    return str(codec[start:(start+amount)])[2:(amount+2)]
 
def APStimestamp(codec,start,amount):
    timestr=str(binascii.b2a_hex(codec[start:(start+amount)]))[2:(amount+2)]
    return timestr[0:4]+"-"+timestr[4:6]+"-"+timestr[6:8]+" "+timestr[8:10]+":"+timestr[10:12]+":"+timestr[12:14]
 
#initialize socket
soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.connect((myIPadress,8899))
 
#get ECUID
ECU_R = 'APS1100160001END'
ECU_Rsend = ECU_R.encode('utf-8')
soc.send(ECU_Rsend)
ECU_Rreceive = soc.recv(1024)
# print ("\nAsking for ECUID number: ")
# print(ECU_Rreceive)
# Decoded explanation
# APS 1100940001 = Open sentence and answer notation
# 216000026497 = ECU_R nummer
# 01 =text unkown
# 00 00 (a7 90)=42896[167,144] 00 00 00 00 00 00 00
# 8x status of UID ca=202 d0=208 d0 d0 d0 d0 d0 d0
# 0008=(aantal UID) maximaal (16)
# 0000=integer unkown
# 10012 = text unkown
# ECU_R_1.2.13009 = Version
# Etc/GMT-8 = ETC ipv UTC Timezone server op lokatie met -8 uur?
# 80 97 1b 01 5d 1e 00 00 00 00 00 00
# END\n
myECUID = APSstr(ECU_Rreceive,13,12) #216000026497
output["ECU_R_ID"] = myECUID
output["maxUID"] = APSint(ECU_Rreceive,46)
output["Version"] = APSstr(ECU_Rreceive,55,15)
output["TimeZone"] = APSstr(ECU_Rreceive,70,9)
#print(output)
 
#get data from UID
ECU_R = 'APS1100280002'+myECUID+"END"  # 2 extra?
ECU_Rsend = ECU_R.encode('utf-8')
soc.send(ECU_Rsend)
ECU_Rreceive = soc.recv(2048)
#print("\nAsking for UID numbers and data: "+str(len(ECU_Rreceive)))
#print(ECU_Rreceive)
#print("\n")
#print(binascii.b2a_hex(ECU_Rreceive))
 
if (len(ECU_Rreceive)>16) :
    #base data valid for all UID
    output["timestamp"] = APStimestamp(ECU_Rreceive,19,14)
    counter = 1
    maxcounter = APSint(ECU_Rreceive,17) #number of inverters
    offset=26
    while counter <= maxcounter:
        # Records will pass for each inverter on this ECU_R (UID)
        # 1-3 APS
        # 4-18 UID
        # 19 number of inverters?
        #------------------------
        # 20-32 Inverter ID (UID)
        # 33 0 or 1 Marks online status of inverter instance
        # 34 unkown "0"
        # 35 unkown "1" Could be country because of 31 (Netherlands)
        # 36-37 Frequency multiplied by 10
        # 38-39 Temperature Celsius Bit 7 of second byte is signbit (1=+, 0=-)?
        # 40-41 Power A Channel A on Inverter
        # 42-43 Voltage A Chanel A on Inverter
        # 44-45 Power B Channel B on Inverter
        # 46-47 Voltage B Chanel B on Inverter
        # 48-51 END or channel C and D till END
        #pick up data for this inverter
        # output={}
        output["UID"+str(counter)] = APSuid(ECU_Rreceive,offset)
        #you can also use UID as index in dictonary
        uid=output["UID"+str(counter)][7:12]
        output["Online"+uid] = APSbool(ECU_Rreceive,offset+6)
        output["Something"+uid] = APSstr(ECU_Rreceive,offset+7,2)
        output["Frequentie"+uid] = APSint(ECU_Rreceive,offset+9)/10.0
        output["Temperature"+uid] = APSint(ECU_Rreceive,offset+11)-100 # check later if sign is bit 7 64 and fahrenheit
        step = 13
        channel = 1
        while (channel <= 2): #QS1 must have 4 times, so C and D but how to see the difference
            output["Power"+uid+"-"+str(channel)] = APSint(ECU_Rreceive,offset+step)
            output["Volt"+uid+"-"+str(channel)] = APSint(ECU_Rreceive,offset+step+2)
            step += 4
            channel += 1
            # print(output)
            #end while channel
 
        offset += step #how big is UID block
        counter += 1
        #print(output)
        #end while UID
    #print(output)
else:
    output["Error"] = "No inverters active"
print(json.dumps(output))

heads up on above, seems to mess up a bit in QS1 data, the bytes per UID are longer for QS1

I’ve taken the above code, and fixed it up a bit, and created the start of a python class that will process data from the ECU:

#!/usr/bin/env python3

import socket
import binascii
import datetime
import json

from pprint import pprint

class APSystemsECU:

    def __init__(self, ipaddr, port=8899, raw_ecu=None, raw_inverter=None):
        self.ipaddr = ipaddr
        self.port = port

        self.recv_size = 2048

        self.ecu_query = 'APS1100160001END'
        self.inverter_query_prefix = 'APS1100280002'
        self.inverter_query_suffix = 'END'
        self.inverter_byte_start = 26

        self.ecu_id = None
        self.qty_of_inverters = 0
        self.inverters = []
        self.firmware = None
        self.timezone = None

        self.ecu_raw_data = raw_ecu
        self.inverter_raw_data = raw_inverter

        self.last_inverter_data = 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}")

    def query_ecu(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((self.ipaddr,self.port))

        sock.send(self.ecu_query.encode('utf-8'))
        self.ecu_raw_data = sock.recv(self.recv_size)

        sock.shutdown(socket.SHUT_RDWR)
        sock.close()

        self.process_ecu_data()

    def query_inverters(self, ecu_id = None):
        if not ecu_id:
            ecu_id = self.ecu_id

        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((self.ipaddr,self.port))
        cmd = self.inverter_query_prefix + self.ecu_id + self.inverter_query_suffix
        sock.send(cmd.encode('utf-8'))

        self.inverter_raw_data = sock.recv(self.recv_size)

        sock.shutdown(socket.SHUT_RDWR)
        sock.close()

        data = self.process_inverter_data()
        self.last_inverter_data = data

        return(data)
 
    def aps_int(self, codec, start):
        return int(binascii.b2a_hex(codec[(start):(start+2)]),16)
    
    def aps_bool(self, codec, start):
        return bool(binascii.b2a_hex(codec[(start):(start+2)]))
    
    def aps_uid(self, codec, start):
        return str(binascii.b2a_hex(codec[(start):(start+12)]))[2:14]
    
    def aps_str(self, codec, start, amount):
        return str(codec[start:(start+amount)])[2:(amount+2)]
    
    def aps_timestamp(self, codec, start, amount):
        timestr=str(binascii.b2a_hex(codec[start:(start+amount)]))[2:(amount+2)]
        return timestr[0:4]+"-"+timestr[4:6]+"-"+timestr[6:8]+" "+timestr[8:10]+":"+timestr[10:12]+":"+timestr[12:14]

    def process_ecu_data(self, data=None):
        if not data:
            data = self.ecu_raw_data

        if len(data) < 16:
            raise Exception("ECU query didn't return minimum 16 bytes, no inverters active.")

        self.ecu_id = self.aps_str(data, 13, 12)
        self.qty_of_inverters = self.aps_int(data, 46)
        self.firmware = self.aps_str(data, 55, 15)
        self.timezone = self.aps_str(data, 70, 9)

    def process_inverter_data(self, data=None):
        if not data:
            data = self.inverter_raw_data

        output = {}

        timestamp = self.aps_timestamp(data, 19, 14)
        inverter_qty = self.aps_int(data, 17)

        output["timestamp"] = timestamp
        output["inverter_qty"] = inverter_qty
        output["inverters"] = []

        # this is the start of the loop of inverters
        location = self.inverter_byte_start

        inverters = []
        for i in range(0, inverter_qty):

            inv={}

            inverter_uid = self.aps_uid(data, location)
            inv["uid"] = inverter_uid
            location += 6

            inv["online"] = self.aps_bool(data, location)
            location += 1

            inv["unknown"] = self.aps_str(data, location, 2)
            location += 2

            inv["frequency"] = self.aps_int(data, location) / 10
            location += 2

            inv["temperature"] = self.aps_int(data, location) - 100
            location += 2

            # a YC600 starts with 4080
            if inverter_uid.startswith("4080"):
                (channel_data, location) = self.process_yc600(data, location)
                inv.update(channel_data)    

            # a QS1 starts with 8020
            elif inverter_uid.startswith("8020"):
                (channel_data, location) = self.process_qs1(data, location)
                inv.update(channel_data)    

            inverters.append(inv)

        total_power = 0
        for i in inverters:
            for p in i["power"]:
                total_power += p


        output["total_power"] = total_power
        output["inverters"] = inverters
        return (output)

    def process_qs1(self, data, location):

        power = []
        voltages = []

        power.append(self.aps_int(data, location))
        location += 2

        voltage = self.aps_int(data, location)
        location += 2

        power.append(self.aps_int(data, location))
        location += 2

        power.append(self.aps_int(data, location))
        location += 2

        power.append(self.aps_int(data, location))
        location += 2

        voltages.append(voltage)

        output = {
            "model" : "QS1",
            "channel_qty" : 4,
            "power" : power,
            "voltage" : voltages
        }

        return (output, location)


    def process_yc600(self, data, location):
        power = []
        voltages = []

        for i in range(0, 2):
            power.append(self.aps_int(data, location))
            location += 2

            voltages.append(self.aps_int(data, location))
            location += 2

        output = {
            "model" : "YC600",
            "channel_qty" : 2,
            "power" : power,
            "voltage" : voltages,
        }

        return (output, location)


if __name__ == "__main__":

    # supply the correct IP address here
    ecu = APSystemsECU("192.168.0.251")

    # get inverter data by querying the ecu directly
    ecu.query_ecu()
    data = ecu.query_inverters()
    print(json.dumps(data, indent=2))


    # sample_yc600_data = bytes.fromhex('415053313130313736303030323030303100072020112412051040800009401601303101f3006f001400e4001400e440800009562201303101f3006f001300e4001400e440800009182601303101f3006f001400e3001400e340800009293301303101f3006f001400e3001300e340800009191301303101f3006f001500e3001400e340800009243401303101f3006f001400e3001400e340800009184001303101f3006f001400e2001400e2454e440a')
    # data = ecu.process_inverter_data(data = sample_yc600_data)
    # print(json.dumps(data, indent=2))

    # sample_qs1_data = bytes.fromhex('415053313130313930303030323030303100072020122915125380200010441301303302570065000200f100010007000680200011026901303302570064000100f200010006000680200011054901303302570065000100f100010006000680200011131401303302570064000100f100000006000680200011234201303302570065000000ef00010005000680200011330401303302570065000400f000000000000080200011352301303302570066000100f1000100060006454e440a')
    # data = ecu.process_inverter_data(data = sample_qs1_data)
    # print(json.dumps(data, indent=2))

I should be able to use this with a HA custom_component to generate all the appropriate sensors, and run the query_inverters() method at a regular interval to get new data.

Please try this on your systems, as I only have the QS1 to test with, I took the sample data from a previous post and wrote a process_yc600 method to based on that code and data.

My data structure is quite a bit different, but still has all the pertinent data.

Output from my system:

{
  "timestamp": "2020-12-29 15:22:53",
  "inverter_qty": 7,
  "inverters": [
    {
      "uid": "802000104413",
      "online": true,
      "unknown": "03",
      "frequency": 59.9,
      "temperature": 1,
      "model": "QS1",
      "channel_qty": 4,
      "power": [
        2,
        1,
        7,
        6
      ],
      "voltage": [
        242
      ]
    },
    {
      "uid": "802000110269",
      "online": true,
      "unknown": "03",
      "frequency": 59.9,
      "temperature": 0,
      "model": "QS1",
      "channel_qty": 4,
      "power": [
        1,
        1,
        6,
        6
      ],
      "voltage": [
        242
      ]
    },
    {
      "uid": "802000110549",
      "online": true,
      "unknown": "03",
      "frequency": 59.9,
      "temperature": 2,
      "model": "QS1",
      "channel_qty": 4,
      "power": [
        1,
        1,
        5,
        6
      ],
      "voltage": [
        241
      ]
    },
    {
      "uid": "802000111314",
      "online": true,
      "unknown": "03",
      "frequency": 59.9,
      "temperature": 0,
      "model": "QS1",
      "channel_qty": 4,
      "power": [
        1,
        0,
        6,
        5
      ],
      "voltage": [
        242
      ]
    },
    {
      "uid": "802000112342",
      "online": true,
      "unknown": "03",
      "frequency": 59.9,
      "temperature": 1,
      "model": "QS1",
      "channel_qty": 4,
      "power": [
        0,
        0,
        5,
        6
      ],
      "voltage": [
        240
      ]
    },
    {
      "uid": "802000113304",
      "online": true,
      "unknown": "03",
      "frequency": 59.9,
      "temperature": 1,
      "model": "QS1",
      "channel_qty": 4,
      "power": [
        2,
        0,
        0,
        0
      ],
      "voltage": [
        240
      ]
    },
    {
      "uid": "802000113523",
      "online": true,
      "unknown": "03",
      "frequency": 59.9,
      "temperature": 2,
      "model": "QS1",
      "channel_qty": 4,
      "power": [
        1,
        1,
        6,
        6
      ],
      "voltage": [
        242
      ]
    }
  ],
  "total_power": 82
}

very interesting, especiallly because when i use the command line sensor, it is getting exceeded with the 255 character limit in json repsonse.
This above script is also runnable manually on command line to test?
Config for custom component in config .yaml is just ‘APSystemsECU:’ ?