Skip to content

Commit

Permalink
better invalid credential handling
Browse files Browse the repository at this point in the history
stats available is dynamic now
  • Loading branch information
marq24 authored and marq24 committed Sep 9, 2023
1 parent 5aff26b commit 46f5ba1
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 74 deletions.
85 changes: 49 additions & 36 deletions custom_components/senec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@
MANUFACTURE,
DEFAULT_HOST,
DEFAULT_NAME,
DEFAULT_SCAN_INTERVAL_SENECV2,
CONF_USE_HTTPS,
CONF_DEV_TYPE,
CONF_DEV_NAME,
CONF_DEV_SERIAL,
CONF_DEV_VERSION,
CONF_SYSTYPE_SENEC,
CONF_SYSTYPE_SENEC_V2,
CONF_SYSTYPE_INVERTER,
CONF_SYSTYPE_WEB,
CONF_DEV_MASTER_NUM
CONF_DEV_MASTER_NUM,
)

_LOGGER = logging.getLogger(__name__)
Expand All @@ -42,56 +45,69 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Set up senec from a config entry."""
global SCAN_INTERVAL
SCAN_INTERVAL = timedelta(seconds=entry.data.get(CONF_SCAN_INTERVAL, 60))
SCAN_INTERVAL = timedelta(seconds=config_entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL_SENECV2))

session = async_get_clientsession(hass)

coordinator = SenecDataUpdateCoordinator(hass, session, entry)
coordinator = SenecDataUpdateCoordinator(hass, session, config_entry)

await coordinator.async_refresh()

if not coordinator.last_update_success:
raise ConfigEntryNotReady

# after the refresh we should know if the lala.cgi return STATISTIC data
# or not...
if CONF_TYPE not in config_entry.data or config_entry.data[CONF_TYPE] in (CONF_SYSTYPE_SENEC, CONF_SYSTYPE_SENEC_V2):
coordinator._statistics_available = coordinator.senec.grid_total_export is not None

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
hass.data[DOMAIN][config_entry.entry_id] = coordinator

for platform in PLATFORMS:
hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, platform))
hass.async_create_task(hass.config_entries.async_forward_entry_setup(config_entry, platform))

entry.add_update_listener(async_reload_entry)
config_entry.add_update_listener(async_reload_entry)
return True


class SenecDataUpdateCoordinator(DataUpdateCoordinator):
"""Define an object to hold Senec data."""

def __init__(self, hass, session, entry):
def __init__(self, hass, session, config_entry):
"""Initialize."""
if CONF_TYPE in entry.data and entry.data[CONF_TYPE] == CONF_SYSTYPE_INVERTER:
self._host = entry.data[CONF_HOST]

# Build-In INVERTER
if CONF_TYPE in config_entry.data and config_entry.data[CONF_TYPE] == CONF_SYSTYPE_INVERTER:
self._host = config_entry.data[CONF_HOST]
self.senec = Inverter(self._host, websession=session)
if CONF_TYPE in entry.data and entry.data[CONF_TYPE] == CONF_SYSTYPE_WEB:

# WEB-API Version...
if CONF_TYPE in config_entry.data and config_entry.data[CONF_TYPE] == CONF_SYSTYPE_WEB:
self._host = "mein-senec.de"

a_master_plant_number = 0
if CONF_DEV_MASTER_NUM in entry.data:
a_master_plant_number = entry.data[CONF_DEV_MASTER_NUM]
self.senec = MySenecWebPortal(user=entry.data[CONF_USERNAME], pwd=entry.data[CONF_PASSWORD],
if CONF_DEV_MASTER_NUM in config_entry.data:
a_master_plant_number = config_entry.data[CONF_DEV_MASTER_NUM]

self.senec = MySenecWebPortal(user=config_entry.data[CONF_USERNAME], pwd=config_entry.data[CONF_PASSWORD],
websession=session, master_plant_number=a_master_plant_number)
# lala.cgi Version...
else:
self._host = entry.data[CONF_HOST]
if CONF_USE_HTTPS in entry.data:
self._use_https = entry.data[CONF_USE_HTTPS]
self._host = config_entry.data[CONF_HOST]
if CONF_USE_HTTPS in config_entry.data:
self._use_https = config_entry.data[CONF_USE_HTTPS]
else:
self._use_https = False
self.senec = Senec(host=self._host, use_https=self._use_https, websession=session)

self.name = entry.title
self._entry = entry
self.senec = Senec(host=self._host, use_https=self._use_https, websession=session)

self.name = config_entry.title
self._config_entry = config_entry
self._statistics_available = False
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)

async def _async_update_data(self):
Expand All @@ -109,25 +125,25 @@ async def _async_switch_to_state(self, switch_key, state):
raise UpdateFailed() from exception


async def async_unload_entry(hass, entry):
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Unload Senec config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
hass.config_entries.async_forward_entry_unload(config_entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok


async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)
await async_unload_entry(hass, config_entry)
await async_setup_entry(hass, config_entry)


class SenecEntity(Entity):
Expand All @@ -140,26 +156,23 @@ def __init__(
) -> None:
self.coordinator = coordinator
self.entity_description = description
self._name = coordinator._entry.title
self._name = coordinator._config_entry.title
self._state = None
self._entry = coordinator._entry
self._host = coordinator._host

@property
def device_info(self) -> dict:
"""Return info for device registry."""
# Setup Device
dtype = self._entry.options.get(CONF_DEV_TYPE, self._entry.data.get(CONF_DEV_TYPE))
dserial = self._entry.options.get(CONF_DEV_SERIAL, self._entry.data.get(CONF_DEV_SERIAL))
dmodel = self._entry.options.get(CONF_DEV_NAME, self._entry.data.get(CONF_DEV_NAME))
dtype = self.coordinator._config_entry.options.get(CONF_DEV_TYPE, self.coordinator._config_entry.data.get(CONF_DEV_TYPE))
dserial = self.coordinator._config_entry.options.get(CONF_DEV_SERIAL, self.coordinator._config_entry.data.get(CONF_DEV_SERIAL))
dmodel = self.coordinator._config_entry.options.get(CONF_DEV_NAME, self.coordinator._config_entry.data.get(CONF_DEV_NAME))
device = self._name
host = self._host
# "hw_version": self._entry.options.get(CONF_DEV_NAME, self._entry.data.get(CONF_DEV_NAME)),
# "hw_version": self.coordinator._config_entry.options.get(CONF_DEV_NAME, self.coordinator._config_entry.data.get(CONF_DEV_NAME)),
return {
"identifiers": {(DOMAIN, host, device)},
"identifiers": {(DOMAIN, self.coordinator._host, device)},
"name": f"{dtype}: {device}",
"model": f"{dmodel} [Serial: {dserial}]",
"sw_version": self._entry.options.get(CONF_DEV_VERSION, self._entry.data.get(CONF_DEV_VERSION)),
"sw_version": self.coordinator._config_entry.options.get(CONF_DEV_VERSION, self.coordinator._config_entry.data.get(CONF_DEV_VERSION)),
"manufacturer": MANUFACTURE,
}

Expand Down
2 changes: 1 addition & 1 deletion custom_components/senec/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def __init__(
else:
self._attr_entity_registry_enabled_default = True

title = self.coordinator._entry.title
title = self.coordinator._config_entry.title
key = self.entity_description.key
name = self.entity_description.name
self.entity_id = f"binary_sensor.{slugify(title)}_{key}"
Expand Down
36 changes: 18 additions & 18 deletions custom_components/senec/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
CONF_DEV_TYPE,
CONF_DEV_TYPE_INT,
CONF_USE_HTTPS,
CONF_SUPPORT_STATS,
CONF_SUPPORT_BDC,
CONF_DEV_NAME,
CONF_DEV_SERIAL,
Expand Down Expand Up @@ -102,9 +101,6 @@ async def _test_connection_senec(self, host, use_https):
self._use_https = use_https
self._stats_available = senec_client.grid_total_export is not None

# just for local testing...
#self._stats_available = False

_LOGGER.info(
"Successfully connect to SENEC.Home (using https? %s) at %s",
use_https, host,
Expand Down Expand Up @@ -150,21 +146,25 @@ async def _test_connection_webapi(self, user, pwd):
websession = self.hass.helpers.aiohttp_client.async_get_clientsession()
try:
senec_web_client = MySenecWebPortal(user=user, pwd=pwd, websession=websession)
await senec_web_client.authenticate(doUpdate=True)
await senec_web_client.update_context()

# TODO: fetch VERSION and other Info from WebApi...
self._device_type = "SENEC WebAPI"
# self._device_type_internal = senec_client.device_type_internal
self._device_type = senec_web_client.product_name
self._device_name = 'SENEC.Num: ' + senec_web_client.senec_num
self._device_serial = senec_web_client.serial_number
self._device_version = senec_web_client.firmwareVersion
self._device_master_plant_number = senec_web_client.masterPlantNumber
_LOGGER.info("Successfully connect to mein-senec.de with '%s'", user)
return True
await senec_web_client.authenticate(doUpdate=False, throw401=True)
if senec_web_client._isAuthenticated:
await senec_web_client.update_context()
self._device_type = "SENEC WebAPI"
# self._device_type_internal = senec_client.device_type_internal
self._device_type = senec_web_client.product_name
self._device_name = 'SENEC.Num: ' + senec_web_client.senec_num
self._device_serial = senec_web_client.serial_number
self._device_version = senec_web_client.firmwareVersion
self._device_master_plant_number = senec_web_client.masterPlantNumber
_LOGGER.info("Successfully connect to mein-senec.de with '%s'", user)
return True
else:
self._errors[CONF_USERNAME] = "login_failed"
self._errors[CONF_PASSWORD] = "login_failed"
_LOGGER.warning("Could not connect to mein-senec.de with '%s', check credentials", user)
except (OSError, HTTPError, Timeout, ClientResponseError):
self._errors[CONF_USERNAME] = "login_failed"
self._errors[CONF_PASSWORD] = "login_failed"
_LOGGER.warning("Could not connect to mein-senec.de with '%s', check credentials", user)
return False

Expand Down Expand Up @@ -283,7 +283,6 @@ async def async_step_system(self, user_input=None):
CONF_USE_HTTPS: self._use_https,
CONF_SCAN_INTERVAL: scan,
CONF_TYPE: CONF_SYSTYPE_SENEC,
CONF_SUPPORT_STATS: self._stats_available,
CONF_DEV_TYPE_INT: self._device_type_internal,
CONF_DEV_TYPE: self._device_type,
CONF_DEV_NAME: self._device_name,
Expand Down Expand Up @@ -376,6 +375,7 @@ async def async_step_websetup(self, user_input=None):
else:
_LOGGER.error("Could not connect to mein-senec.de with User '%s', check credentials", user)
self._errors[CONF_USERNAME]
self._errors[CONF_PASSWORD]

else:
user_input = {}
Expand Down
1 change: 0 additions & 1 deletion custom_components/senec/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
CONF_DEV_TYPE_INT: Final = "dtype_int"
CONF_USE_HTTPS: Final = "use_https"
CONF_SUPPORT_BDC: Final = "has_bdc_support"
CONF_SUPPORT_STATS: Final = "has_statistics"
CONF_DEV_NAME: Final = "dname"
CONF_DEV_SERIAL: Final = "dserial"
CONF_DEV_VERSION: Final = "version"
Expand Down
28 changes: 16 additions & 12 deletions custom_components/senec/pysenec_ha/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,8 @@ def grid_total_export(self) -> float:
"""
Total energy exported to grid export (kWh)
"""
if hasattr(self, '_raw') and "STATISTIC" in self._raw and "LIVE_GRID_EXPORT" in self._raw["STATISTIC"]:
if hasattr(self, '_raw') and "STATISTIC" in self._raw and "LIVE_GRID_EXPORT" in self._raw["STATISTIC"] and \
self._raw["STATISTIC"]["LIVE_GRID_EXPORT"] != "VARIABLE_NOT_FOUND":
return self._raw["STATISTIC"]["LIVE_GRID_EXPORT"]

@property
Expand Down Expand Up @@ -1441,7 +1442,7 @@ async def getSystemOverviewClassic(self):
self._isAuthenticated = False
await self.update()

async def authenticate(self, doUpdate: bool):
async def authenticate(self, doUpdate: bool, throw401: bool):
_LOGGER.info("***** authenticate(self) ********")
self.checkCookieJarType()
auth_payload = {
Expand All @@ -1462,12 +1463,16 @@ async def authenticate(self, doUpdate: bool):
_LOGGER.error("Login failed with Code " + str(res.status))
self.purgeSenecCookies()
except ClientResponseError as exc:
if exc.status == 401:
self.purgeSenecCookies()
self._isAuthenticated = False
#_LOGGER.error(str(exc))
if throw401:
raise exc
else:
_LOGGER.error("Login exception with Code " + str(exc.status))
self.purgeSenecCookies()
if exc.status == 401:
self.purgeSenecCookies()
self._isAuthenticated = False
else:
_LOGGER.error("Login exception with Code " + str(exc.status))
self.purgeSenecCookies()

async def update(self):
if self._isAuthenticated:
Expand All @@ -1476,7 +1481,7 @@ async def update(self):
await self.update_now_kW_stats()
await self.update_full_kWh_stats()
else:
await self.authenticate(doUpdate=True)
await self.authenticate(doUpdate=True, throw401=False)

async def update_now_kW_stats(self):
_LOGGER.debug("***** update_now_kW_stats(self) ********")
Expand Down Expand Up @@ -1517,7 +1522,6 @@ async def update_now_kW_stats(self):
self._isAuthenticated = False
await self.update()


async def update_full_kWh_stats(self):
# grab TOTAL stats
a_url = f"{self._SENEC_API_URL_END}" % str(self._master_plant_number)
Expand Down Expand Up @@ -1548,7 +1552,7 @@ async def update_context(self):
await self.update_get_customer()
await self.update_get_systems(self._master_plant_number)
else:
await self.authenticate(doUpdate=False)
await self.authenticate(doUpdate=False, throw401=False)

async def update_get_customer(self):
_LOGGER.debug("***** update_get_customer(self) ********")
Expand All @@ -1568,7 +1572,7 @@ async def update_get_customer(self):
# nachname
else:
self._isAuthenticated = False
await self.authenticate(doUpdate=False)
await self.authenticate(doUpdate=False, throw401=False)

async def update_get_systems(self, a_plant_number: int):
_LOGGER.debug("***** update_get_systems(self) ********")
Expand Down Expand Up @@ -1596,7 +1600,7 @@ async def update_get_systems(self, a_plant_number: int):

else:
self._isAuthenticated = False
await self.authenticate(doUpdate=False)
await self.authenticate(doUpdate=False, throw401=False)

@property
def senec_num(self) -> str:
Expand Down
8 changes: 5 additions & 3 deletions custom_components/senec/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
INVERTER_SENSOR_TYPES,
WEB_SENSOR_TYPES,
CONF_SUPPORT_BDC,
CONF_SUPPORT_STATS,
CONF_SYSTYPE_INVERTER,
CONF_SYSTYPE_WEB
)
Expand All @@ -39,19 +38,22 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry,
if addEntity:
entity = SenecSensor(coordinator, description)
entities.append(entity)

if CONF_TYPE in config_entry.data and config_entry.data[CONF_TYPE] == CONF_SYSTYPE_WEB:
for description in WEB_SENSOR_TYPES:
entity = SenecSensor(coordinator, description)
entities.append(entity)

else:
for description in MAIN_SENSOR_TYPES:
addEntity = description.controls is None
if not addEntity:
if 'require_stats_fields' in description.controls:
if CONF_SUPPORT_STATS not in config_entry.data or config_entry.data[CONF_SUPPORT_STATS]:
if coordinator._statistics_available:
addEntity = True
else:
addEntity = True

if addEntity:
entity = SenecSensor(coordinator, description)
entities.append(entity)
Expand All @@ -74,7 +76,7 @@ def __init__(
else:
self._attr_entity_registry_enabled_default = True

title = self.coordinator._entry.title
title = self.coordinator._config_entry.title
key = self.entity_description.key
name = self.entity_description.name
self.entity_id = f"sensor.{slugify(title)}_{key}"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/senec/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def __init__(
else:
self._attr_entity_registry_enabled_default = True

title = self.coordinator._entry.title
title = self.coordinator._config_entry.title
key = self.entity_description.key
name = self.entity_description.name
self.entity_id = f"switch.{slugify(title)}_{key}"
Expand Down
Loading

0 comments on commit 46f5ba1

Please sign in to comment.