Skip to content

Commit 8eb48e7

Browse files
committed
Added split to show each "bit" of the sensor as a sperate entity.
1 parent af77cd4 commit 8eb48e7

2 files changed

Lines changed: 137 additions & 4 deletions

File tree

custom_components/enpal_webparser/const.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,5 +224,19 @@
224224
"voltage_wallbox_connector_1_phase_a": "mdi:transmission-tower",
225225
"voltage_wallbox_connector_1_phase_b": "mdi:transmission-tower",
226226
"voltage_wallbox_connector_1_phase_c": "mdi:transmission-tower",
227+
228+
# Inverter System State bits
229+
"inverter_system_state_decimal": "mdi:numeric",
230+
"inverter_system_state_flags": "mdi:state-machine",
231+
"inverter_system_state_standby": "mdi:pause-circle",
232+
"inverter_system_state_grid_connected": "mdi:transmission-tower",
233+
"inverter_system_state_grid_connected_normally": "mdi:check-circle",
234+
"inverter_system_state_grid_derating_power_rationing": "mdi:gauge-low",
235+
"inverter_system_state_grid_derating_internal_cause": "mdi:gauge-low",
236+
"inverter_system_state_normal_stop": "mdi:stop-circle",
237+
"inverter_system_state_stop_due_to_faults": "mdi:alert-circle",
238+
"inverter_system_state_stop_due_to_power_rationing": "mdi:flash-off",
239+
"inverter_system_state_shutdown": "mdi:power",
240+
"inverter_system_state_spot_check": "mdi:magnify",
227241
}
228242

custom_components/enpal_webparser/utils.py

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# See README.md for setup and usage instructions.
1717
#
1818

19+
import logging
1920
import re
2021
from datetime import datetime
2122
from typing import Any, Dict, List, Optional, Tuple
@@ -29,6 +30,94 @@
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

34123
def 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]:
153242
def 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

Comments
 (0)