Skip to content

Commit 5e7fe71

Browse files
committed
Service: write_holding_register/write_holding_registers allow selecting device
Service: add read_holding_register/read_holding_registers Log only first failed connection break as 'warning', susquence tries to connect only as 'debug'. Reduced Log-File flooding as inverters are usally down >= 12 hours a day
1 parent d473d59 commit 5e7fe71

File tree

6 files changed

+365
-94
lines changed

6 files changed

+365
-94
lines changed

custom_components/solarman/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"documentation": "https://github.com/StephanJoubert/home_assistant_solarman/blob/main/README.md",
88
"iot_class": "local_polling",
99
"issue_tracker": "https://github.com/StephanJoubert/home_assistant_solarman/issues",
10-
"requirements": ["pyyaml", "umodbus"],
10+
"requirements": ["pyyaml","pysolarmanv5"],
1111
"version": "1.0.0"
1212
}
1313

custom_components/solarman/pysolarmanv5_local.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,9 @@ def _v5_frame_def(self):
132132
self.v5_offsettime = bytes.fromhex("00000000")
133133
self.v5_checksum = bytes.fromhex("00") # placeholder value
134134
self.v5_end = bytes.fromhex("15")
135-
self.v5_header_len = 12 # v5_start(1) + v5_length(2) + v5_controlcode(2) + v5_serial(2) + v5_loggerserial(4) + v5_frametype(1)
135+
self.v5_header_len = 11 # v5_start(1) + v5_length(2) + v5_controlcode(2) + v5_serial(2) + v5_loggerserial(4)
136+
self.v5_frame_len_without_payload = self.v5_header_len + 2 #Header + v5_checksum(1) + v5_end(1)
137+
self.v5_payloadheader_len = 14 # frametype(1) + status(1) + totalWorkingTime(4) + powerOnTime(4) + offsetTime(4)
136138

