Skip to content

Commit 2d30cce

Browse files
authored
2025.8.1 (#24)
### Added - New Min Cell Voltage sensor (register 35043) to monitor minimum battery cell voltage - New Max Cell Voltage sensor (register 36943) to monitor maximum battery cell voltage - New Reset Device button (register 41000) to allow resetting the battery management system via Home Assistant ### Changed - Clean up code and improve overall code quality ### Changed - Clean up code and improve overall code quality
2 parents da413ee + ab4af42 commit 2d30cce

13 files changed

Lines changed: 564 additions & 311 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Changelog
22

3-
# Changelog
3+
## [2025.8.1] - 2025-08-12
4+
5+
### Added
6+
- New Min Cell Voltage sensor (register 35043) to monitor minimum battery cell voltage
7+
- New Max Cell Voltage sensor (register 36943) to monitor maximum battery cell voltage
8+
- New Reset Device button (register 41000) to allow resetting the battery management system via Home Assistant
49

510
## [2025.8.0] - 2025-08-09
611

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ This is a custom HACS-compatible integration for the Marstek Venus E home batter
2626
- Select entity for control modes (e.g., force mode, grid standard)
2727
- Backup mode control and charge/discharge to SOC included
2828
- Includes calculated sensors: round-trip efficiency (total/monthly) and stored energy
29-
<!-- - Efficient background polling with per-sensor scan intervals -->
29+
- Reset button to allow manual reset of the battery management system via Home Assistant
3030
- Some advanced sensors are disabled by default to keep the UI clean
3131
- UI-based configuration (Config Flow)
3232
- Fully local, no cloud required
@@ -56,7 +56,7 @@ Dit is een aangepaste HACS-integratie voor de Marstek Venus E thuisbatterij via
5656
- Select-entiteiten voor bedieningsmodi (bijv. force mode, netstandaard)
5757
- Back-up modus besturing en laden/ontladen tot SOC inbegrepen
5858
- Inclusief berekende sensoren: round-trip rendement (totaal/maandelijks) en opgeslagen energie
59-
<!-- - Efficiënte achtergrondpolling met per-sensor scan-intervallen -->
59+
- Resetknop om handmatig het batterijbeheersysteem te resetten via Home Assistant
6060
- Sommige geavanceerde sensoren standaard uitgeschakeld voor een schone gebruikersinterface
6161
- Configuratie via UI (Config Flow)
6262
- Volledig lokaal, geen cloud nodig
@@ -115,6 +115,8 @@ The following Modbus registers are used by this integration:
115115
| 35010 | Max Cell Temperature | int16 | 2 | 1 | °c | sensor | Maximum cell temperature |
116116
| 35011 | Min Cell Temperature | int16 | 2 | 1 | °c | sensor | Minimum cell temperature |
117117
| 35100 | Inverter State | uint16 | 2 | 1 | - | sensor | Inverter state (0=Sleep, 1=Standby, 2=Charge, 3=Discharge, 4=Backup Mode, 5=OTA Upgrade) |
118+
| 37007 | Max Cell Voltage | uint16 | 2 | 0.001 | V | sensor | Maximum cell voltage across battery cells |
119+
| 37008 | Min Cell Voltage | uint16 | 2 | 0.001 | V | sensor | Minimum cell voltage across battery cells |
118120
| 36000 | Alarm Status | uint16 | 4 | - | - | sensor | Alarm status bits (see bit descriptions) |
119121
| 36100 | Fault Status | uint16 | 8 | - | - | sensor | Fault status bits (64 bits total over 4 registers) |
120122
| 41010 | Discharge Limit | uint16 | 2 | 1 | - | sensor | Discharge limit mode switch (0=High (2500 W), 1=Low (800 W))|

custom_components/marstek_modbus/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"select",
2222
"button",
2323
"number",
24+
"binary_sensor",
25+
# "update",
2426
] # List of supported platforms for this integration
2527

