Home Assistant Git Exporter

This commit is contained in:
root
2024-05-31 09:39:52 +02:00
parent cd6fa93633
commit d5ccfbb540
1353 changed files with 43876 additions and 0 deletions

View 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)

View File

@@ -0,0 +1,241 @@
import logging
import requests
import voluptuous as vol
import traceback
import datetime as dt
from datetime import timedelta
from .APSystemsSocket import APSystemsSocket, APSystemsInvalidData
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.entity import Entity
from homeassistant import config_entries, exceptions
from homeassistant.helpers import device_registry as dr
from homeassistant.components.persistent_notification import (
create as create_persistent_notification
)
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [ "sensor", "binary_sensor", "switch" ]
class WiFiSet():
ipaddr = ""
ssid = ""
wpa = ""
cache = 3
WiFiSet = WiFiSet()
# handle all the communications with the ECUR class and deal with our need for caching, etc
class ECUR():
def __init__(self, ipaddr, ssid, wpa, cache, nographs):
self.ecu = APSystemsSocket(ipaddr, nographs)
self.cache_count = 0
self.data_from_cache = False
self.querying = True
self.inverters_online = True
self.ecu_restarting = False
self.cached_data = {}
WiFiSet.ipaddr = ipaddr
WiFiSet.ssid = ssid
WiFiSet.wpa = wpa
WiFiSet.cache = cache
def stop_query(self):
self.querying = False
def start_query(self):
self.querying = True
def inverters_off(self):
headers = {'X-Requested-With': 'XMLHttpRequest'}
url = 'http://'+ str(WiFiSet.ipaddr) + '/index.php/configuration/set_switch_all_off'
try:
get_url = requests.post(url, headers=headers)
self.inverters_online = False
_LOGGER.debug(f"Response from ECU on switching the inverters off: {str(get_url.status_code)}")
except Exception as err:
_LOGGER.warning(f"Attempt to switch inverters off failed with error: {err} (This switch is only compatible with ECU-R pro and ECU-C type ECU's)")
def inverters_on(self):
headers = {'X-Requested-With': 'XMLHttpRequest'}
url = 'http://'+ str(WiFiSet.ipaddr) + '/index.php/configuration/set_switch_all_on'
try:
get_url = requests.post(url, headers=headers)
self.inverters_online = True
_LOGGER.debug(f"Response from ECU on switching the inverters on: {str(get_url.status_code)}")
except Exception as err:
_LOGGER.warning(f"Attempt to switch inverters on failed with error: {err} (This switch is only compatible with ECU-R pro and ECU-C type ECU's)")
def use_cached_data(self, 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
if self.cache_count == WiFiSet.cache:
_LOGGER.warning(f"Communication with the ECU failed after {WiFiSet.cache} repeated attempts.")
data = {'SSID': WiFiSet.ssid, 'channel': 0, 'method': 2, 'psk_wep': '', 'psk_wpa': WiFiSet.wpa}
_LOGGER.debug(f"Data sent with URL: {data}")
# Determine ECU type to decide ECU restart (for ECU-C and ECU-R with sunspec only)
if (self.cached_data.get("ecu_id", None)[0:3] == "215") or (self.cached_data.get("ecu_id", None)[0:4] == "2162"):
url = 'http://' + str(WiFiSet.ipaddr) + '/index.php/management/set_wlan_ap'
headers = {'X-Requested-With': 'XMLHttpRequest'}
try:
get_url = requests.post(url, headers=headers, data=data)
_LOGGER.debug(f"Response from ECU on restart: {str(get_url.status_code)}")
self.ecu_restarting = True
except Exception as err:
_LOGGER.warning(f"Attempt to restart ECU failed with error: {err}. Querying is stopped automatically.")
self.querying = False
else:
# Older ECU-R models starting with 2160
_LOGGER.warning("Try manually power cycling the ECU. Querying is stopped automatically, turn switch back on after restart of ECU.")
self.querying = False
if self.cached_data.get("ecu_id", None) == None:
_LOGGER.debug(f"Cached data {self.cached_data}")
raise UpdateFailed(f"Unable to get correct data from ECU, and no cached data. See log for details, and try power cycling the ECU.")
return self.cached_data
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 query=False")
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 = self.ecu.query_ecu()
_LOGGER.debug("Got data from ECU")
# we got good results, so we store it and set flags about our cache state
if data["ecu_id"] != None:
self.cached_data = data
self.cache_count = 0
self.data_from_cache = False
self.ecu_restarting = False
self.error_message = ""
else:
msg = f"Using cached data from last successful communication from ECU. Error: no ecu_id returned"
_LOGGER.warning(msg)
data = self.use_cached_data(msg)
except APSystemsInvalidData as err:
msg = f"Using cached data from last successful communication from ECU. Invalid data error: {err}"
if str(err) != 'timed out':
_LOGGER.warning(msg)
data = self.use_cached_data(msg)
except Exception as err:
msg = f"Using cached data from last successful communication from ECU. Exception error: {err}"
_LOGGER.warning(msg)
data = self.use_cached_data(msg)
data["data_from_cache"] = self.data_from_cache
data["querying"] = self.querying
data["restart_ecu"] = self.ecu_restarting
_LOGGER.debug(f"Returning {data}")
if data.get("ecu_id", None) == None:
raise UpdateFailed(f"Somehow data doesn't contain a valid ecu_id")
return data
async def update_listener(hass, config):
# Handle options update being triggered by config entry options updates
_LOGGER.debug(f"Configuration updated: {config.as_dict()}")
ecu = ECUR(config.data["host"],
config.data["SSID"],
config.data["WPA-PSK"],
config.data["CACHE"],
config.data["stop_graphs"]
)
async def async_setup_entry(hass, config):
# Setup the APsystems platform """
hass.data.setdefault(DOMAIN, {})
host = config.data["host"]
interval = timedelta(seconds=config.data["scan_interval"])
# Defaults for new parameters that might not have been set yet from previous integration versions
cache = config.data.get("CACHE", 5)
ssid = config.data.get("SSID", "ECU-WiFi_SSID")
wpa = config.data.get("WPA-PSK", "myWiFipassword")
nographs = config.data.get("stop_graphs", False)
ecu = ECUR(host, ssid, wpa, cache, nographs)
async def do_ecu_update():
return await hass.async_add_executor_job(ecu.update)
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=DOMAIN,
update_method=do_ecu_update,
update_interval=interval,
)
hass.data[DOMAIN] = {
"ecu" : ecu,
"coordinator" : coordinator
}
await coordinator.async_config_entry_first_refresh()
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=config.entry_id,
identifiers={(DOMAIN, f"ecu_{ecu.ecu.ecu_id}")},
manufacturer="APSystems",
suggested_area="Roof",
name=f"ECU {ecu.ecu.ecu_id}",
model=ecu.ecu.firmware,
sw_version=ecu.ecu.firmware,
)
inverters = coordinator.data.get("inverters", {})
for uid,inv_data in inverters.items():
model = inv_data.get("model", "Inverter")
device_registry.async_get_or_create(
config_entry_id=config.entry_id,
identifiers={(DOMAIN, f"inverter_{uid}")},
manufacturer="APSystems",
suggested_area="Roof",
name=f"Inverter {uid}",
model=inv_data.get("model")
)
await hass.config_entries.async_forward_entry_setups(config, PLATFORMS)
config.async_on_unload(config.add_update_listener(update_listener))
return True
async def async_remove_config_entry_device(hass, config, device_entry) -> bool:
if device_entry is not None:
# Notify the user that the device has been removed
create_persistent_notification(
hass,
title="Important notification",
message=f"The following device was removed from the system: {device_entry}"
)
return True
else:
return False
async def async_unload_entry(hass, config):
unload_ok = await hass.config_entries.async_unload_platforms(config, PLATFORMS)
coordinator = hass.data[DOMAIN].get("coordinator")
ecu = hass.data[DOMAIN].get("ecu")
ecu.stop_query()
if unload_ok:
hass.data[DOMAIN].pop(config.entry_id)
return unload_ok

