1616# See README.md for setup and usage instructions.
1717#
1818
19+ import logging
1920import re
2021from datetime import datetime
2122from typing import Any , Dict , List , Optional , Tuple
2930 UNIT_DEVICE_CLASS_MAP ,
3031)
3132
33+ _LOGGER = logging .getLogger (__name__ )
34+
35+ # --- Inverter System State Bit-Definitionen ---
36+ INV_BITS = [
37+ (0 , "Standby" ),
38+ (1 , "Grid-connected" ),
39+ (2 , "Grid-connected normally" ),
40+ (3 , "Grid derating (power rationing)" ),
41+ (4 , "Grid derating (internal cause)" ),
42+ (5 , "Normal stop" ),
43+ (6 , "Stop due to faults" ),
44+ (7 , "Stop due to power rationing" ),
45+ (8 , "Shutdown" ),
46+ (9 , "Spot check" ),
47+ ]
48+
49+ # Used to detect and parse the inverter system state string.
50+ INV_STATE_RE = re .compile (
51+ r"(?:State\s*)?Decimal\s*[:=]\s*(\d+)\s*Bits\s*[:=]\s*([01]{2,})" ,
52+ re .IGNORECASE | re .DOTALL ,
53+ )
54+
55+
56+ def expand_inverter_system_state (group : str , raw_text : str , timestamp_iso : Optional [str ]) -> List [Dict [str , Any ]]:
57+ """
58+ Zerlegt den langen 'System State'-String in mehrere, kurze Sensoren.
59+ Nutzt das ISO-Timestamp der Originalzeile.
60+ """
61+ out : List [Dict [str , Any ]] = []
62+ m = INV_STATE_RE .search (raw_text or "" )
63+ if not m :
64+ # Leave a compact version if regex not matched to keep sensor available.
65+ compact = (raw_text or "" )[:240 ]
66+ out .append ({
67+ "name" : friendly_name (group , "System state (compact)" ),
68+ "value" : compact ,
69+ "unit" : None ,
70+ "device_class" : None ,
71+ "enabled" : True ,
72+ "enpal_last_update" : timestamp_iso ,
73+ })
74+ _LOGGER .debug ("[Enpal] INV split: regex not matched, created compact sensor only (group=%s)" , group )
75+ return out
76+
77+ dec = m .group (1 )
78+ bitstr = m .group (2 ).strip ()
79+
80+ # Decimals as separate sensor
81+ out .append ({
82+ "name" : friendly_name (group , "System state decimal" ),
83+ "value" : dec ,
84+ "unit" : None ,
85+ "device_class" : None ,
86+ "enabled" : True ,
87+ "enpal_last_update" : timestamp_iso ,
88+ })
89+
90+ # Flags as summary sensor
91+ set_flags : List [str ] = []
92+ # LSB right: idx 0 = right border
93+ for idx , label in INV_BITS :
94+ active = (idx < len (bitstr )) and (bitstr [- (idx + 1 )] == "1" )
95+ if active :
96+ set_flags .append (label )
97+ summary = ", " .join (set_flags ) if set_flags else "None"
98+ out .append ({
99+ "name" : friendly_name (group , "System state flags" ),
100+ "value" : summary [:240 ],
101+ "unit" : None ,
102+ "device_class" : None ,
103+ "enabled" : True ,
104+ "enpal_last_update" : timestamp_iso ,
105+ })
106+
107+ # Ech individual flag as separate sensor
108+ for idx , label in INV_BITS :
109+ active = (idx < len (bitstr )) and (bitstr [- (idx + 1 )] == "1" )
110+ out .append ({
111+ "name" : friendly_name (group , f"System state: { label } " ),
112+ "value" : "on" if active else "off" ,
113+ "unit" : None ,
114+ "device_class" : None , # no binary_sensor here, just a regular sensor with on/off
115+ "enabled" : True ,
116+ "enpal_last_update" : timestamp_iso ,
117+ })
118+
119+ _LOGGER .debug ("[Enpal] INV split created %d sensors for group=%s" , len (out ), group )
120+ return out
32121
33122
34123def make_id (name : str ) -> str :
@@ -47,7 +136,7 @@ def friendly_name(group: str, sensor: str) -> str:
47136 """Format a friendly sensor name with group context."""
48137 group_lower = group .lower ()
49138 parts = sensor .split ('.' )
50- label = []
139+ label : List [ str ] = []
51140 skip_next = False
52141
53142 for i , part in enumerate (parts ):
@@ -129,7 +218,7 @@ def parse_enpal_html_sensors(
129218) -> List [Dict [str , Any ]]:
130219 """parsing the html content and extracting sensor data."""
131220 soup = BeautifulSoup (html , 'html.parser' )
132- sensors = []
221+ sensors : List [ Dict [ str , Any ]] = []
133222
134223 for card in soup .find_all ("div" , class_ = "card" ):
135224 if not isinstance (card , Tag ):
@@ -153,7 +242,7 @@ def extract_group_from_card(card: Tag) -> Optional[str]:
153242def parse_card_rows (card : Tag , group : str , groups : List [str ]) -> List [Dict [str , Any ]]:
154243 """Extracts sensors from a group."""
155244 rows = card .find_all ("tr" )[1 :] # assume first row == header
156- sensor_list = []
245+ sensor_list : List [ Dict [ str , Any ]] = []
157246
158247 for row in rows :
159248 if not isinstance (row , Tag ):
@@ -171,7 +260,7 @@ def parse_card_rows(card: Tag, group: str, groups: List[str]) -> List[Dict[str,
171260 value_clean , unit = normalize_value_and_unit (value_raw , unit , device_class , DEFAULT_UNITS )
172261 timestamp_iso = parse_timestamp (timestamp_str )
173262
174- sensor = {
263+ sensor : Dict [ str , Any ] = {
175264 "name" : friendly_name (group , raw_name ),
176265 "value" : value_clean ,
177266 "unit" : unit ,
@@ -184,6 +273,36 @@ def parse_card_rows(card: Tag, group: str, groups: List[str]) -> List[Dict[str,
184273 if sensor_id in DEVICE_CLASS_OVERRIDES :
185274 sensor ["device_class" ] = DEVICE_CLASS_OVERRIDES [sensor_id ]
186275
276+ # Trigger if the raw value matches the bit pattern (Regex) OR
277+ # if it's very long and contains "Bits". Works independent of sensor name/ID.
278+ should_expand = False
279+ try :
280+ if isinstance (value_raw , str ):
281+ if INV_STATE_RE .search (value_raw ):
282+ should_expand = True
283+ elif len (value_raw ) > 200 and "Bits" in value_raw :
284+ should_expand = True
285+ except Exception as ex :
286+ _LOGGER .debug ("[Enpal] INV expand check failed: %s" , ex )
287+
288+ if should_expand :
289+ expanded = expand_inverter_system_state (group , value_raw , timestamp_iso )
290+
291+ # Keep the original sensor: truncate its value so it's valid and retains its unique_id for compatibility.
292+ sensor ["value" ] = (value_raw or "" )[:240 ]
293+ sensor_list .append (sensor )
294+
295+ # Add the new split sensors (if any)
296+ if expanded :
297+ sensor_list .extend (expanded )
298+ _LOGGER .info (
299+ "[Enpal] Expanded inverter state into %d sensors (group=%s, base=%s)" ,
300+ len (expanded ), group , raw_name
301+ )
302+ else :
303+ _LOGGER .debug ("[Enpal] INV expand matched, but produced no extra sensors (group=%s)" , group )
304+ continue
305+
187306 sensor_list .append (sensor )
188307
189308 return sensor_list
0 commit comments