Home Assistant Git Exporter
This commit is contained in:
350
config/custom_components/apsystems_ecur/APSystemsSocket.py
Normal file
350
config/custom_components/apsystems_ecur/APSystemsSocket.py
Normal file
@@ -0,0 +1,350 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import socket
|
||||
import binascii
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class APSystemsInvalidData(Exception):
|
||||
pass
|
||||
|
||||
class APSystemsSocket:
|
||||
def __init__(self, ipaddr, nographs, port=8899, raw_ecu=None, raw_inverter=None):
|
||||
global no_graphs
|
||||
no_graphs = nographs
|
||||
self.ipaddr = ipaddr
|
||||
self.port = port
|
||||
|
||||
# what do we expect socket data to end in
|
||||
self.recv_suffix = b'END\n'
|
||||
|
||||
# how long to wait on socket commands until we get our recv_suffix
|
||||
self.timeout = 10
|
||||
|
||||
# how big of a buffer to read at a time from the socket
|
||||
# https://github.com/ksheumaker/homeassistant-apsystems_ecur/issues/108
|
||||
self.recv_size = 1024
|
||||
|
||||
# how long to wait between socket open/closes
|
||||
self.socket_sleep_time = 5
|
||||
|
||||
self.cmd_suffix = "END\n"
|
||||
self.ecu_query = "APS1100160001" + self.cmd_suffix
|
||||
self.inverter_query_prefix = "APS1100280002"
|
||||
self.inverter_query_suffix = self.cmd_suffix
|
||||
self.inverter_signal_prefix = "APS1100280030"
|
||||
self.inverter_signal_suffix = self.cmd_suffix
|
||||
self.ecu_id = None
|
||||
self.qty_of_inverters = 0
|
||||
self.qty_of_online_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.vsl = 0
|
||||
self.tsl = 0
|
||||
self.ecu_raw_data = raw_ecu
|
||||
self.inverter_raw_data = raw_inverter
|
||||
self.inverter_raw_signal = None
|
||||
self.read_buffer = b''
|
||||
self.socket = None
|
||||
self.socket_open = False
|
||||
self.errors = []
|
||||
|
||||
def send_read_from_socket(self, cmd):
|
||||
try:
|
||||
self.sock.settimeout(self.timeout)
|
||||
self.sock.sendall(cmd.encode('utf-8'))
|
||||
time.sleep(self.socket_sleep_time)
|
||||
self.read_buffer = b''
|
||||
self.sock.settimeout(self.timeout)
|
||||
# An infinite loop was causing the integration to block
|
||||
# https://github.com/ksheumaker/homeassistant-apsystems_ecur/issues/115
|
||||
# Solution might cause a new issue when large solar array's applies
|
||||
self.read_buffer = self.sock.recv(self.recv_size)
|
||||
return self.read_buffer
|
||||
except Exception as err:
|
||||
self.close_socket()
|
||||
raise APSystemsInvalidData(err)
|
||||
|
||||
def close_socket(self):
|
||||
try:
|
||||
if self.socket_open:
|
||||
self.sock.shutdown(socket.SHUT_RDWR)
|
||||
self.sock.close()
|
||||
self.socket_open = False
|
||||
except Exception as err:
|
||||
raise APSystemsInvalidData(err)
|
||||
|
||||
def open_socket(self):
|
||||
self.socket_open = False
|
||||
try:
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.sock.settimeout(self.timeout)
|
||||
self.sock.connect((self.ipaddr, self.port))
|
||||
self.socket_open = True
|
||||
except Exception as err:
|
||||
raise APSystemsInvalidData(err)
|
||||
|
||||
def query_ecu(self):
|
||||
#read ECU data
|
||||
self.open_socket()
|
||||
self.ecu_raw_data = self.send_read_from_socket(self.ecu_query)
|
||||
self.close_socket()
|
||||
try:
|
||||
self.process_ecu_data()
|
||||
except Exception as err:
|
||||
raise APSystemsInvalidData(err)
|
||||
|
||||
#read inverter data
|
||||
# Some ECUs like the socket to be closed and re-opened between commands
|
||||
self.open_socket()
|
||||
cmd = self.inverter_query_prefix + self.ecu_id + self.inverter_query_suffix
|
||||
self.inverter_raw_data = self.send_read_from_socket(cmd)
|
||||
self.close_socket()
|
||||
|
||||
#read signal data
|
||||
# Some ECUs like the socket to be closed and re-opened between commands
|
||||
self.open_socket()
|
||||
cmd = self.inverter_signal_prefix + self.ecu_id + self.inverter_signal_suffix
|
||||
self.inverter_raw_signal = self.send_read_from_socket(cmd)
|
||||
self.close_socket()
|
||||
|
||||
data = self.process_inverter_data()
|
||||
data["ecu_id"] = self.ecu_id
|
||||
if self.lifetime_energy != 0:
|
||||
data["lifetime_energy"] = self.lifetime_energy
|
||||
data["current_power"] = self.current_power
|
||||
# apply filter for ECU-R-pro firmware bug where both are zero
|
||||
if self.qty_of_inverters > 0:
|
||||
data["qty_of_inverters"] = self.qty_of_inverters
|
||||
data["today_energy"] = self.today_energy
|
||||
data["qty_of_online_inverters"] = self.qty_of_online_inverters
|
||||
return(data)
|
||||
|
||||
def aps_int_from_bytes(self, codec: bytes, start: int, length: int) -> int:
|
||||
try:
|
||||
return int (binascii.b2a_hex(codec[(start):(start+length)]), 16)
|
||||
except ValueError as err:
|
||||
debugdata = binascii.b2a_hex(codec)
|
||||
error = f"Unable to convert binary to int with length={length} at location={start} with data={debugdata}"
|
||||
raise APSystemsInvalidData(error)
|
||||
|
||||
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_datetimestamp(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 check_ecu_checksum(self, data, cmd):
|
||||
datalen = len(data) - 1
|
||||
try:
|
||||
checksum = int(data[5:9])
|
||||
except ValueError as err:
|
||||
debugdata = binascii.b2a_hex(data)
|
||||
error = f"could not extract checksum int from '{cmd}' data={debugdata}"
|
||||
raise APSystemsInvalidData(error)
|
||||
|
||||
if datalen != checksum:
|
||||
debugdata = binascii.b2a_hex(data)
|
||||
error = f"Checksum on '{cmd}' failed checksum={checksum} datalen={datalen} data={debugdata}"
|
||||
raise APSystemsInvalidData(error)
|
||||
|
||||
start_str = self.aps_str(data, 0, 3)
|
||||
end_str = self.aps_str(data, len(data) - 4, 3)
|
||||
|
||||
if start_str != 'APS':
|
||||
debugdata = binascii.b2a_hex(data)
|
||||
error = f"Result on '{cmd}' incorrect start signature '{start_str}' != APS data={debugdata}"
|
||||
raise APSystemsInvalidData(error)
|
||||
|
||||
if end_str != 'END':
|
||||
debugdata = binascii.b2a_hex(data)
|
||||
error = f"Result on '{cmd}' incorrect end signature '{end_str}' != END data={debugdata}"
|
||||
raise APSystemsInvalidData(error)
|
||||
|
||||
return True
|
||||
|
||||
def process_ecu_data(self, data=None):
|
||||
if self.ecu_raw_data != '' and (self.aps_str(self.ecu_raw_data,9,4)) == '0001':
|
||||
data = self.ecu_raw_data
|
||||
_LOGGER.debug(binascii.b2a_hex(data))
|
||||
self.check_ecu_checksum(data, "ECU Query")
|
||||
self.ecu_id = self.aps_str(data, 13, 12)
|
||||
self.lifetime_energy = self.aps_int_from_bytes(data, 27, 4) / 10
|
||||
self.current_power = self.aps_int_from_bytes(data, 31, 4)
|
||||
self.today_energy = self.aps_int_from_bytes(data, 35, 4) / 100
|
||||
if self.aps_str(data,25,2) == "01":
|
||||
self.qty_of_inverters = self.aps_int_from_bytes(data, 46, 2)
|
||||
self.qty_of_online_inverters = self.aps_int_from_bytes(data, 48, 2)
|
||||
self.vsl = int(self.aps_str(data, 52, 3))
|
||||
self.firmware = self.aps_str(data, 55, self.vsl)
|
||||
self.tsl = int(self.aps_str(data, 55 + self.vsl, 3))
|
||||
self.timezone = self.aps_str(data, 58 + self.vsl, self.tsl)
|
||||
elif self.aps_str(data,25,2) == "02":
|
||||
self.qty_of_inverters = self.aps_int_from_bytes(data, 39, 2)
|
||||
self.qty_of_online_inverters = self.aps_int_from_bytes(data, 41, 2)
|
||||
self.vsl = int(self.aps_str(data, 49, 3))
|
||||
self.firmware = self.aps_str(data, 52, self.vsl)
|
||||
|
||||
def process_signal_data(self, data=None):
|
||||
signal_data = {}
|
||||
if self.inverter_raw_signal != '' and (self.aps_str(self.inverter_raw_signal,9,4)) == '0030':
|
||||
data = self.inverter_raw_signal
|
||||
_LOGGER.debug(binascii.b2a_hex(data))
|
||||
self.check_ecu_checksum(data, "Signal Query")
|
||||
if not self.qty_of_inverters:
|
||||
return signal_data
|
||||
location = 15
|
||||
for i in range(0, self.qty_of_inverters):
|
||||
uid = self.aps_uid(data, location)
|
||||
location += 6
|
||||
strength = data[location]
|
||||
location += 1
|
||||
strength = int((strength / 255) * 100)
|
||||
signal_data[uid] = strength
|
||||
return signal_data
|
||||
|
||||
def process_inverter_data(self, data=None):
|
||||
output = {}
|
||||
if self.inverter_raw_data != '' and (self.aps_str(self.inverter_raw_data,9,4)) == '0002':
|
||||
data = self.inverter_raw_data
|
||||
_LOGGER.debug(binascii.b2a_hex(data))
|
||||
self.check_ecu_checksum(data, "Inverter data")
|
||||
istr = ''
|
||||
cnt1 = 0
|
||||
cnt2 = 26
|
||||
if self.aps_str(data, 14, 2) == '00':
|
||||
timestamp = self.aps_datetimestamp(data, 19, 14)
|
||||
inverter_qty = self.aps_int_from_bytes(data, 17, 2)
|
||||
self.last_update = timestamp
|
||||
output["timestamp"] = timestamp
|
||||
output["inverters"] = {}
|
||||
signal = self.process_signal_data()
|
||||
inverters = {}
|
||||
|
||||
while cnt1 < inverter_qty:
|
||||
inv={}
|
||||
if self.aps_str(data, 15, 2) == '01':
|
||||
inverter_uid = self.aps_uid(data, cnt2)
|
||||
inv["uid"] = inverter_uid
|
||||
inv["online"] = bool(self.aps_int_from_bytes(data, cnt2 + 6, 1))
|
||||
istr = self.aps_str(data, cnt2 + 7, 2)
|
||||
|
||||
# Should graphs be updated?
|
||||
if inv["online"] == False and no_graphs == True:
|
||||
inv["signal"] = None
|
||||
else:
|
||||
inv["signal"] = signal.get(inverter_uid, 0)
|
||||
|
||||
# Distinguishes the different inverters from this point down
|
||||
if istr in [ '01', '04', '05']:
|
||||
power = []
|
||||
voltages = []
|
||||
|
||||
# Should graphs be updated?
|
||||
if inv["online"] == True:
|
||||
inv["temperature"] = self.aps_int_from_bytes(data, cnt2 + 11, 2) - 100
|
||||
if inv["online"] == False and no_graphs == True:
|
||||
inv["frequency"] = None
|
||||
power.append(None)
|
||||
voltages.append(None)
|
||||
power.append(None)
|
||||
voltages.append(None)
|
||||
else:
|
||||
inv["frequency"] = self.aps_int_from_bytes(data, cnt2 + 9, 2) / 10
|
||||
power.append(self.aps_int_from_bytes(data, cnt2 + 13, 2))
|
||||
voltages.append(self.aps_int_from_bytes(data, cnt2 + 15, 2))
|
||||
power.append(self.aps_int_from_bytes(data, cnt2 + 17, 2))
|
||||
voltages.append(self.aps_int_from_bytes(data, cnt2 + 19, 2))
|
||||
|
||||
inv_details = {
|
||||
"model" : "YC600/DS3 series",
|
||||
"channel_qty" : 2,
|
||||
"power" : power,
|
||||
"voltage" : voltages
|
||||
}
|
||||
inv.update(inv_details)
|
||||
cnt2 = cnt2 + 21
|
||||
elif istr == '02':
|
||||
power = []
|
||||
voltages = []
|
||||
|
||||
# Should graphs be updated?
|
||||
if inv["online"]:
|
||||
inv["temperature"] = self.aps_int_from_bytes(data, cnt2 + 11, 2) - 100
|
||||
if inv["online"] == False and no_graphs == True:
|
||||
inv["frequency"] = None
|
||||
power.append(None)
|
||||
voltages.append(None)
|
||||
power.append(None)
|
||||
voltages.append(None)
|
||||
power.append(None)
|
||||
voltages.append(None)
|
||||
power.append(None)
|
||||
else:
|
||||
inv["frequency"] = self.aps_int_from_bytes(data, cnt2 + 9, 2) / 10
|
||||
power.append(self.aps_int_from_bytes(data, cnt2 + 13, 2))
|
||||
voltages.append(self.aps_int_from_bytes(data, cnt2 + 15, 2))
|
||||
power.append(self.aps_int_from_bytes(data, cnt2 + 17, 2))
|
||||
voltages.append(self.aps_int_from_bytes(data, cnt2 + 19, 2))
|
||||
power.append(self.aps_int_from_bytes(data, cnt2 + 21, 2))
|
||||
voltages.append(self.aps_int_from_bytes(data, cnt2 + 23, 2))
|
||||
power.append(self.aps_int_from_bytes(data, cnt2 + 25, 2))
|
||||
|
||||
inv_details = {
|
||||
"model" : "YC1000/QT2",
|
||||
"channel_qty" : 4,
|
||||
"power" : power,
|
||||
"voltage" : voltages
|
||||
}
|
||||
inv.update(inv_details)
|
||||
cnt2 = cnt2 + 27
|
||||
elif istr == '03':
|
||||
power = []
|
||||
voltages = []
|
||||
|
||||
# Should graphs be updated?
|
||||
if inv["online"]:
|
||||
inv["temperature"] = self.aps_int_from_bytes(data, cnt2 + 11, 2) - 100
|
||||
if inv["online"] == False and no_graphs == True:
|
||||
inv["frequency"] = None
|
||||
power.append(None)
|
||||
voltages.append(None)
|
||||
power.append(None)
|
||||
power.append(None)
|
||||
power.append(None)
|
||||
else:
|
||||
inv["frequency"] = self.aps_int_from_bytes(data, cnt2 + 9, 2) / 10
|
||||
power.append(self.aps_int_from_bytes(data, cnt2 + 13, 2))
|
||||
voltages.append(self.aps_int_from_bytes(data, cnt2 + 15, 2))
|
||||
power.append(self.aps_int_from_bytes(data, cnt2 + 17, 2))
|
||||
power.append(self.aps_int_from_bytes(data, cnt2 + 19, 2))
|
||||
power.append(self.aps_int_from_bytes(data, cnt2 + 21, 2))
|
||||
|
||||
inv_details = {
|
||||
"model" : "QS1",
|
||||
"channel_qty" : 4,
|
||||
"power" : power,
|
||||
"voltage" : voltages
|
||||
}
|
||||
inv.update(inv_details)
|
||||
cnt2 = cnt2 + 23
|
||||
else:
|
||||
cnt2 = cnt2 + 9
|
||||
inverters[inverter_uid] = inv
|
||||
cnt1 = cnt1 + 1
|
||||
self.inverters = inverters
|
||||
output["inverters"] = inverters
|
||||
return (output)
|
||||
Reference in New Issue
Block a user