2628

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""
2+
Module for creating binary sensor entities for Marstek Venus battery devices.
3+
Binary sensors read Modbus registers asynchronously via the coordinator.
4+
"""
5+
6+
import logging
7+
8+
from homeassistant.components.binary_sensor import BinarySensorEntity
9+
from homeassistant.config_entries import ConfigEntry
10+
from homeassistant.core import HomeAssistant
11+
from homeassistant.helpers.entity import Entity, EntityCategory
12+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
13+
14+
from .coordinator import MarstekCoordinator
15+
from .const import DOMAIN, MANUFACTURER, MODEL, BINARY_SENSOR_DEFINITIONS
16+
17+
_LOGGER = logging.getLogger(__name__)
18+
19+
20+
def get_entity_type(entity) -> str:
21+
"""
22+
Determine the entity type based on its class inheritance.
23+
24+
Args:
25+
entity: The entity instance.
26+
27+
Returns:
28+
A lowercase string representing the entity type
29+
(e.g., 'switch', 'sensor', 'binary_sensor').
30+
"""
31+
for base in entity.__class__.__mro__:
32+
if issubclass(base, Entity) and base.__name__.endswith("Entity"):
33+
return base.__name__.replace("Entity", "").lower()
34+
return "entity"
35+
36+
37+
async def async_setup_entry(
38+
hass: HomeAssistant,
39+
entry: ConfigEntry,
40+
async_add_entities: AddEntitiesCallback,
41+
):
42+
"""
43+
Set up binary sensor entities when the config entry is loaded.
44+
45+
This function retrieves the coordinator from hass.data,
46+
creates binary sensor entities based on BINARY_SENSOR_DEFINITIONS,
47+
and registers them with Home Assistant.
48+
49+
Args:
50+
hass: Home Assistant instance.
51+
entry: Configuration entry.
52+
async_add_entities: Callback to add entities.
53+
"""
54+
coordinator = hass.data[DOMAIN][entry.entry_id]
55+
56+
await coordinator.async_config_entry_first_refresh()
57+
58+
entities = []
59+
60+
for definition in BINARY_SENSOR_DEFINITIONS:
61+
entities.append(MarstekBinarySensor(coordinator, definition))
62+
63+
async_add_entities(entities)
64+
65+
66+
class MarstekBinarySensor(BinarySensorEntity):
67+
"""
68+
Representation of a Modbus binary sensor entity for Marstek Venus.
69+
70+
Sensor state is read asynchronously via
71+
the coordinator communicating with the Modbus device.
72+
"""
73+
74+
def __init__(self, coordinator: MarstekCoordinator, definition: dict):
75+
"""
76+
Initialize the binary sensor entity.
77+
78+
Args:
79+
coordinator: The data update coordinator instance.
80+
definition: Dictionary containing sensor configuration.
81+
"""
82+
self.coordinator = coordinator
83+
self.definition = definition
84+
85+
# Set entity attributes from definition
86+
self._attr_name = f"{self.definition['name']}"
87+
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.definition['key']}"
88+
self._attr_has_entity_name = True
89+
90+
# Set optional attributes if provided in definition
91+
self._state = None
92+
self._key = definition["key"]
93+
self._register = definition["register"]
94+
95+
# set category if defined in the definition
96+
if "category" in self.definition:
97+
self._attr_entity_category = EntityCategory(self.definition.get("category"))
98+
99+
# Set icon if defined in the button definition
100+
if "icon" in self.definition:
101+
self._attr_icon = self.definition.get("icon")
102+
103+
# Optional: disable entity by default if specified in the definition
104+
if definition.get("enabled_by_default") is False:
105+
self._attr_entity_registry_enabled_default = False
106+
107+
async def async_added_to_hass(self):
108+
"""Handle entity added to Home Assistant by fetching initial state."""
109+
await self.async_update()
110+
self.async_write_ha_state()
111+
112+
@property
113+
def available(self) -> bool:
114+
"""Return True if coordinator update succeeded and state is known."""
115+
return self.coordinator.last_update_success and (self._state is not None)
116+
117+
@property
118+
def is_on(self) -> bool | None:
119+
"""Return True if binary sensor is on, False if off, None if unknown."""
120+
return self._state
121+
122+
async def async_update(self):
123+
"""
124+
Fetch the latest binary sensor state from the coordinator's Modbus client.
125+
126+
Reads the configured register asynchronously and updates internal state.
127+
"""
128+
data_type = self.definition.get("data_type", "uint16")
129+
register = self._register
130+
count = self.definition.get("count", 1)
131+
132+
try:
133+
value = await self.coordinator.client.async_read_register(
134+
register=register,
135+
data_type=data_type,
136+
count=count,
137+
sensor_key=self._key,
138+
)
139+
except Exception as e:
140+
_LOGGER.error("Error reading register 0x%X: %s", register, e)
141+
self._state = None
142+
return
143+
144+
if value is not None:
145+
if value == 1:
146+
self._state = True
147+
elif value == 0:
148+
self._state = False
149+
else:
150+
_LOGGER.warning(
151+
"Unknown register value %s for binary sensor %s", value, self._attr_name
152+
)
153+
self._state = None
154+
else:
155+
self._state = None
156+
157+
await self.coordinator.async_update_value(
158+
self._key,
159+
self._state,
160+
register=register,
161+
scale=self.definition.get("scale"),
162+
unit=self.definition.get("unit"),
163+
entity_type=get_entity_type(self),
164+
)
165+
166+
@property
167+
def device_info(self) -> dict:
168+
"""Return device info for device registry grouping."""
169+
return {
170+
"identifiers": {(DOMAIN, self.coordinator.config_entry.entry_id)},
171+
"name": self.coordinator.config_entry.title,
172+
"manufacturer": MANUFACTURER,
173+
"model": MODEL,
174+
"entry_type": "service",
175+
}

custom_components/marstek_modbus/button.py

Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,64 +3,117 @@
33
via Modbus register writes.
44
"""
55