137139
@staticmethod
138140
def _calculate_v5_frame_checksum(frame, length):
@@ -235,7 +237,6 @@ def _v5_frame_decoder(self, v5_frame):
235237
"""
236238

237239
modbus_frame = b""
238-
frame_len_without_payload_len = 13
239240
v5_start = int.from_bytes(self.v5_start, byteorder="big")
240241

241242
self.log.debug("_v5_frame_decoder: Check frame buffer len: %i", len(v5_frame))
@@ -273,27 +274,27 @@ def _v5_frame_decoder(self, v5_frame):
273274
continue
274275

275276
(payload_len,) = struct.unpack("<H", v5_frame[1:3])
276-
if frame_len < (frame_len_without_payload_len + payload_len + 3):
277-
self.log.debug("_v5_frame_decoder: V5 frame not complete.")
277+
if frame_len < (self.v5_frame_len_without_payload + payload_len):
278+
self.log.debug(f"_v5_frame_decoder: V5 frame not complete. frame length={frame_len} expected length={self.v5_frame_len_without_payload + payload_len}")
278279
return b"" #need to wait for more bytes to be received
279280

280-
if (v5_frame[frame_len_without_payload_len + payload_len - 1] != int.from_bytes(self.v5_end, byteorder="big")):
281+
if (v5_frame[self.v5_frame_len_without_payload + payload_len - 1] != int.from_bytes(self.v5_end, byteorder="big")):
281282
self.log.debug("_v5_frame_decoder: V5 frame contains invalid end value")
282283
v5_frame.pop()
283284
continue
284285

285-
if v5_frame[frame_len_without_payload_len + payload_len - 2] != self._calculate_v5_frame_checksum(v5_frame, frame_len_without_payload_len + payload_len):
286+
if v5_frame[self.v5_frame_len_without_payload + payload_len - 2] != self._calculate_v5_frame_checksum(v5_frame, self.v5_frame_len_without_payload + payload_len):
286287
self.log.debug("_v5_frame_decoder: V5 frame contains invalid V5 checksumd")
287288
v5_frame.pop()
288289
continue
289290

290-
if ((payload_len - frame_len_without_payload_len) <= 5):
291-
self.log.debug("_v5_frame_decoder: V5 frame no RTU frame (too short)")
292-
v5_frame.pop()
293-
continue
294-
295-
modbus_frame = v5_frame[25 : frame_len_without_payload_len + payload_len - 2]
296-
self.log.debug("_v5_frame_decoder: V5 frame found:" + modbus_frame.hex(" "))
291+
if ((payload_len-self.v5_payloadheader_len) < 5):
292+
self.log.debug("_v5_frame_decoder: V5 frame contains invalid RTU (to small)- > create RTU error frame")
293+
modbus_frame = b'\x01\x80\x02\xC0\x01'
294+
else:
295+
modbus_frame = v5_frame[self.v5_header_len + self.v5_payloadheader_len : self.v5_frame_len_without_payload + payload_len - 2]
296+
297+
self.log.debug("_v5_frame_decoder: V5 frame found (hex): " + modbus_frame.hex(" "))
297298
break
298299

299300
return modbus_frame

custom_components/solarman/sensor.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def _do_setup_platform(hass: HomeAssistant, config, async_add_entities : AddEnti
3838
inverter_sn = config.get(CONF_INVERTER_SERIAL)
3939
if inverter_sn == 0:
4040
inverter_sn = _inverter_scanner.get_serialno()
41-
41+
4242
inverter_mb_slaveid = config.get(CONF_INVERTER_MB_SLAVEID)
4343
if not inverter_mb_slaveid:
4444
inverter_mb_slaveid = DEFAULT_INVERTER_MB_SLAVEID
@@ -70,10 +70,10 @@ def _do_setup_platform(hass: HomeAssistant, config, async_add_entities : AddEnti
7070
_LOGGER.debug(hass_sensors)
7171

7272
async_add_entities(hass_sensors)
73-
# Register the services with home assistant.
74-
register_services (hass, inverter)
75-
76-
73+
# Register the services with home assistant.
74+
register_services (hass)
75+
76+
7777

7878

7979

@@ -156,6 +156,10 @@ def unique_id(self):
156156
def state(self):
157157
# Return the state of the sensor.
158158
return self.p_state
159+
160+
def inverter(self):
161+
# Return the inverter of the sensor. """
162+
return self.inverter
159163

160164
def update(self):
161165
self.p_state = getattr(self.inverter, self._field_name, None)
Lines changed: 150 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,190 @@
1-
from homeassistant.core import HomeAssistant
2-
from homeassistant.helpers import config_validation as cv
1+
from homeassistant.core import HomeAssistant, SupportsResponse
2+
from homeassistant.helpers import config_validation as cv, entity_registry, entity
3+
from homeassistant.helpers.entity_component import EntityComponent
4+
from homeassistant.exceptions import ServiceValidationError
35
import voluptuous as vol
46
from .const import *
57
from .solarman import Inverter
8+
import logging
69

10+
log = logging.getLogger(__name__)
711

8-
SERVICE_WRITE_REGISTER = 'write_holding_register'
9-
SERVICE_WRITE_MULTIPLE_REGISTERS = 'write_multiple_holding_registers'
12+
SERVICE_READ_HOLDING_REGISTER = 'read_holding_register'
13+
SERVICE_READ_MULTIPLE_HOLDING_REGISTERS = 'read_multiple_holding_registers'
14+
SERVICE_WRITE_HOLDING_REGISTER = 'write_holding_register'
15+
SERVICE_WRITE_MULTIPLE_HOLDING_REGISTERS = 'write_multiple_holding_registers'
16+
PARAM_DEVICE = 'device'
1017
PARAM_REGISTER = 'register'
11-
PARAM_VALUE = 'value'
12-
PARAM_VALUES = 'values'
13-
18+
PARAM_COUNT = 'count'
19+
PARAM_VALUE = 'value'
20+
PARAM_VALUES = 'values'
1421

1522

