diff --git a/server/apsystems_ecur/APSystemsECUR.py b/server/apsystems_ecur/APSystemsECUR.py new file mode 100755 index 0000000..fe7bc3d --- /dev/null +++ b/server/apsystems_ecur/APSystemsECUR.py @@ -0,0 +1,413 @@ +#!/usr/bin/env python3 + +import asyncio +import socket +import binascii +import datetime +import json +import logging + +_LOGGER = logging.getLogger(__name__) + +from pprint import pprint + +class APSystemsInvalidData(Exception): + pass + +class APSystemsECUR: + + def __init__(self, ipaddr, port=8899, raw_ecu=None, raw_inverter=None): + 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 = 5 + + # how many times do we try the same command in a single update before failing + self.cmd_attempts = 3 + + # how big of a buffer to read at a time from the socket + self.recv_size = 4096 + + self.qs1_ids = [ "802", "801" ] + self.yc600_ids = [ "406", "407", "408", "409" ] + self.yc1000_ids = [ "501", "502", "503", "504" ] + + 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.inverter_byte_start = 26 + + 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 + + self.read_buffer = b'' + + self.reader = None + self.writer = 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 async_read_from_socket(self): + self.read_buffer = b'' + end_data = None + + while end_data != self.recv_suffix: + self.read_buffer += await self.reader.read(self.recv_size) + size = len(self.read_buffer) + end_data = self.read_buffer[size-4:] + + return self.read_buffer + + async def async_send_read_from_socket(self, cmd): + current_attempt = 0 + while current_attempt < self.cmd_attempts: + current_attempt += 1 + + self.writer.write(cmd.encode('utf-8')) + await self.writer.drain() + + try: + return await asyncio.wait_for(self.async_read_from_socket(), + timeout=self.timeout) + except Exception as err: + pass + + self.writer.close() + raise APSystemsInvalidData(f"Incomplete data from ECU after {current_attempt} attempts, cmd='{cmd.rstrip()}' data={self.read_buffer}") + + async def async_query_ecu(self): + self.reader, self.writer = await asyncio.open_connection(self.ipaddr, self.port) + _LOGGER.info(f"Connected to {self.ipaddr} {self.port}") + + cmd = self.ecu_query + self.ecu_raw_data = await self.async_send_read_from_socket(cmd) + + self.process_ecu_data() + + cmd = self.inverter_query_prefix + self.ecu_id + self.inverter_query_suffix + self.inverter_raw_data = await self.async_send_read_from_socket(cmd) + + cmd = self.inverter_signal_prefix + self.ecu_id + self.inverter_signal_suffix + self.inverter_raw_signal = await self.async_send_read_from_socket(cmd) + + self.writer.close() + + data = self.process_inverter_data() + 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 query_ecu(self): + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((self.ipaddr,self.port)) + + sock.sendall(self.ecu_query.encode('utf-8')) + self.ecu_raw_data = sock.recv(self.recv_size) + + self.process_ecu_data() + + cmd = self.inverter_query_prefix + self.ecu_id + self.inverter_query_suffix + sock.sendall(cmd.encode('utf-8')) + self.inverter_raw_data = sock.recv(self.recv_size) + + cmd = self.inverter_signal_prefix + self.ecu_id + self.inverter_signal_suffix + sock.sendall(cmd.encode('utf-8')) + self.inverter_raw_signal = sock.recv(self.recv_size) + + sock.shutdown(socket.SHUT_RDWR) + sock.close() + + data = self.process_inverter_data() + + 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 aps_int(self, codec, start): + try: + return int(binascii.b2a_hex(codec[(start):(start+2)]), 16) + except ValueError as err: + debugdata = binascii.b2a_hex(codec) + raise APSystemsInvalidData(f"Unable to convert binary to int location={start} data={debugdata}") + + def aps_short(self, codec, start): + try: + return int(binascii.b2a_hex(codec[(start):(start+1)]), 8) + except ValueError as err: + debugdata = binascii.b2a_hex(codec) + raise APSystemsInvalidData(f"Unable to convert binary to short int location={start} data={debugdata}") + + def aps_double(self, codec, start): + try: + return int (binascii.b2a_hex(codec[(start):(start+4)]), 16) + except ValueError as err: + debugdata = binascii.b2a_hex(codec) + raise APSystemsInvalidData(f"Unable to convert binary to double location={start} data={debugdata}") + + 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 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) + raise APSystemsInvalidData(f"Error getting checksum int from '{cmd}' data={debugdata}") + + if datalen != checksum: + debugdata = binascii.b2a_hex(data) + raise APSystemsInvalidData(f"Checksum on '{cmd}' failed checksum={checksum} datalen={datalen} data={debugdata}") + + 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) + raise APSystemsInvalidData(f"Result on '{cmd}' incorrect start signature '{start_str}' != APS data={debugdata}") + + if end_str != 'END': + debugdata = binascii.b2a_hex(data) + raise APSystemsInvalidData(f"Result on '{cmd}' incorrect end signature '{end_str}' != END data={debugdata}") + + return True + + def process_ecu_data(self, data=None): + if not data: + data = self.ecu_raw_data + + self.check_ecu_checksum(data, "ECU Query") + + 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) + self.lifetime_energy = self.aps_double(data, 27) / 10 + self.today_energy = self.aps_double(data, 35) / 100 + self.current_power = self.aps_double(data, 31) + + + def process_signal_data(self, data=None): + signal_data = {} + + if not data: + data = self.inverter_raw_signal + + 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): + if not data: + data = self.inverter_raw_data + + self.check_ecu_checksum(data, "Inverter data") + + output = {} + + timestamp = self.aps_timestamp(data, 19, 14) + inverter_qty = self.aps_int(data, 17) + + self.last_update = timestamp + output["timestamp"] = timestamp + output["inverter_qty"] = inverter_qty + output["inverters"] = {} + + # this is the start of the loop of inverters + location = self.inverter_byte_start + + signal = self.process_signal_data() + + 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 + + inv["signal"] = signal.get(inverter_uid, 0) + + # the first 3 digits determine the type of inverter + inverter_type = inverter_uid[0:3] + if inverter_type in self.yc600_ids: + (channel_data, location) = self.process_yc600(data, location) + inv.update(channel_data) + + elif inverter_type in self.qs1_ids: + (channel_data, location) = self.process_qs1(data, location) + inv.update(channel_data) + + elif inverter_type in self.yc1000_ids: + (channel_data, location) = self.process_yc1000(data, location) + inv.update(channel_data) + + else: + raise APSystemsInvalidData(f"Unsupported inverter type {inverter_type}") + + inverters[inverter_uid] = inv + + output["inverters"] = inverters + return (output) + + def process_yc1000(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 + + voltage = self.aps_int(data, location) + location += 2 + + 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 + + voltages.append(voltage) + + output = { + "model" : "YC1000", + "channel_qty" : 4, + "power" : power, + "voltage" : voltages + } + + return (output, location) + + + 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) + + diff --git a/server/apsystems_ecur/__init__.py b/server/apsystems_ecur/__init__.py new file mode 100755 index 0000000..ca0d37a --- /dev/null +++ b/server/apsystems_ecur/__init__.py @@ -0,0 +1,154 @@ +import logging + +import voluptuous as vol +import traceback +from datetime import timedelta + +from .APSystemsECUR import APSystemsECUR, APSystemsInvalidData +from homeassistant.helpers.discovery import load_platform +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_HOST +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +_LOGGER = logging.getLogger(__name__) + +from .const import DOMAIN + +CONF_INTERVAL = "interval" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN : vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_INTERVAL) : cv.time_period_seconds + }) +}, extra=vol.ALLOW_EXTRA) + +PLATFORMS = [ "sensor", "binary_sensor" ] + + +## handle all the communications with the ECUR class and deal with our need for caching, etc +class ECUR(): + + def __init__(self, ipaddr): + self.ecu = APSystemsECUR(ipaddr) + self.cache_count = 0 + self.cache_max = 5 + self.data_from_cache = False + self.querying = True + self.error_messge = "" + self.cached_data = {} + + async def stop_query(self): + self.querying = False + + async def start_query(self): + self.querying = True + + async def update(self): + data = {} + + # if we aren't actively quering data, pull data form the cache + # this is so we can stop querying after sunset + if not self.querying: + + _LOGGER.debug("Not querying ECU due to stopped") + data = self.cached_data + self.data_from_cache = True + + data["data_from_cache"] = self.data_from_cache + data["querying"] = self.querying + return self.cached_data + + _LOGGER.debug("Querying ECU") + try: + data = await self.ecu.async_query_ecu() + _LOGGER.debug("Got data from ECU") + + # we got good results, so we store it and set flags about our + # cache state + self.cached_data = data + self.cache_count = 0 + self.data_from_cache = False + self.error_message = "" + + except APSystemsInvalidData as err: + + msg = f"Using cached data from last successful communication from ECU. Error: {err}" + _LOGGER.warning(msg) + + # we got invalid data, so we need to pull from cache + self.error_msg = msg + self.cache_count += 1 + self.data_from_cache = True + data = self.cached_data + + if self.cache_count > self.cache_max: + raise Exception(f"Error using cached data for more than {self.cache_max} times.") + + except Exception as err: + + msg = f"Using cached data from last successful communication from ECU. Error: {err}" + _LOGGER.warning(msg) + + # we got invalid data, so we need to pull from cache + self.error_msg = msg + self.cache_count += 1 + self.data_from_cache = True + data = self.cached_data + + if self.cache_count > self.cache_max: + raise Exception(f"Error using cached data for more than {self.cache_max} times.") + + data["data_from_cache"] = self.data_from_cache + data["querying"] = self.querying + _LOGGER.debug(f"Returning {data}") + return data + +async def async_setup(hass, config): + """ Setup the APsystems platform """ + hass.data.setdefault(DOMAIN, {}) + + host = config[DOMAIN].get(CONF_HOST) + interval = config[DOMAIN].get(CONF_INTERVAL) + if not interval: + interval = timedelta(seconds=60) + + ecu = ECUR(host) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=ecu.update, + update_interval=interval, + ) + + await coordinator.async_refresh() + + hass.data[DOMAIN] = { + "ecu" : ecu, + "coordinator" : coordinator + } + + async def handle_stop_query(call): + await ecu.stop_query() + coordinator.async_refresh() + + async def handle_start_query(call): + await ecu.start_query() + coordinator.async_refresh() + + hass.services.async_register(DOMAIN, "start_query", handle_start_query) + hass.services.async_register(DOMAIN, "stop_query", handle_stop_query) + + for component in PLATFORMS: + load_platform(hass, component, DOMAIN, {}, config) + + return True diff --git a/server/apsystems_ecur/binary_sensor.py b/server/apsystems_ecur/binary_sensor.py new file mode 100755 index 0000000..8ed2f05 --- /dev/null +++ b/server/apsystems_ecur/binary_sensor.py @@ -0,0 +1,86 @@ +"""Example integration using DataUpdateCoordinator.""" + +from datetime import timedelta +import logging + +import async_timeout + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, +) + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + DOMAIN, + RELOAD_ICON, + CACHE_ICON +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_platform(hass, config, add_entities, discovery_info=None): + + ecu = hass.data[DOMAIN].get("ecu") + coordinator = hass.data[DOMAIN].get("coordinator") + + + sensors = [ + APSystemsECUBinarySensor(coordinator, ecu, "data_from_cache", + label="Using Cached Data", icon=CACHE_ICON), + APSystemsECUBinarySensor(coordinator, ecu, "querying", + label="Querying Enabled", icon=RELOAD_ICON), + ] + + add_entities(sensors) + + +class APSystemsECUBinarySensor(CoordinatorEntity, BinarySensorEntity): + + def __init__(self, coordinator, ecu, field, label=None, devclass=None, icon=None): + + super().__init__(coordinator) + + self.coordinator = coordinator + + self._ecu = ecu + self._field = field + self._label = label + if not label: + self._label = field + self._icon = icon + + self._name = f"ECU {self._label}" + self._state = None + + @property + def unique_id(self): + return f"{self._ecu.ecu.ecu_id}_{self._field}" + + @property + def name(self): + return self._name + + @property + def is_on(self): + return self.coordinator.data.get(self._field) + + @property + def icon(self): + return self._icon + + @property + def device_state_attributes(self): + + attrs = { + "ecu_id" : self._ecu.ecu.ecu_id, + "firmware" : self._ecu.ecu.firmware, + "last_update" : self._ecu.ecu.last_update + } + return attrs + + diff --git a/server/apsystems_ecur/const.py b/server/apsystems_ecur/const.py new file mode 100755 index 0000000..061a247 --- /dev/null +++ b/server/apsystems_ecur/const.py @@ -0,0 +1,7 @@ +DOMAIN = 'apsystems_ecur' +SOLAR_ICON = "mdi:solar-power" +FREQ_ICON = "mdi:sine-wave" +SIGNAL_ICON = "mdi:signal" +RELOAD_ICON = "mdi:reload" +CACHE_ICON = "mdi:cached" + diff --git a/server/apsystems_ecur/manifest.json b/server/apsystems_ecur/manifest.json new file mode 100755 index 0000000..1336aeb --- /dev/null +++ b/server/apsystems_ecur/manifest.json @@ -0,0 +1,10 @@ +{ + "codeowners": ["@ksheumaker"], + "config_flow": false, + "dependencies": [], + "documentation": "https://github.com/ksheumaker/homeassistant-apsystems_ecur", + "domain": "apsystems_ecur", + "name": "APSystems PV solar ECU-R", + "version": "1.0.1", + "requirements": [] +} diff --git a/server/apsystems_ecur/sensor.py b/server/apsystems_ecur/sensor.py new file mode 100755 index 0000000..4c4aad4 --- /dev/null +++ b/server/apsystems_ecur/sensor.py @@ -0,0 +1,208 @@ +"""Example integration using DataUpdateCoordinator.""" + +from datetime import timedelta +import logging + +import async_timeout + +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + DOMAIN, + SOLAR_ICON, + FREQ_ICON, + SIGNAL_ICON +) + +from homeassistant.const import ( + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + VOLT, + TEMP_CELSIUS, + PERCENTAGE, + FREQUENCY_HERTZ +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_platform(hass, config, add_entities, discovery_info=None): + + ecu = hass.data[DOMAIN].get("ecu") + coordinator = hass.data[DOMAIN].get("coordinator") + + + sensors = [ + APSystemsECUSensor(coordinator, ecu, "current_power", + label="Current Power", unit=POWER_WATT, + devclass=DEVICE_CLASS_POWER, icon=SOLAR_ICON), + APSystemsECUSensor(coordinator, ecu, "today_energy", + label="Today Energy", unit=ENERGY_KILO_WATT_HOUR, + devclass=DEVICE_CLASS_ENERGY, icon=SOLAR_ICON), + APSystemsECUSensor(coordinator, ecu, "lifetime_energy", + label="Lifetime Energy", unit=ENERGY_KILO_WATT_HOUR, + devclass=DEVICE_CLASS_ENERGY, icon=SOLAR_ICON), + ] + + inverters = coordinator.data.get("inverters", {}) + for uid,inv_data in inverters.items(): + #_LOGGER.warning(f"Inverter {uid} {inv_data.get('channel_qty')}") + sensors.extend([ + APSystemsECUInverterSensor(coordinator, ecu, uid, + "temperature", label="Temperature", + devclass=DEVICE_CLASS_TEMPERATURE, unit=TEMP_CELSIUS), + APSystemsECUInverterSensor(coordinator, ecu, uid, + "frequency", label="Frequency", unit=FREQUENCY_HERTZ, + devclass=None, icon=FREQ_ICON), + APSystemsECUInverterSensor(coordinator, ecu, uid, + "voltage", label="Voltage", unit=VOLT, + devclass=DEVICE_CLASS_VOLTAGE), + APSystemsECUInverterSensor(coordinator, ecu, uid, + "signal", label="Signal", unit=PERCENTAGE, + icon=SIGNAL_ICON) + + ]) + for i in range(0, inv_data.get("channel_qty", 0)): + sensors.append( + APSystemsECUInverterSensor(coordinator, ecu, uid, f"power", + index=i, label=f"Power Ch {i+1}", unit=POWER_WATT, + devclass=DEVICE_CLASS_POWER, icon=SOLAR_ICON) + ) + + add_entities(sensors) + + +class APSystemsECUInverterSensor(CoordinatorEntity, Entity): + def __init__(self, coordinator, ecu, uid, field, index=0, label=None, icon=None, unit=None, devclass=None): + + super().__init__(coordinator) + + self.coordinator = coordinator + + self._index = index + self._uid = uid + self._ecu = ecu + self._field = field + self._devclass = devclass + self._label = label + if not label: + self._label = field + self._icon = icon + self._unit = unit + + self._name = f"Inverter {self._uid} {self._label}" + self._state = None + + @property + def unique_id(self): + field = self._field + if self._index != None: + field = f"{field}_{self._index}" + return f"{self._ecu.ecu.ecu_id}_{self._uid}_{field}" + + @property + def device_class(self): + return self._devclass + + @property + def name(self): + return self._name + + @property + def state(self): + #_LOGGER.warning(f"State called for {self._field}") + if self._field == "voltage": + return self.coordinator.data.get("inverters", {}).get(self._uid, {}).get("voltage", [])[0] + elif self._field == "power": + #_LOGGER.warning(f"POWER {self._uid} {self._index}") + return self.coordinator.data.get("inverters", {}).get(self._uid, {}).get("power", [])[self._index] + else: + return self.coordinator.data.get("inverters", {}).get(self._uid, {}).get(self._field) + + @property + def icon(self): + return self._icon + + @property + def unit_of_measurement(self): + return self._unit + + + @property + def device_state_attributes(self): + + attrs = { + "ecu_id" : self._ecu.ecu.ecu_id, + "last_update" : self._ecu.ecu.last_update, + } + return attrs + + + +class APSystemsECUSensor(CoordinatorEntity, Entity): + + def __init__(self, coordinator, ecu, field, label=None, icon=None, unit=None, devclass=None): + + super().__init__(coordinator) + + self.coordinator = coordinator + + self._ecu = ecu + self._field = field + self._label = label + if not label: + self._label = field + self._icon = icon + self._unit = unit + self._devclass = devclass + + self._name = f"ECU {self._label}" + self._state = None + + @property + def unique_id(self): + return f"{self._ecu.ecu.ecu_id}_{self._field}" + + @property + def name(self): + return self._name + + @property + def device_class(self): + return self._devclass + + @property + def state(self): + #_LOGGER.warning(f"State called for {self._field}") + return self.coordinator.data.get(self._field) + + @property + def icon(self): + return self._icon + + @property + def unit_of_measurement(self): + return self._unit + + + @property + def device_state_attributes(self): + + attrs = { + "ecu_id" : self._ecu.ecu.ecu_id, + "firmware" : self._ecu.ecu.firmware, + "last_update" : self._ecu.ecu.last_update + } + return attrs + + + diff --git a/server/apsystems_ecur/services.yaml b/server/apsystems_ecur/services.yaml new file mode 100644 index 0000000..fab9330 --- /dev/null +++ b/server/apsystems_ecur/services.yaml @@ -0,0 +1,5 @@ +start_query: + description: Start querying the ECU for new data (i.e. when sunrise) + +stop_query: + description: Stop querying the ECU for new data (i.e. when sunset) diff --git a/server/apsystems_ecur/test.py b/server/apsystems_ecur/test.py new file mode 100755 index 0000000..b1aa114 --- /dev/null +++ b/server/apsystems_ecur/test.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +from APSystemsECUR import APSystemsECUR +import time +import asyncio +from pprint import pprint + +ecu_ip = "192.168.0.251" +sleep = 60 + +loop = asyncio.get_event_loop() +ecu = APSystemsECUR(ecu_ip) + +while True: + try: + data = loop.run_until_complete(ecu.async_query_ecu()) + print(f"[OK] Timestamp: {data.get('timestamp')} Current Power: {data.get('current_power')}") + pprint(data) + except Exception as err: + print(f"[ERROR] {err}") + + print(f"Sleeping for {sleep} sec") + time.sleep(sleep) + +