View File

@@ -0,0 +1,92 @@
from datetime import timedelta
import logging
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
)
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
)
from .const import (
DOMAIN,
RELOAD_ICON,
CACHE_ICON,
RESTART_ICON
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(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, "restart_ecu",
label="Restart", icon=RESTART_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 extra_state_attributes(self):
attrs = {
"ecu_id" : self._ecu.ecu.ecu_id,
"firmware" : self._ecu.ecu.firmware,
"timezone" : self._ecu.ecu.timezone,
"last_update" : self._ecu.ecu.last_update
}
return attrs
@property
def entity_category(self):
return EntityCategory.DIAGNOSTIC
@property
def device_info(self):
parent = f"ecu_{self._ecu.ecu.ecu_id}"
return {
"identifiers": {
(DOMAIN, parent),
}
}

View File

@@ -0,0 +1,103 @@
import logging
import voluptuous as vol
import traceback
from datetime import timedelta
from homeassistant.core import callback
from .APSystemsSocket import APSystemsSocket, APSystemsInvalidData
from homeassistant import config_entries, exceptions
from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
from .const import DOMAIN, CONF_SSID, CONF_WPA_PSK, CONF_CACHE, CONF_STOP_GRAPHS
STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str,
vol.Required(CONF_SCAN_INTERVAL, default=300): int,
vol.Optional(CONF_CACHE, default=5): int,
vol.Optional(CONF_SSID, default="ECU-WIFI_local"): str,
vol.Optional(CONF_WPA_PSK, default="default"): str,
vol.Optional(CONF_STOP_GRAPHS, default=False): bool,
})
@config_entries.HANDLERS.register(DOMAIN)
class APSsystemsFlowHandler(config_entries.ConfigFlow):
VERSION = 1
def __init__(self):
_LOGGER.debug("Starting config flow class...")
async def async_step_user(self, user_input=None):
_LOGGER.debug("Starting user step")
errors = {}
if user_input is None:
_LOGGER.debug("Show form because user input is empty")
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
_LOGGER.debug("User input is not empty, processing input")
try:
_LOGGER.debug("Initial attempt to query ECU")
ap_ecu = APSystemsSocket(user_input["host"], user_input["stop_graphs"])
test_query = await self.hass.async_add_executor_job(ap_ecu.query_ecu)
ecu_id = test_query.get("ecu_id", None)
if ecu_id != None:
return self.async_create_entry(title=f"ECU: {ecu_id}", data=user_input)
else:
errors["host"] = "no_ecuid"
except APSystemsInvalidData as err:
_LOGGER.exception(f"APSystemsInvalidData exception: {err}")
errors["host"] = "cannot_connect"
except Exception as err:
_LOGGER.exception(f"Unknown error occurred during setup: {err}")
errors["host"] = "unknown"
@staticmethod
@callback
def async_get_options_flow(config_entry):
_LOGGER.debug("get options flow")
return APSsystemsOptionsFlowHandler(config_entry)
class APSsystemsOptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
_LOGGER.debug("Starting options flow step class")
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
errors = {}
if user_input is None:
return self.async_show_form(
step_id="init",
errors=errors,
data_schema=vol.Schema({
vol.Required(CONF_HOST, default=self.config_entry.data.get(CONF_HOST)): str,
vol.Optional(CONF_SCAN_INTERVAL, default=300,
description={"suggested_value": self.config_entry.data.get(CONF_SCAN_INTERVAL)}): int,
vol.Optional(CONF_CACHE, default=5,
description={"suggested_value": self.config_entry.data.get(CONF_CACHE)}): int,
vol.Optional(CONF_SSID, default="ECU-WiFi_SSID",
description={"suggested_value": self.config_entry.data.get(CONF_SSID)}): str,
vol.Optional(CONF_WPA_PSK, default="myWiFipassword",
description={"suggested_value": self.config_entry.data.get(CONF_WPA_PSK)}): str,
vol.Optional(CONF_STOP_GRAPHS, default=self.config_entry.data.get(CONF_STOP_GRAPHS)): bool
})
)
try:
ap_ecu = APSystemsSocket(user_input["host"], user_input["stop_graphs"])
_LOGGER.debug("Attempt to query ECU")
test_query = await self.hass.async_add_executor_job(ap_ecu.query_ecu)
ecu_id = test_query.get("ecu_id", None)
if ecu_id != None:
self.hass.config_entries.async_update_entry(
self.config_entry, data=user_input, options=self.config_entry.options
)
coordinator = self.hass.data[DOMAIN].get("coordinator")
coordinator.update_interval = timedelta(seconds=self.config_entry.data.get(CONF_SCAN_INTERVAL))
return self.async_create_entry(title=f"ECU: {ecu_id}", data={})
else:
errors["host"] = "no_ecuid"
except APSystemsInvalidData as err:
errors["host"] = "cannot_connect"
except Exception as err:
_LOGGER.debug(f"Unknown error occurred during setup: {err}")
errors["host"] = "unknown"