1623
# Register the services one can invoke on the inverter.
1724
# Apart from this, it also need to be defined in the file
1825
# services.yaml for the Home Assistant UI in "Developer Tools"
1926

27+
SERVICE_READ_REGISTER_SCHEMA = vol.Schema(
28+
{
29+
vol.Required(PARAM_DEVICE): vol.All(vol.Coerce(str)),
30+
vol.Required(PARAM_REGISTER): vol.All(vol.Coerce(int), vol.Range(min=0, max=65535))
31+
}
32+
)
33+
34+
SERVICE_READ_MULTIPLE_REGISTERS_SCHEMA = vol.Schema(
35+
{
36+
vol.Required(PARAM_DEVICE): vol.All(vol.Coerce(str)),
37+
vol.Required(PARAM_REGISTER): vol.All(vol.Coerce(int), vol.Range(min=0, max=65535)),
38+
vol.Required(PARAM_COUNT): vol.All(vol.Coerce(int), vol.Range(min=0, max=65535))
39+
}
40+
)
2041

2142
SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema(
2243
{
44+
vol.Required(PARAM_DEVICE): vol.All(vol.Coerce(str)),
2345
vol.Required(PARAM_REGISTER): vol.All(vol.Coerce(int), vol.Range(min=0, max=65535)),
2446
vol.Required(PARAM_VALUE): vol.All(vol.Coerce(int), vol.Range(min=0, max=65535)),
2547
}
2648
)
2749

2850
SERVICE_WRITE_MULTIPLE_REGISTERS_SCHEMA = vol.Schema(
2951
{
52+
vol.Required(PARAM_DEVICE): vol.All(vol.Coerce(str)),
3053
vol.Required(PARAM_REGISTER): vol.All(vol.Coerce(int), vol.Range(min=0, max=65535)),
3154
vol.Required(PARAM_VALUES): vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(min=0, max=65535))]),
3255
}
3356
)
3457

35-
def register_services (hass: HomeAssistant, inverter: Inverter ):
58+
def register_services (hass: HomeAssistant ):
59+
60+
def getInverter(device_id):
61+
inverter: Inverter | None
62+
entity_comp: EntityComponent[entity.Entity] | None
63+
registry = entity_registry.async_get(hass)
64+
entries = entity_registry.async_entries_for_device(registry, device_id)
65+
for entity_reg in entries:
66+
entity_id = entity_reg.entity_id
67+
domain = entity_id.partition(".")[0]
68+
entity_comp = hass.data.get("entity_components", {}).get(domain)
69+
if entity_comp is None:
70+
log.info(f'read_holding_register: Component for {entity_id} not loaded')
71+
continue
72+
73+
if (entity_obj := entity_comp.get_entity(entity_id)) is None:
74+
log.info(f'read_holding_register: Entity {entity_id} not found')
75+
continue
76+
77+
if (inverter := entity_obj.inverter) is None:
78+
log.info(f'read_holding_register: Entity {entity_id} has no inverter')
79+
continue
80+
81+
break
82+
83+
return inverter
84+
85+
86+
async def read_holding_register(call) -> int:
87+
if (inverter := getInverter(call.data.get(PARAM_DEVICE))) is None:
88+
raise ServiceValidationError(
89+
"No communication interface for device found",
90+
translation_domain=DOMAIN,
91+
translation_key="no_interface_found"
92+
)
93+
94+
try:
95+
response = inverter.service_read_holding_register( register=call.data.get(PARAM_REGISTER) )
96+
except Exception as e:
97+
raise ServiceValidationError(
98+
e,
99+
translation_domain=DOMAIN,
100+
translation_key="call_failed"
101+
)
102+
103+
result = {call.data.get(PARAM_REGISTER): response[0]}
104+
return result
105+
106+
async def read_multiple_holding_registers(call) -> int:
107+
if (inverter := getInverter(call.data.get(PARAM_DEVICE))) is None:
108+
raise ServiceValidationError(
109+
"No communication interface for device found",
110+
translation_domain=DOMAIN,
111+
translation_key="no_interface_found"
112+
)
113+
114+
try:
115+
response = inverter.service_read_multiple_holding_registers(
116+
register=call.data.get(PARAM_REGISTER),
117+
count=call.data.get(PARAM_COUNT) )
118+
except Exception as e:
119+
raise ServiceValidationError(
120+
e,
121+
translation_domain=DOMAIN,
122+
translation_key="call_failed"
123+
)
124+
125+
result = {}
126+
register=call.data.get(PARAM_REGISTER)
127+
for i in range(0,call.data.get(PARAM_COUNT)):
128+
result[register+i] = response[i]
129+
return result
36130

