242 lines
9.5 KiB
Python
242 lines
9.5 KiB
Python
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
|