1414from .const import DEFAULT_SCAN_INTERVALS , SUPPORTED_VERSIONS , DEFAULT_UNIT_ID
1515
1616from .helpers .modbus_client import MarstekModbusClient
17+ from pathlib import Path
1718
1819_LOGGER = logging .getLogger (__name__ )
1920
@@ -110,6 +111,10 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
110111 self ._connection_suspended = False
111112 self ._suspension_reset_time = None
112113
114+ self ._consecutive_timeout_cycles = 0
115+ self ._max_consecutive_timeout_cycles = 3
116+ self ._timeout_ratio_reconnect_threshold = 0.5
117+
113118 # Connection health tracking for diagnostics
114119 self ._last_successful_read = None
115120 self ._connection_established_at = None
@@ -215,8 +220,14 @@ async def async_init(self):
215220 _LOGGER .info ("Successfully connected to Modbus device at %s:%d" , self .host , self .port )
216221 return connected
217222
218- async def async_read_value (self , sensor : dict , key : str ):
219- """Helper to read a single sensor value from Modbus with logging and type checking."""
223+ async def async_read_value (self , sensor : dict , key : str , track_failure : bool = True ):
224+ """Helper to read a single sensor value from Modbus with logging and type checking.
225+
226+ Args:
227+ sensor: sensor definition dict
228+ key: the sensor key
229+ track_failure: if False, timeouts will not count towards timeout metrics
230+ """
220231 entity_type = self ._entity_types .get (key , get_entity_type (sensor ))
221232
222233 # Determine scale and unit
@@ -260,9 +271,11 @@ async def async_read_value(self, sensor: dict, key: str):
260271 return None
261272
262273 except asyncio .TimeoutError :
274+ if track_failure :
275+ self ._timeouts_in_cycle = getattr (self , "_timeouts_in_cycle" , 0 ) + 1
263276 _LOGGER .warning (
264- "Timeout reading %s '%s' at register %d - connection may be slow or incorrect" ,
265- entity_type , key , sensor ["register" ]
277+ "Timeout reading %s '%s' at register %d from %s:%d - connection may be slow or incorrect" ,
278+ entity_type , key , sensor ["register" ], self . client . host , self . client . port
266279 )
267280 return None
268281 except Exception as e :
@@ -348,6 +361,7 @@ async def _async_update_data(self):
348361 # Track if we actually attempted any reads (not just skipped due to intervals)
349362 attempted_reads = 0
350363 successful_reads = 0
364+ self ._timeouts_in_cycle = 0
351365
352366 # Connection throttling: if too many failures, temporarily stop attempting connections
353367 if self ._connection_suspended :
@@ -358,14 +372,7 @@ async def _async_update_data(self):
358372
359373 # Force reconnect after suspension
360374 try :
361- _LOGGER .debug ("Closing existing connection before reconnect" )
362- await self .client .async_close ()
363- except Exception as exc :
364- _LOGGER .debug ("Error closing client during reconnect: %s" , exc )
365-
366- try :
367- _LOGGER .info ("Attempting to reconnect to %s:%d" , self .host , self .port )
368- connected = await self .client .async_connect ()
375+ connected = await self .client .async_reconnect ()
369376 if connected :
370377 _LOGGER .info ("Successfully reconnected after suspension" )
371378 else :
@@ -464,6 +471,7 @@ async def _async_update_data(self):
464471
465472 # Connection retry logic: only track failures if we actually attempted reads
466473 if attempted_reads > 0 :
474+ timeout_reads = int (getattr (self , "_timeouts_in_cycle" , 0 ) or 0 )
467475 if successful_reads > 0 :
468476 # At least some data successfully retrieved - reset failure counter
469477 if self ._consecutive_failures > 0 :
@@ -472,18 +480,46 @@ async def _async_update_data(self):
472480 self ._consecutive_failures = 0
473481 self ._connection_suspended = False
474482 self ._last_successful_read = now
483+
484+ if timeout_reads and (timeout_reads / attempted_reads ) >= self ._timeout_ratio_reconnect_threshold :
485+ self ._consecutive_timeout_cycles += 1
486+ _LOGGER .warning (
487+ "High timeout rate detected (%d/%d) - consecutive timeout cycles: %d/%d" ,
488+ timeout_reads ,
489+ attempted_reads ,
490+ self ._consecutive_timeout_cycles ,
491+ self ._max_consecutive_timeout_cycles ,
492+ )
493+ else :
494+ self ._consecutive_timeout_cycles = 0
495+
496+ if self ._consecutive_timeout_cycles >= self ._max_consecutive_timeout_cycles :
497+ try :
498+ _LOGGER .info (
499+ "Attempting reconnect due to repeated timeouts (%d/%d cycles)" ,
500+ self ._consecutive_timeout_cycles ,
501+ self ._max_consecutive_timeout_cycles ,
502+ )
503+ connected = await self .client .async_reconnect ()
504+ if connected :
505+ _LOGGER .info ("Successfully reconnected after repeated timeouts" )
506+ self ._consecutive_timeout_cycles = 0
507+ self ._connection_established_at = now
508+ else :
509+ _LOGGER .warning ("Reconnect attempt after repeated timeouts failed" )
510+ except Exception as exc :
511+ _LOGGER .error ("Exception during reconnect after repeated timeouts: %s" , exc )
475512 elif successful_reads == 0 :
476513 # We attempted reads but ALL failed - connection issue
477514 self ._consecutive_failures += 1
478515 _LOGGER .warning ("All read attempts failed (%d/%d) - consecutive failures: %d/%d" ,
479516 successful_reads , attempted_reads ,
480517 self ._consecutive_failures , self ._max_consecutive_failures )
481518
482- # Try to reconnect immediately on failure
519+ # Try to reconnect immediately on failure (use reconnect helper)
483520 try :
484521 _LOGGER .info ("Attempting immediate reconnection after read failures" )
485- await self .client .async_close ()
486- connected = await self .client .async_connect ()
522+ connected = await self .client .async_reconnect ()
487523 if connected :
488524 _LOGGER .info ("Successfully reconnected" )
489525 self ._consecutive_failures = 0
@@ -502,6 +538,7 @@ async def _async_update_data(self):
502538 "Will retry in 5 minutes to prevent resource exhaustion." ,
503539 self ._consecutive_failures
504540 )
541+ self ._consecutive_timeout_cycles = 0
505542 else :
506543 _LOGGER .debug ("No sensors due for update in this cycle" )
507544
@@ -567,22 +604,90 @@ def get_registers(version: str):
567604 % (version_raw , ", " .join (sorted (allowed )))
568605 )
569606
570- # Map the validated version token to the correct registers module.
571- # Support the new tokens 'e v1/v2' and 'e v3'.
607+ def _normalize_section (section ):
608+ """Convert mapping-based sections into the legacy list-of-dicts format."""
609+ if isinstance (section , dict ):
610+ normalized = []
611+ for key , value in section .items ():
612+ entry = dict (value or {})
613+ entry .setdefault ("key" , key )
614+ normalized .append (entry )
615+ return normalized
616+ if isinstance (section , list ):
617+ return section
618+ return []
619+
620+ # Prefer YAML-based register definitions placed in the `registers/` folder.
621+ # Map version tokens to YAML filenames.
622+ filename_map = {
623+ "e v1/v2" : "e_v12.yaml" ,
624+ "e v3" : "e_v3.yaml" ,
625+ "d" : "d.yaml" ,
626+ "a" : "a.yaml" ,
627+ }
628+
629+ yaml_filename = filename_map .get (version )
630+ if yaml_filename :
631+ yaml_path = Path (__file__ ).parent / "registers" / yaml_filename
632+ if yaml_path .exists ():
633+ try :
634+ import yaml
635+
636+ with open (yaml_path , "r" , encoding = "utf-8" ) as fh :
637+ data = yaml .safe_load (fh ) or {}
638+
639+ return {
640+ "SENSOR_DEFINITIONS" : _normalize_section (data .get ("SENSOR_DEFINITIONS" )),
641+ "BINARY_SENSOR_DEFINITIONS" : _normalize_section (data .get ("BINARY_SENSOR_DEFINITIONS" )),
642+ "SELECT_DEFINITIONS" : _normalize_section (data .get ("SELECT_DEFINITIONS" )),
643+ "SWITCH_DEFINITIONS" : _normalize_section (data .get ("SWITCH_DEFINITIONS" )),
644+ "NUMBER_DEFINITIONS" : _normalize_section (data .get ("NUMBER_DEFINITIONS" )),
645+ "BUTTON_DEFINITIONS" : _normalize_section (data .get ("BUTTON_DEFINITIONS" )),
646+ "EFFICIENCY_SENSOR_DEFINITIONS" : _normalize_section (
647+ data .get ("EFFICIENCY_SENSOR_DEFINITIONS" )
648+ ),
649+ "STORED_ENERGY_SENSOR_DEFINITIONS" : _normalize_section (
650+ data .get ("STORED_ENERGY_SENSOR_DEFINITIONS" )
651+ ),
652+ }
653+ except Exception as e :
654+ _LOGGER .warning ("Failed to load YAML registers %s: %s" , yaml_path , e )
655+
656+ # Fall back to legacy Python modules if YAML not present or failed to load
572657 if version == "e v1/v2" :
573658 from . import registers_v12 as registers
574659 elif version == "e v3" :
575660 from . import registers_v3 as registers
576661 elif version == "d" :
577- from . import registers_v12 as registers
578-
662+ from . import registers_d as registers
663+ elif version == "a" :
664+ # No legacy Python module for A exists; return empty definitions as fallback
665+ registers = None
666+
667+ if registers :
668+ return {
669+ "SENSOR_DEFINITIONS" : getattr (registers , "SENSOR_DEFINITIONS" , []),
670+ "BINARY_SENSOR_DEFINITIONS" : getattr (registers , "BINARY_SENSOR_DEFINITIONS" , []),
671+ "SELECT_DEFINITIONS" : getattr (registers , "SELECT_DEFINITIONS" , []),
672+ "SWITCH_DEFINITIONS" : getattr (registers , "SWITCH_DEFINITIONS" , []),
673+ "NUMBER_DEFINITIONS" : getattr (registers , "NUMBER_DEFINITIONS" , []),
674+ "BUTTON_DEFINITIONS" : getattr (registers , "BUTTON_DEFINITIONS" , []),
675+ "EFFICIENCY_SENSOR_DEFINITIONS" : getattr (
676+ registers , "EFFICIENCY_SENSOR_DEFINITIONS" , []
677+ ),
678+ "STORED_ENERGY_SENSOR_DEFINITIONS" : getattr (
679+ registers , "STORED_ENERGY_SENSOR_DEFINITIONS" , []
680+ ),
681+ }
682+
683+ # Default empty return if nothing found
579684 return {
580- "SENSOR_DEFINITIONS" : getattr ( registers , "SENSOR_DEFINITIONS" , []) ,
581- "BINARY_SENSOR_DEFINITIONS" : getattr ( registers , "BINARY_SENSOR_DEFINITIONS" , []) ,
582- "SELECT_DEFINITIONS" : getattr ( registers , "SELECT_DEFINITIONS" , []) ,
583- "SWITCH_DEFINITIONS" : getattr ( registers , "SWITCH_DEFINITIONS" , []) ,
584- "NUMBER_DEFINITIONS" : getattr ( registers , "NUMBER_DEFINITIONS" , []) ,
585- "BUTTON_DEFINITIONS" : getattr ( registers , "BUTTON_DEFINITIONS" , []) ,
586- "EFFICIENCY_SENSOR_DEFINITIONS" : getattr ( registers , "EFFICIENCY_SENSOR_DEFINITIONS" , []) ,
587- "STORED_ENERGY_SENSOR_DEFINITIONS" : getattr ( registers , "STORED_ENERGY_SENSOR_DEFINITIONS" , []) ,
685+ "SENSOR_DEFINITIONS" : [] ,
686+ "BINARY_SENSOR_DEFINITIONS" : [] ,
687+ "SELECT_DEFINITIONS" : [] ,
688+ "SWITCH_DEFINITIONS" : [] ,
689+ "NUMBER_DEFINITIONS" : [] ,
690+ "BUTTON_DEFINITIONS" : [] ,
691+ "EFFICIENCY_SENSOR_DEFINITIONS" : [] ,
692+ "STORED_ENERGY_SENSOR_DEFINITIONS" : [] ,
588693 }
0 commit comments