6-
# Import necessary components and modules
6+
import logging
7+
78
from homeassistant.components.button import ButtonEntity
89
from homeassistant.config_entries import ConfigEntry
910
from homeassistant.core import HomeAssistant
1011
from homeassistant.helpers.entity_platform import AddEntitiesCallback
11-
from .const import BUTTON_DEFINITIONS, DOMAIN, MANUFACTURER, MODEL
12+
from homeassistant.helpers.entity import EntityCategory
13+
1214
from .coordinator import MarstekCoordinator
15+
from .const import BUTTON_DEFINITIONS, DOMAIN, MANUFACTURER, MODEL
1316

14-
# Set up logging for debugging purposes
15-
import logging
1617
_LOGGER = logging.getLogger(__name__)
1718

19+
20+
async def async_setup_entry(
21+
hass: HomeAssistant,
22+
entry: ConfigEntry,
23+
async_add_entities: AddEntitiesCallback,
24+
):
25+
"""
26+
Set up the MarstekButton entities using the provided config entry.
27+
28+
Retrieves the coordinator and creates button entities
29+
from the button definitions, then adds them to Home Assistant.
30+
"""
31+
# Retrieve coordinator instance for this config entry
32+
coordinator = hass.data[DOMAIN][entry.entry_id]
33+
34+
# Ensure coordinator has latest data before creating entities
35+
await coordinator.async_config_entry_first_refresh()
36+
37+
# Create button entities for all button definitions
38+
entities = [MarstekButton(coordinator, definition) for definition in BUTTON_DEFINITIONS]
39+
40+
# Register all button entities with Home Assistant
41+
async_add_entities(entities)
42+
43+
1844
class MarstekButton(ButtonEntity):
1945
"""ButtonEntity to trigger actions on the Marstek Venus battery."""
2046

2147
def __init__(self, coordinator: MarstekCoordinator, definition: dict):
2248
"""
2349
Initialize the button entity with the coordinator and set attributes.
24-
This includes the name, unique ID, and register information.
50+
51+
Args:
52+
coordinator: Data update coordinator instance.
53+
definition: Dictionary with button configuration.
2554
"""
2655
self.coordinator = coordinator
2756
self.definition = definition
28-
self._attr_name = f"{self.definition['name']}"
57+
58+
# Set entity name and unique ID
59+
self._attr_name = self.definition["name"]
2960
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.definition['key']}"
3061
self._attr_has_entity_name = True
62+
63+
# Register and command value to write on button press
3164
self._register = self.definition["register"]
32-
self._value = self.definition.get("value", 1) # Default value to write on press
65+
self._command = self.definition.get("command", 1) # Default command is 1
66+
67+
# Set entity category if defined in the definition
68+
if "category" in self.definition:
69+
try:
70+
self._attr_entity_category = EntityCategory(self.definition["category"])
71+
except ValueError:
72+
_LOGGER.warning(
73+
"Unknown entity category %s for button %s",
74+
self.definition["category"],
75+
self._attr_name,
76+
)
3377

34-
# Optional: disable entity by default if specified in the button definition
78+
# Set icon if defined in the button definition
79+
if "icon" in self.definition:
80+
self._attr_icon = self.definition["icon"]
81+
82+
# Disable entity by default if specified in the definition
3583
if self.definition.get("enabled_by_default") is False:
3684
self._attr_entity_registry_enabled_default = False
3785

3886
async def async_press(self) -> None:
39-
"""Handle button press by writing the specified value to the Modbus register."""
40-
success = self.coordinator.client.write_register(self._register, self._value)
87+
"""
88+
Handle button press by writing the specified value to the Modbus register.
89+
"""
90+
success = await self.coordinator.client.async_write_register(
91+
self._register, self._command
92+
)
4193
if success:
42-
_LOGGER.debug("Successfully wrote value %s to register %s on button press", self._value, self._register)
94+
_LOGGER.debug(
95+
"Successfully wrote value %s to register %s on button press",
96+
self._command,
97+
self._register,
98+
)
4399
else:
44-
_LOGGER.warning("Failed to write value %s to register %s on button press", self._value, self._register)
100+
_LOGGER.warning(
101+
"Failed to write value %s to register %s on button press",
102+
self._command,
103+
self._register,
104+
)
45105

46106
@property
47107
def device_info(self):
48-
"""Return device information to associate entities with a device in the UI.
108+
"""
109+
Return device information to associate entities with a device in the UI.
49110
50-
This enables the "Rename associated entities?" dialog when the user renames the integration instance.
51-
It also groups all entities under one device in the Home Assistant device registry.
111+
This enables grouping of entities and supports the "Rename associated entities?" dialog.
52112
"""
53113
return {
54114
"identifiers": {(DOMAIN, self.coordinator.config_entry.entry_id)},
55115
"name": self.coordinator.config_entry.title,
56116
"manufacturer": MANUFACTURER,
57117
"model": MODEL,
58-
"entry_type": "service"
59-
}
60-
61-
# Setup function to add the button entities to Home Assistant
62-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
63-
"""Set up the MarstekButton entities using the provided config entry."""
64-
coordinator = MarstekCoordinator(hass, entry)
65-
entities = [MarstekButton(coordinator, definition) for definition in BUTTON_DEFINITIONS]
66-
async_add_entities(entities)
118+
"entry_type": "service",
119+
}

0 commit comments

Comments
 (0)