37131
async def write_holding_register(call) -> None:
38-
inverter.service_write_holding_register(
39-
register=call.data.get(PARAM_REGISTER),
40-
value=call.data.get(PARAM_VALUE))
132+
log.debug(f'write_holding_register: call={call}')
133+
if (inverter := getInverter(call.data.get(PARAM_DEVICE))) is None:
134+
raise ServiceValidationError(
135+
"No communication interface for device found",
136+
translation_domain=DOMAIN,
137+
translation_key="no_interface_found",
138+
)
139+
140+
try:
141+
inverter.service_write_holding_register(
142+
register=call.data.get(PARAM_REGISTER),
143+
value=call.data.get(PARAM_VALUE))
144+
except Exception as e:
145+
raise ServiceValidationError(
146+
e,
147+
translation_domain=DOMAIN,
148+
translation_key="call_failed"
149+
)
150+
41151
return
42152

43153
async def write_multiple_holding_registers(call) -> None:
44-
inverter.service_write_multiple_holding_registers(
45-
register=call.data.get(PARAM_REGISTER),
46-
values=call.data.get(PARAM_VALUES))
154+
log.debug(f'write_holding_register: call={call}')
155+
if (inverter := getInverter(call.data.get(PARAM_DEVICE))) is None:
156+
raise ServiceValidationError(
157+
"No communication interface for device found",
158+
translation_domain=DOMAIN,
159+
translation_key="no_interface_found",
160+
)
161+
162+
try:
163+
inverter.service_write_multiple_holding_registers(
164+
register=call.data.get(PARAM_REGISTER),
165+
values=call.data.get(PARAM_VALUES))
166+
except Exception as e:
167+
raise ServiceValidationError(
168+
e,
169+
translation_domain=DOMAIN,
170+
translation_key="call_failed"
171+
)
172+
47173
return
48174

49175
hass.services.async_register(
50-
DOMAIN, SERVICE_WRITE_REGISTER, write_holding_register, schema=SERVICE_WRITE_REGISTER_SCHEMA
176+
DOMAIN, SERVICE_READ_HOLDING_REGISTER, read_holding_register, schema=SERVICE_READ_REGISTER_SCHEMA, supports_response=SupportsResponse.OPTIONAL
177+
)
178+
179+
hass.services.async_register(
180+
DOMAIN, SERVICE_READ_MULTIPLE_HOLDING_REGISTERS, read_multiple_holding_registers, schema=SERVICE_READ_MULTIPLE_REGISTERS_SCHEMA, supports_response=SupportsResponse.OPTIONAL
181+
)
182+
183+
hass.services.async_register(
184+
DOMAIN, SERVICE_WRITE_HOLDING_REGISTER, write_holding_register, schema=SERVICE_WRITE_REGISTER_SCHEMA
51185
)
52186

53187
hass.services.async_register(
54-
DOMAIN, SERVICE_WRITE_MULTIPLE_REGISTERS, write_multiple_holding_registers, schema=SERVICE_WRITE_MULTIPLE_REGISTERS_SCHEMA
188+
DOMAIN, SERVICE_WRITE_MULTIPLE_HOLDING_REGISTERS, write_multiple_holding_registers, schema=SERVICE_WRITE_MULTIPLE_REGISTERS_SCHEMA
55189
)
56190
return

0 commit comments

Comments
 (0)