Files
homeassistant_config/config/custom_components/apsystems_ecur/__init__.py
2024-05-31 09:39:52 +02:00

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