66from datetime import datetime
77from http .cookies import BaseCookie , SimpleCookie , Morsel
88from time import time
9- from typing import Union , cast
9+ from typing import Union , cast , Final
1010
1111import aiohttp
1212import xmltodict
@@ -2690,6 +2690,8 @@ def yield_produced_total(self) -> float:
26902690 return self ._yield_produced_total
26912691 return None
26922692
2693+ #USER_AGENT: Final = "SENEC.App/4.8.1 (LMFD) okhttp/4.12.0"
2694+ USER_AGENT : Final = "SENEC.App okhttp/4.12.0"
26932695
26942696class MySenecWebPortal :
26952697 def __init__ (self , user , pwd , web_session , master_plant_number : int = 0 , lang : str = "en" , options : dict = None ):
@@ -2788,6 +2790,13 @@ def __init__(self, user, pwd, web_session, master_plant_number: int = 0, lang: s
27882790 self ._SENEC_APP_TOTAL_V2 = APP_BASE_URL2 + "v2/senec/systems/%s/measurements?resolution=YEAR&from=1514764800&to=%s"
27892791 self ._SENEC_APP_TECHDATA = APP_BASE_URL2 + "v1/senec/systems/%s/technical-data"
27902792
2793+ # 2025/06/11 [NEW APP URLS -but with LESS DATA!]
2794+ # APP_BASE_MEASSURE_URL3 = "https://senec-app-measurements-proxy.prod.senec.dev/"
2795+ # APP_BASE_SYSTEM_URL3 = "https://senec-app-systems-proxy.prod.senec.dev/"
2796+ # self._SENEC_APP_NOW = f"{APP_BASE_MEASSURE_URL3}v1/systems/%s/dashboard"
2797+ # self._SENEC_APP_TOTAL_V3= f"{APP_BASE_MEASSURE_URL3}v1/systems/%s/measurements?resolution=YEAR&from=2018-01-01T00:00:00Z&to=%s"
2798+ # self._SENEC_APP_TECHDATA= f"{APP_BASE_SYSTEM_URL3}systems/%s/details"
2799+
27912800 # https://app-gateway.prod.senec.dev/v1/senec/systems/%s/abilities
27922801 # https://app-gateway.prod.senec.dev/v1/senec/systems/%s/operational-mode -> "COM70"
27932802
@@ -2911,7 +2920,7 @@ async def app_authenticate(self, retry: bool = True, do_update: bool = False):
29112920 "username" : self ._SENEC_USERNAME ,
29122921 "password" : self ._SENEC_PASSWORD
29132922 }
2914- async with self .web_session .post (self ._SENEC_APP_AUTH , json = auth_payload , ssl = False ) as res :
2923+ async with self .web_session .post (self ._SENEC_APP_AUTH , headers = { "User-Agent" : USER_AGENT }, json = auth_payload , ssl = False ) as res :
29152924 try :
29162925 res .raise_for_status ()
29172926 if res .status == 200 :
@@ -2938,7 +2947,7 @@ async def app_authenticate(self, retry: bool = True, do_update: bool = False):
29382947 _LOGGER .warning (f"APP-API: Login failed with Code { res .status } " )
29392948
29402949 except ClientResponseError as ioexc :
2941- _LOGGER .warning (f"APP-API: Could not login to APP-API: { ioexc } " )
2950+ _LOGGER .warning (f"APP-API: Could not login to APP-API: { type ( ioexc ) } - { ioexc } " )
29422951
29432952 async def app_update_context (self , retry : bool = True ):
29442953 _LOGGER .debug ("***** app_update_context(self) ********" )
@@ -2952,7 +2961,7 @@ async def app_update_context(self, retry: bool = True):
29522961 async def app_get_master_plant_id (self , retry : bool = True ):
29532962 _LOGGER .debug ("***** APP-API: get_master_plant_id(self) ********" )
29542963 if self ._app_is_authenticated :
2955- headers = {"Authorization" : self ._app_token }
2964+ headers = {"Authorization" : self ._app_token , "User-Agent" : USER_AGENT }
29562965 try :
29572966 async with self .web_session .get (self ._SENEC_APP_GET_SYSTEMS , headers = headers , ssl = False ) as res :
29582967 try :
@@ -3002,14 +3011,11 @@ async def app_get_master_plant_id(self, retry: bool = True):
30023011 await self .app_authenticate (retry = False )
30033012 except Exception as exc :
30043013 if res is not None :
3005- _LOGGER .error (
3006- f"APP-API: Error while access { self ._SENEC_APP_GET_SYSTEMS } : '{ exc } ' - Response is: '{ res } ' [retry={ retry } ]" )
3014+ _LOGGER .error (f"APP-API: Error while access { self ._SENEC_APP_GET_SYSTEMS } : '{ exc } ' - Response is: '{ res } ' [retry={ retry } ]" )
30073015 else :
3008- _LOGGER .error (
3009- f"APP-API: Error while access { self ._SENEC_APP_GET_SYSTEMS } : '{ exc } ' [retry={ retry } ]" )
3016+ _LOGGER .error (f"APP-API: Error while access { self ._SENEC_APP_GET_SYSTEMS } : '{ exc } ' [retry={ retry } ]" )
30103017 except Exception as exc :
3011- _LOGGER .error (
3012- f"APP-API: Error when try to call 'self.web_session.get()' for { self ._SENEC_APP_GET_SYSTEMS } : '{ exc } ' [retry={ retry } ]" )
3018+ _LOGGER .error (f"APP-API: Error when try to call 'self.web_session.get()' for { self ._SENEC_APP_GET_SYSTEMS } : '{ exc } ' [retry={ retry } ]" )
30133019 else :
30143020 if retry :
30153021 await self .app_authenticate (retry = False )
@@ -3019,7 +3025,7 @@ async def app_get_data(self, a_url: str) -> dict:
30193025 if self ._app_token is not None :
30203026 _LOGGER .debug (f"APP-API get { a_url } " )
30213027 try :
3022- headers = {"Authorization" : self ._app_token }
3028+ headers = {"Authorization" : self ._app_token , "User-Agent" : USER_AGENT }
30233029 async with self .web_session .get (url = a_url , headers = headers , ssl = False ) as res :
30243030 res .raise_for_status ()
30253031 if res .status == 200 :
@@ -3075,6 +3081,12 @@ async def app_update_total(self, retry: bool = True):
30753081 # status_url = f"{self._SENEC_APP_TOTAL}" % (
30763082 # str(self._app_master_plant_id), today.strftime('%Y'), today.strftime('%m'))
30773083 status_url = f"{ self ._SENEC_APP_TOTAL_V2 } " % (str (self ._app_master_plant_id ), str (int (today .timestamp ())))
3084+
3085+ # 2025/06/11 [might be required later, when SENEC will shut down the old endpoints]
3086+ # today = datetime.now(timezone.utc) + relativedelta(months=+1)
3087+ # to_date = today.strftime('%Y-%m-%dT%H:%M:%SZ')
3088+ # status_url = f"{self._SENEC_APP_TOTAL_V3}" % (str(self._app_master_plant_id), to_date)
3089+
30783090 data = await self .app_get_data (a_url = status_url )
30793091 if data is not None and "measurements" in data and "timeseries" in data :
30803092 self ._app_raw_total_v2 = data
@@ -3178,7 +3190,7 @@ async def app_post_data(self, a_url: str, post_data: dict, read_response: bool =
31783190 if self ._app_token is not None :
31793191 _LOGGER .debug (f"APP-API post { post_data } to { a_url } " )
31803192 try :
3181- headers = {"Authorization" : self ._app_token }
3193+ headers = {"Authorization" : self ._app_token , "User-Agent" : USER_AGENT }
31823194 async with self .web_session .post (url = a_url , headers = headers , json = post_data , ssl = False ) as res :
31833195 res .raise_for_status ()
31843196 if res .status == 200 :
@@ -3402,7 +3414,7 @@ async def app_set_allow_intercharge_all(self, value_to_set: bool, sync: bool = T
34023414 async def app_get_system_abilities (self ):
34033415 # 'app_get_system_abilities' not used (yet)
34043416 if self ._app_master_plant_id is not None and self ._app_token is not None :
3405- headers = {"Authorization" : self ._app_token }
3417+ headers = {"Authorization" : self ._app_token , "User-Agent" : USER_AGENT }
34063418 a_url = f"{ self ._SENEC_APP_GET_ABILITIES } " % str (self ._app_master_plant_id )
34073419 async with self .web_session .get (url = a_url , headers = headers , ssl = False ) as res :
34083420 res .raise_for_status ()
@@ -3550,7 +3562,7 @@ async def update(self):
35503562 async def update_peak_shaving (self ):
35513563 _LOGGER .info ("***** update_peak_shaving(self) ********" )
35523564 a_url = f"{ self ._SENEC_API_GET_PEAK_SHAVING } { self ._master_plant_number } "
3553- async with self .web_session .get (a_url , ssl = False ) as res :
3565+ async with self .web_session .get (a_url , headers = { "User-Agent" : USER_AGENT }, ssl = False ) as res :
35543566 try :
35553567 res .raise_for_status ()
35563568 if res .status == 200 :
@@ -3586,7 +3598,7 @@ async def set_peak_shaving(self, new_peak_shaving: dict):
35863598 # Senec self allways sends all get-parameter, even if not needed. So we will do it the same way
35873599 a_url = f"{ self ._SENEC_API_SET_PEAK_SHAVING_BASE_URL } { self ._master_plant_number } &mode={ new_peak_shaving ['mode' ].upper ()} &capacityLimit={ new_peak_shaving ['capacity' ]} &endzeit={ new_peak_shaving ['end_time' ]} "
35883600
3589- async with self .web_session .post (a_url , ssl = False ) as res :
3601+ async with self .web_session .post (a_url , headers = { "User-Agent" : USER_AGENT }, ssl = False ) as res :
35903602 try :
35913603 res .raise_for_status ()
35923604 if res .status == 200 :
@@ -3612,7 +3624,7 @@ async def set_peak_shaving(self, new_peak_shaving: dict):
36123624 async def update_spare_capacity (self ):
36133625 _LOGGER .info ("***** update_spare_capacity(self) ********" )
36143626 a_url = f"{ self ._SENEC_API_SPARE_CAPACITY_BASE_URL } { self ._master_plant_number } { self ._SENEC_API_GET_SPARE_CAPACITY } "
3615- async with self .web_session .get (a_url , ssl = False ) as res :
3627+ async with self .web_session .get (a_url , headers = { "User-Agent" : USER_AGENT }, ssl = False ) as res :
36163628 try :
36173629 res .raise_for_status ()
36183630 if res .status == 200 :
@@ -3639,7 +3651,7 @@ async def set_spare_capacity(self, new_spare_capacity: int):
36393651 _LOGGER .debug ("***** set_spare_capacity(self) ********" )
36403652 a_url = f"{ self ._SENEC_API_SPARE_CAPACITY_BASE_URL } { self ._master_plant_number } { self ._SENEC_API_SET_SPARE_CAPACITY } { new_spare_capacity } "
36413653
3642- async with self .web_session .post (a_url , ssl = False ) as res :
3654+ async with self .web_session .post (a_url , headers = { "User-Agent" : USER_AGENT }, ssl = False ) as res :
36433655 try :
36443656 res .raise_for_status ()
36453657 if res .status == 200 :
@@ -3748,7 +3760,7 @@ async def update_sgready_state(self):
37483760 if self .SGREADY_SUPPORTED :
37493761 _LOGGER .info ("***** update_update_sgready_state(self) ********" )
37503762 a_url = f"{ self ._SENEC_API_GET_SGREADY_STATE } " % (str (self ._master_plant_number ))
3751- async with self .web_session .get (a_url , ssl = False ) as res :
3763+ async with self .web_session .get (a_url , headers = { "User-Agent" : USER_AGENT }, ssl = False ) as res :
37523764 try :
37533765 res .raise_for_status ()
37543766 if res .status == 200 :
@@ -3781,7 +3793,7 @@ async def update_sgready_conf(self):
37813793 if self .SGREADY_SUPPORTED :
37823794 _LOGGER .info ("***** update_update_sgready_conf(self) ********" )
37833795 a_url = f"{ self ._SENEC_API_GET_SGREADY_CONF } " % (str (self ._master_plant_number ))
3784- async with self .web_session .get (a_url , ssl = False ) as res :
3796+ async with self .web_session .get (a_url , headers = { "User-Agent" : USER_AGENT }, ssl = False ) as res :
37853797 try :
37863798 res .raise_for_status ()
37873799 if res .status == 200 :
@@ -3821,7 +3833,7 @@ async def set_sgready_conf(self, new_sgready_data: dict):
38213833 post_data [a_key ] = self ._sgready_conf_data [a_key ]
38223834
38233835 if len (post_data ) > 0 and post_data_to_backend :
3824- async with self .web_session .post (a_url , ssl = False , json = post_data ) as res :
3836+ async with self .web_session .post (a_url , headers = { "User-Agent" : USER_AGENT }, ssl = False , json = post_data ) as res :
38253837 try :
38263838 res .raise_for_status ()
38273839 if res .status == 200 :
@@ -3867,7 +3879,7 @@ async def update_get_customer(self):
38673879 _LOGGER .debug ("***** update_get_customer(self) ********" )
38683880
38693881 # grab NOW and TODAY stats
3870- async with self .web_session .get (self ._SENEC_WEB_GET_CUSTOMER , ssl = False ) as res :
3882+ async with self .web_session .get (self ._SENEC_WEB_GET_CUSTOMER , headers = { "User-Agent" : USER_AGENT }, ssl = False ) as res :
38713883 res .raise_for_status ()
38723884 if res .status == 200 :
38733885 try :
@@ -3890,7 +3902,7 @@ async def update_get_systems(self, a_plant_number: int, autodetect_mode: bool):
38903902 _LOGGER .debug ("***** update_get_systems(self) ********" )
38913903
38923904 a_url = f"{ self ._SENEC_WEB_GET_SYSTEM_INFO } " % str (a_plant_number )
3893- async with self .web_session .get (a_url , ssl = False ) as res :
3905+ async with self .web_session .get (a_url , headers = { "User-Agent" : USER_AGENT }, ssl = False ) as res :
38943906 res .raise_for_status ()
38953907 if res .status == 200 :
38963908 try :
@@ -4198,7 +4210,10 @@ def system_state(self) -> str:
41984210 # 'mainControllerState': {'name': 'EIGENVERBRAUCH', 'severity': 'INFO'}, 'firmwareVersion': '123',
41994211 # 'guiVersion': 123}, 'warranty': {'endDate': 1700000000, 'warrantyTermInMonths': 123},
42004212 if self ._app_raw_tech_data is not None and "mcu" in self ._app_raw_tech_data :
4201- return self ._app_raw_tech_data ["mcu" ]["mainControllerState" ]["name" ].replace ('_' , ' ' )
4213+ if "mainControllerState" in self ._app_raw_tech_data ["mcu" ]:
4214+ return self ._app_raw_tech_data ["mcu" ]["mainControllerState" ]["name" ].replace ('_' , ' ' )
4215+ elif "mainControllerUnitState" in self ._app_raw_tech_data ["mcu" ]:
4216+ return self ._app_raw_tech_data ["mcu" ]["mainControllerUnitState" ]["name" ].replace ('_' , ' ' )
42024217
42034218 #######################################################################################################
42044219 # 'batteryInverter': {'state': {'name': 'RUN_GRID', 'severity': 'INFO'}, 'vendor': 'XXX',
@@ -4269,28 +4284,33 @@ def battery_temp_max(self) -> float:
42694284 def _battery_module_count (self ) -> int :
42704285 # internal use only...
42714286 if self ._app_raw_tech_data is not None and "batteryPack" in self ._app_raw_tech_data :
4272- return self ._app_raw_tech_data ["batteryPack" ]["numberOfBatteryModules" ]
4287+ if "numberOfBatteryModules" in self ._app_raw_tech_data ["batteryPack" ]:
4288+ return self ._app_raw_tech_data ["batteryPack" ]["numberOfBatteryModules" ]
42734289 return 0
42744290
42754291 @property
42764292 def battery_state_voltage (self ) -> float :
42774293 if self ._app_raw_tech_data is not None and "batteryPack" in self ._app_raw_tech_data :
4278- return self ._app_raw_tech_data ["batteryPack" ]["currentVoltageInV" ]
4294+ if "currentVoltageInV" in self ._app_raw_tech_data ["batteryPack" ]:
4295+ return self ._app_raw_tech_data ["batteryPack" ]["currentVoltageInV" ]
42794296
42804297 @property
42814298 def battery_state_current (self ) -> float :
42824299 if self ._app_raw_tech_data is not None and "batteryPack" in self ._app_raw_tech_data :
4283- return self ._app_raw_tech_data ["batteryPack" ]["currentCurrentInA" ]
4300+ if "currentCurrentInA" in self ._app_raw_tech_data ["batteryPack" ]:
4301+ return self ._app_raw_tech_data ["batteryPack" ]["currentCurrentInA" ]
42844302
42854303 @property
42864304 def _not_used_currentChargingLevelInPercent (self ) -> float :
42874305 if self ._app_raw_tech_data is not None and "batteryPack" in self ._app_raw_tech_data :
4288- return self ._app_raw_tech_data ["batteryPack" ]["currentChargingLevelInPercent" ]
4306+ if "currentChargingLevelInPercent" in self ._app_raw_tech_data ["batteryPack" ]:
4307+ return self ._app_raw_tech_data ["batteryPack" ]["currentChargingLevelInPercent" ]
42894308
42904309 @property
42914310 def battery_soh_remaining_capacity (self ) -> float :
42924311 if self ._app_raw_tech_data is not None and "batteryPack" in self ._app_raw_tech_data :
4293- return self ._app_raw_tech_data ["batteryPack" ]["remainingCapacityInPercent" ]
4312+ if "remainingCapacityInPercent" in self ._app_raw_tech_data ["batteryPack" ]:
4313+ return self ._app_raw_tech_data ["batteryPack" ]["remainingCapacityInPercent" ]
42944314
42954315 #######################################################################################################
42964316 # 'batteryModules': [{'ordinal': 1, 'state': {'state': 'OK', 'severity': 'INFO'}, 'vendor': 'XXX',
0 commit comments