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+ }
0 commit comments