View File

@@ -0,0 +1,13 @@
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"
RESTART_ICON = "mdi:restart"
POWER_ICON = "mdi:power"
CONF_SSID = "SSID"
CONF_WPA_PSK = "WPA-PSK"
CONF_CACHE = "CACHE"
CONF_STOP_GRAPHS = "stop_graphs"

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
import logging
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
TO_REDACT = {CONF_TOKEN}
from .const import (
DOMAIN
)
_LOGGER = logging.getLogger(__name__)
async def async_get_device_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry
) -> dict:
"""Return diagnostics for a config entry."""
_LOGGER.debug("Diagnostics being called")
ecu = hass.data[DOMAIN].get("ecu")
_LOGGER.debug(f"Diagnostics being called {ecu}")
diag_data = {"entry": async_redact_data(ecu.ecu.dump_data(), TO_REDACT)}
return diag_data

View File

@@ -0,0 +1,14 @@
{
"domain": "apsystems_ecur",
"name": "APSystems PV solar ECU",
"codeowners": ["@ksheumaker"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/ksheumaker/homeassistant-apsystems_ecur",
"integration_type": "hub",
"iot_class": "local_polling",
"issue_tracker": "https://github.com/ksheumaker/homeassistant-apsystems_ecur/issues",
"loggers": ["custom_components.apsystems_ecur"],
"requirements": [],
"version": "v1.4.3"
}

View File

@@ -0,0 +1,284 @@
from datetime import timedelta, datetime, date
import logging
import async_timeout
from homeassistant.util import dt as dt_util
from homeassistant.components.sensor import SensorEntity
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorStateClass,
)
from .const import (
DOMAIN,
SOLAR_ICON,
FREQ_ICON,
SIGNAL_ICON
)
from homeassistant.const import (
UnitOfPower,
UnitOfEnergy,
UnitOfTemperature,
UnitOfElectricPotential,
UnitOfFrequency,
PERCENTAGE
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(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=UnitOfPower.WATT,
devclass=SensorDeviceClass.POWER,
icon=SOLAR_ICON,
stateclass=SensorStateClass.MEASUREMENT
),
APSystemsECUSensor(coordinator, ecu, "today_energy",
label="Today Energy",
unit=UnitOfEnergy.KILO_WATT_HOUR,
devclass=SensorDeviceClass.ENERGY,
icon=SOLAR_ICON,
stateclass=SensorStateClass.TOTAL_INCREASING
),
APSystemsECUSensor(coordinator, ecu, "lifetime_energy",
label="Lifetime Energy",
unit=UnitOfEnergy.KILO_WATT_HOUR,
devclass=SensorDeviceClass.ENERGY,
icon=SOLAR_ICON,
stateclass=SensorStateClass.TOTAL_INCREASING
),
APSystemsECUSensor(coordinator, ecu, "qty_of_inverters",
label="Inverters",
icon=SOLAR_ICON,
entity_category=EntityCategory.DIAGNOSTIC
),
APSystemsECUSensor(coordinator, ecu, "qty_of_online_inverters",
label="Inverters Online",
icon=SOLAR_ICON,
entity_category=EntityCategory.DIAGNOSTIC
),
]
inverters = coordinator.data.get("inverters", {})
for uid,inv_data in inverters.items():
_LOGGER.debug(f"Inverter {uid} {inv_data.get('channel_qty')}")
# https://github.com/ksheumaker/homeassistant-apsystems_ecur/issues/110
if inv_data.get("channel_qty") != None:
sensors.extend([
APSystemsECUInverterSensor(coordinator, ecu, uid, "temperature",
label="Temperature",
unit=UnitOfTemperature.CELSIUS,
devclass=SensorDeviceClass.TEMPERATURE,
stateclass=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC
),
APSystemsECUInverterSensor(coordinator, ecu, uid, "frequency",
label="Frequency",
unit=UnitOfFrequency.HERTZ,
stateclass=SensorStateClass.MEASUREMENT,
devclass=SensorDeviceClass.FREQUENCY,
icon=FREQ_ICON,
entity_category=EntityCategory.DIAGNOSTIC
),
APSystemsECUInverterSensor(coordinator, ecu, uid, "voltage",
label="Voltage",
unit=UnitOfElectricPotential.VOLT,
stateclass=SensorStateClass.MEASUREMENT,
devclass=SensorDeviceClass.VOLTAGE, entity_category=EntityCategory.DIAGNOSTIC
),
APSystemsECUInverterSensor(coordinator, ecu, uid, "signal",
label="Signal",
unit=PERCENTAGE,
stateclass=SensorStateClass.MEASUREMENT,
devclass=SensorDeviceClass.SIGNAL_STRENGTH,
icon=SIGNAL_ICON,
entity_category=EntityCategory.DIAGNOSTIC
)
])
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=UnitOfPower.WATT,
devclass=SensorDeviceClass.POWER,
icon=SOLAR_ICON,
stateclass=SensorStateClass.MEASUREMENT
)
)
add_entities(sensors)
class APSystemsECUInverterSensor(CoordinatorEntity, SensorEntity):
def __init__(self, coordinator, ecu, uid, field, index=0, label=None, icon=None, unit=None, devclass=None, stateclass=None, entity_category=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._stateclass = stateclass
self._entity_category = entity_category
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.debug(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.debug(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 extra_state_attributes(self):
attrs = {
"ecu_id" : self._ecu.ecu.ecu_id,
"inverter_uid" : self._uid,
"last_update" : self._ecu.ecu.last_update,
}
return attrs
@property
def state_class(self):
_LOGGER.debug(f"State class {self._stateclass} - {self._field}")
return self._stateclass
@property
def device_info(self):
parent = f"inverter_{self._uid}"
return {
"identifiers": {
(DOMAIN, parent),
}
}
@property
def entity_category(self):
return self._entity_category
class APSystemsECUSensor(CoordinatorEntity, SensorEntity):
def __init__(self, coordinator, ecu, field, label=None, icon=None, unit=None, devclass=None, stateclass=None, entity_category=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._stateclass = stateclass
self._entity_category = entity_category
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.debug(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 extra_state_attributes(self):
attrs = {
"ecu_id" : self._ecu.ecu.ecu_id,
"Firmware" : self._ecu.ecu.firmware,
"Timezone" : self._ecu.ecu.timezone,
"last_update" : self._ecu.ecu.last_update
}
return attrs
@property
def state_class(self):
_LOGGER.debug(f"State class {self._stateclass} - {self._field}")
return self._stateclass
@property
def device_info(self):
parent = f"ecu_{self._ecu.ecu.ecu_id}"
return {
"identifiers": {
(DOMAIN, parent),
}
}
@property
def entity_category(self):
return self._entity_category

View File

@@ -0,0 +1,132 @@
import logging
from homeassistant.util import dt as dt_util
from homeassistant.components.switch import SwitchEntity
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity
)
from .const import (
DOMAIN,
RELOAD_ICON,
POWER_ICON
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config, add_entities, discovery_info=None):
ecu = hass.data[DOMAIN].get("ecu")
coordinator = hass.data[DOMAIN].get("coordinator")
switches = [
APSystemsECUQuerySwitch(coordinator, ecu, "query_device",
label="Query Device", icon=RELOAD_ICON),
APSystemsECUInvertersSwitch(coordinator, ecu, "inverters_online",
label="Inverters Online", icon=POWER_ICON),
]
add_entities(switches)
class APSystemsECUQuerySwitch(CoordinatorEntity, SwitchEntity):
def __init__(self, coordinator, ecu, field, label=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 = True
@property
def unique_id(self):
return f"{self._ecu.ecu.ecu_id}_{self._field}"
@property
def name(self):
return self._name
@property
def icon(self):
return self._icon
@property
def device_info(self):
parent = f"ecu_{self._ecu.ecu.ecu_id}"
return {
"identifiers": {
(DOMAIN, parent),
}
}
@property
def entity_category(self):
return EntityCategory.CONFIG
@property
def is_on(self):
return self._ecu.querying
def turn_off(self, **kwargs):
self._ecu.stop_query()
self._state = False
self.schedule_update_ha_state()
def turn_on(self, **kwargs):
self._ecu.start_query()
self._state = True
self.schedule_update_ha_state()
class APSystemsECUInvertersSwitch(CoordinatorEntity, SwitchEntity):
def __init__(self, coordinator, ecu, field, label=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 = True
@property
def unique_id(self):
return f"{self._ecu.ecu.ecu_id}_{self._field}"
@property
def name(self):
return self._name
@property
def icon(self):
return self._icon
@property
def device_info(self):
parent = f"ecu_{self._ecu.ecu.ecu_id}"
return {
"identifiers": {
(DOMAIN, parent),
}
}
@property
def entity_category(self):
return EntityCategory.CONFIG
@property
def is_on(self):
return self._ecu.inverters_online
def turn_off(self, **kwargs):
self._ecu.inverters_off()
self._state = False
self.schedule_update_ha_state()
def turn_on(self, **kwargs):
self._ecu.inverters_on()
self._state = True
self.schedule_update_ha_state()

View File

@@ -0,0 +1,48 @@
{
"options": {
"step": {
"init": {
"data": {
"host": "ECU IP-Adresse (bitte Verbindungstabelle in der readme prüfen).",
"scan_interval": "ECU Abfrage Frequenz in Sekunden (minimum 300 empfohlen).",
"CACHE": "Wiederholungen, wenn ECU ausfällt (Bereich zwischen 1 - 5 empfohlen)",
"SSID": "SSID angeben (nur für Modelle ECU-R (sunspec) und ECU-C)",
"WPA-PSK": "Kennwort angeben (nur für Modelle ECU-R (sunspec) und ECU-C)",
"stop_graphs": "Aktualisieren die Diagramme nicht, wenn die Wechselrichter offline sind"
},
"title": "APsystems ECU Konfiguration"
}
},
"error": {
"cannot_connect": "Es wurde keine ECU unter dieser IP-Adresse gefunden oder life-time energy ist Null.",
"no_ecuid": "Es wurde keine ECU ID von der ECU zurückgegeben.",
"unknown": "Unbekannter Fehler, bitte Logs überprüfen."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"config": {
"step": {
"user": {
"data": {
"host": "ECU IP-Adresse (bitte Verbindungstabelle in der readme prüfen).",
"scan_interval": "ECU Abfrage Frequenz in Sekunden (minimum 300 empfohlen).",
"CACHE": "Wiederholungen, wenn ECU ausfällt (Bereich zwischen 1 - 5 empfohlen)",
"SSID": "SSID angeben (nur für Modelle ECU-R (sunspec) und ECU-C)",
"WPA-PSK": "Kennwort angeben (nur für Modelle ECU-R (sunspec) und ECU-C)",
"stop_graphs": "Aktualisieren die Diagramme nicht, wenn die Wechselrichter offline sind"
},
"title": "APsystems ECU Optionen"
}
},
"error": {
"cannot_connect": "Es wurde keine ECU unter dieser IP-Adresse gefunden oder life-time energy ist Null.",
"no_ecuid": "Es wurde keine ECU ID von der ECU zurückgegeben.",
"unknown": "Unbekannter Fehler, bitte Logs überprüfen."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@@ -0,0 +1,48 @@
{
"options": {
"step": {
"init": {
"data": {
"host": "ECU IP address (follow connection method table in readme)",
"scan_interval": "ECU query interval in seconds (minimum 300 recommended)",
"CACHE": "Retries when ECU fails (range between 1 - 5 recommended)",
"SSID": "Specify SSID (For ECU-R (sunspec) and ECU-C models only)",
"WPA-PSK": "Specify password (For ECU-R (sunspec) and ECU-C models only)",
"stop_graphs": "Do not update graphs when inverters are offline"
},
"title": "APsystems ECU Config"
}
},
"error": {
"cannot_connect": "Can't find ECU at this IP-Address or life-time energy is zero",
"no_ecuid": "No ECU ID returned from ECU",
"unknown": "Unknown error, see log for details"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"config": {
"step": {
"user": {
"data": {
"host": "ECU IP address (follow connection method table in readme)",
"scan_interval": "ECU query interval in seconds (minimum 300 recommended)",
"CACHE": "Retries when ECU fails (range between 1 - 5 recommended)",
"SSID": "Specify SSID (For ECU-R (sunspec) and ECU-C models only)",
"WPA-PSK": "Specify password (For ECU-R (sunspec) and ECU-C models only)",
"stop_graphs": "Do not update graphs when inverters are offline"
},
"title": "APsystems ECU Options"
}
},
"error": {
"cannot_connect": "Can't find ECU at this IP-Address or life-time energy is zero",
"no_ecuid": "No ECU ID returned from ECU",
"unknown": "Unknown error, see log for details"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@@ -0,0 +1,48 @@
{
"options": {
"step": {
"init": {
"data": {
"host": "Dirección IP de la ECU (Sigue el método para conectarse en la tabla del archivo readme)",
"scan_interval": "Intervalo de conexión a la ECU en segundos (Mínimo 300 recomendado)",
"CACHE": "Reintentos cuando la ECU falla (Rango entre 1 - 5 recomendado)",
"SSID": "Introduce SSID (Solo para modelos ECU-R (sunspec) and ECU-C)",
"WPA-PSK": "Introduce contraseña (Solo para modelos ECU-R (sunspec) and ECU-C)",
"stop_graphs": "No actualice los gráficos cuando los inversores estén fuera de línea"
},
"title": "Configuración APsystems ECU"
}
},
"error": {
"cannot_connect": "No puedo encontrar la ECU en esta dirección IP o la energía life-time es cero",
"no_ecuid": "La ECU no ha devuelto ningún ECU ID",
"unknown": "Error desconocido, lee el log para mas detalles"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"config": {
"step": {
"user": {
"data": {
"host": "Dirección IP de la ECU (Sigue el método para conectarse en la tabla del archivo readme)",
"scan_interval": "Intervalo de conexión a la ECU en segundos (Mínimo 300 recomendado)",
"CACHE": "Reintentos cuando la ECU falla (Rango entre 1 - 5 recomendado)",
"SSID": "Introduce SSID (Solo para modelos ECU-R (sunspec) and ECU-C)",
"WPA-PSK": "Introduce contraseña (Solo para modelos ECU-R (sunspec) and ECU-C)",
"stop_graphs": "No actualice los gráficos cuando los inversores estén fuera de línea"
},
"title": "Configuración APsystems ECU"
}
},
"error": {
"cannot_connect": "No puedo encontrar la ECU en esta dirección IP o la energía life-time es cero",
"no_ecuid": "La ECU no ha devuelto ningún ECU ID",
"unknown": "Error desconocido, lee el log para mas detalles"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@@ -0,0 +1,49 @@
{
"options": {
"step": {
"init": {
"data": {
"host": "Adresse IP ECU (Voir “Prerequisites” dans le fichier Readme)",
"scan_interval": "Intervalle des requêtes sur l'ECU en secondes (Min de 300 recommandées)",
"CACHE": "Nombre de tentatives en cas d'échec de communication (1 à 5 recommandée)",
"SSID": "Spécifier le SSID (Pour ECU-R (Sunspec) et ECU-C seulement)",
"WPA-PSK": "Spécifier le mot de passe (Pour ECU-R (Sunspec) et ECU-C seulement)",
"stop_graphs": "Ne pas mettre à jour les graphiques lorsque les onduleurs sont hors ligne"
},
"title": "Configuration ECU APsystems"
}
},
"error": {
"cannot_connect": "Ne trouve pas d'ECU à cette adresse IP ou énergie totale produite nulle",
"no_ecuid": "Pas d'ID ECU retourné pour cet ECU",
"unknown": "Erreur inconnue, veuillez consulter le journal des logs pour plus de détails"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"config": {
"step": {
"user": {
"data": {
"host": "Adresse IP ECU (Voir “Prerequisites” dans le fichier Readme)",
"scan_interval": "Intervalle des requêtes sur l'ECU en secondes (Min de 300 recommandées)",
"CACHE": "Nombre de tentatives en cas d'échec de communication (1 à 5 recommandée)",
"SSID": "Spécifier le SSID (Pour ECU-R (Sunspec) et ECU-C seulement)",
"WPA-PSK": "Spécifier le mot de passe (Pour ECU-R (Sunspec) et ECU-C seulement)",
"stop_graphs": "Ne pas mettre à jour les graphiques lorsque les onduleurs sont hors ligne"
},
"title": "Options ECU APsystems"
}
},
"error": {
"cannot_connect": "Ne trouve pas d'ECU à cette adresse IP ou énergie totale produite nulle",
"inverter": "Type d'onduleur inconnu, veuillez vérifier les journaux",
"no_ecuid": "Pas d'ID ECU retourné pour cet ECU",
"unknown": "Erreur inconnue, veuillez consulter le journal des logs pour plus de détails"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@@ -0,0 +1,48 @@
{
"options": {
"step": {
"init": {
"data": {
"host": "ECU IP-adres (volg de connectie methode tabel in de readme)",
"scan_interval": "ECU query interval in seconden (minimum 300 aanbevolen)",
"CACHE": "Pogingen als de ECU niet reageert (tussen 1 - 5 aanbevolen)",
"SSID": "Specificeer SSID (Alleen voor ECU-R (sunspec) en ECU-C modellen)",
"WPA-PSK": "Specificeer wachtwoord (voor ECU-R (sunspec) en ECU-C modellen)",
"stop_graphs": "Werk grafieken niet bij als de omvormers offline zijn"
},
"title": "APsystems ECU Configuratie"
}
},
"error": {
"cannot_connect": "Kan de ECU niet vinden op dit IP-adres of life-time energy is nul",
"no_ecuid": "Geen ECU ID ontvangen",
"unknown": "Onbekende fout, zie het log for details"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"config": {
"step": {
"user": {
"data": {
"host": "ECU IP-adres (volg de connectie methode tabel in de readme)",
"scan_interval": "ECU query interval in seconden (minimum 300 aanbevolen)",
"CACHE": "Pogingen als de ECU niet reageert (tussen 1 - 5 aanbevolen)",
"SSID": "Specificeer SSID (Alleen voor ECU-R (sunspec) en ECU-C modellen)",
"WPA-PSK": "Specificeer wachtwoord (voor ECU-R (sunspec) en ECU-C modellen)",
"stop_graphs": "Werk grafieken niet bij als de omvormers offline zijn"
},
"title": "APsystems ECU Opties"
}
},
"error": {
"cannot_connect": "Kan de ECU niet vinden op dit IP-adres of life-time energy is nul",
"no_ecuid": "Geen ECU ID ontvangen",
"unknown": "Onbekende fout, zie het log voor details"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}