Skip to content

Commit 212a285

Browse files
committed
add write support and validation
needs more work and testing, but works for basic values. less than 1 register in size.
1 parent 18c05c9 commit 212a285

6 files changed

+344
-94
lines changed

inverter.py

+85-13
Original file line numberDiff line numberDiff line change
@@ -115,19 +115,81 @@ def print_info(self):
115115
self.__log.info('\tUnit: %s\n', str(self.unit))
116116
self.__log.info('\tModbus Version: %s\n', str(self.modbus_version))
117117

118-
def read_variable(self, variable_name : str, registry_type : Registry_Type):
118+
def write_variable(self, entry : registry_map_entry, value : str, registry_type : Registry_Type = Registry_Type.HOLDING):
119+
""" writes a value to a ModBus register; todo: registry_type to handle other write functions"""
120+
121+
#read current value
122+
current_registers = self.read_registers(start=entry.register, end=entry.register, registry_type=registry_type)
123+
results = self.process_registery(current_registers, self.protocolSettings.get_registry_map(registry_type))
124+
current_value = current_registers[entry.register]
125+
126+
127+
if not self.protocolSettings.validate_registry_entry(entry, current_value):
128+
raise ValueError("Invalid value in register. unsafe to write")
129+
130+
if not self.protocolSettings.validate_registry_entry(entry, value):
131+
raise ValueError("Invalid new value. unsafe to write")
132+
133+
#handle codes
134+
if entry.variable_name+"_codes" in self.protocolSettings.codes:
135+
codes = self.protocolSettings.codes[entry.variable_name+"_codes"]
136+
for key, val in codes.items():
137+
if val == value: #convert "string" to key value
138+
value = key
139+
break
140+
141+
#results[entry.variable_name]
142+
ushortValue : int = None #ushort
143+
if entry.data_type == Data_Type.USHORT:
144+
ushortValue = int(value)
145+
if ushortValue < 0 or ushortValue > 65535:
146+
raise ValueError("Invalid value")
147+
elif entry.data_type.value > 200 or entry.data_type == Data_Type.BYTE: #bit types
148+
bit_size = Data_Type.getSize(entry.data_type)
149+
150+
new_val = int(value)
151+
if 0 > new_val or new_val > 2**bit_size:
152+
raise ValueError("Invalid value")
153+
154+
bit_index = entry.register_bit
155+
bit_mask = ((1 << bit_size) - 1) << bit_index # Create a mask for extracting X bits starting from bit_index
156+
clear_mask = ~(bit_mask) # Mask for clearing the bits to be updated
157+
158+
# Clear the bits to be updated in the current_value
159+
ushortValue = current_value & clear_mask
160+
161+
# Set the bits according to the new_value at the specified bit position
162+
ushortValue |= (new_val << bit_index) & bit_mask
163+
164+
bit_mask = (1 << bit_size) - 1 # Create a mask for extracting X bits
165+
check_value = (ushortValue >> bit_index) & bit_mask
166+
167+
if check_value != new_val:
168+
raise ValueError("something went wrong bitwise")
169+
else:
170+
raise TypeError("Unsupported data type")
171+
172+
173+
174+
175+
if ushortValue == None:
176+
raise ValueError("Invalid value - None")
177+
178+
self.reader.write_register(entry.register, ushortValue, registry_type=registry_type)
179+
180+
181+
def read_variable(self, variable_name : str, registry_type : Registry_Type, entry : registry_map_entry = None):
119182
##clean for convinecne
120-
variable_name = variable_name.strip().lower().replace(' ', '_')
121-
if registry_type == Registry_Type.INPUT:
122-
registry_map = self.protocolSettings.input_registry_map
123-
elif registry_type == Registry_Type.HOLDING:
124-
registry_map = self.protocolSettings.holding_registry_map
125-
126-
entry : registry_map_entry = None
127-
for e in registry_map:
128-
if e.variable_name == variable_name:
129-
entry = e
130-
break
183+
if variable_name:
184+
variable_name = variable_name.strip().lower().replace(' ', '_')
185+
186+
registry_map = self.protocolSettings.get_registry_map(registry_type)
187+
188+
if entry == None:
189+
for e in registry_map:
190+
if e.variable_name == variable_name:
191+
entry = e
192+
break
131193

132194
if entry:
133195
start : int = 0
@@ -148,6 +210,7 @@ def read_registers(self, ranges : list[tuple] = None, start : int = 0, end : int
148210

149211

150212
if not ranges: #ranges is empty, use min max
213+
end = end + 1
151214
ranges = []
152215
start = start - batch_size
153216
while( start := start + batch_size ) < end:
@@ -218,7 +281,6 @@ def process_registery(self, registry : dict, map : list[registry_map_entry]) ->
218281

219282
if item.register not in registry:
220283
continue
221-
222284
value = ''
223285

224286
if item.data_type == Data_Type.UINT: #read uint
@@ -320,6 +382,16 @@ def process_registery(self, registry : dict, map : list[registry_map_entry]) ->
320382
info[item.variable_name] = value
321383

322384
return info
385+
386+
def read_registry(self, registry_type : Registry_Type = Registry_Type.INPUT) -> dict[str,str]:
387+
map = self.protocolSettings.get_registry_map(registry_type)
388+
if not map:
389+
return {}
390+
391+
registry = self.read_registers(self.protocolSettings.get_registry_ranges(registry_type), registry_type=registry_type)
392+
info = self.process_registery(registry, map)
393+
return info
394+
323395

324396
def read_input_registry(self) -> dict[str,str]:
325397
''' reads input registers and returns as clean dict object inverters '''

invertermodbustomqtt.py

+75-4
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
from inverter import Inverter
3535

36-
from protocol_settings import protocol_settings,Data_Type,registry_map_entry,Registry_Type
36+
from protocol_settings import protocol_settings,Data_Type,registry_map_entry,Registry_Type,WriteMode
3737

3838
__logo = """
3939
____ _ _ ____ __ __ ___ _____ _____
@@ -102,6 +102,9 @@ class InverterModBusToMQTT:
102102

103103
__max_precision : int = -1
104104

105+
__write : bool = False
106+
''' enable / disable write mode - setting'''
107+
105108
__analyze_protocol : bool = False
106109
''' enable / disable analyze mode'''
107110

@@ -199,6 +202,7 @@ def init_invertermodbustomqtt(self):
199202
protocol_version = str(self.__settings.get(section, 'protocol_version'))
200203

201204
self.__analyze_protocol = self.__settings.getboolean(section, 'analyze_protocol', fallback=False)
205+
self.__write = self.__settings.getboolean(section, 'write', fallback=False)
202206
self.__analyze_protocol_save_load = self.__settings.getboolean(section, 'analyze_protocol_save_load', fallback=False)
203207

204208

@@ -317,9 +321,17 @@ def on_connect(self, client, userdata, flags, rc):
317321
self.__mqtt_connected = True
318322

319323

324+
__write_topics : dict[str, registry_map_entry] = {}
325+
320326
def on_message(self, client, userdata, msg):
321327
""" The callback for when a PUBLISH message is received from the server. """
322-
self.__log.info(msg.topic+" "+str(msg.payload))
328+
self.__log.info(msg.topic+" "+str(msg.payload.decode('utf-8')))
329+
330+
#self.inverter.protocolSettings.validate_registry_entry
331+
if msg.topic in self.__write_topics:
332+
entry = self.__write_topics[msg.topic]
333+
self.inverter.write_variable(entry, value=str(msg.payload.decode('utf-8')))
334+
323335

324336
def run(self):
325337
"""
@@ -340,6 +352,9 @@ def run(self):
340352

341353
print("using serial number: " + self.__device_serial_number)
342354

355+
if self.__write:
356+
self.enable_write()
357+
343358
if self.__mqtt_discovery_enabled:
344359
self.mqtt_discovery()
345360

@@ -424,6 +439,55 @@ def run(self):
424439
# If all the inverters are not online because no power is being generated then we sleep for 1 min
425440
time.sleep(self.__offline_interval)
426441

442+
443+
def enable_write(self):
444+
"""
445+
enable write to modbus; must pass tests.
446+
"""
447+
print("Validating Protocol for Writing")
448+
self.__write = False
449+
score_percent = self.validate_protocol(Registry_Type.HOLDING)
450+
if(score_percent > 90):
451+
self.__write = True
452+
print("enable write - validation passed")
453+
454+
self.__write_topics = {}
455+
#subscribe to write topics
456+
for entry in self.inverter.protocolSettings.holding_registry_map:
457+
if entry.write_mode == WriteMode.WRITE:
458+
#__write_topics
459+
topic : str = self.__mqtt_topic + "/write/" + entry.variable_name.lower().replace(' ', '_')
460+
self.__write_topics[topic] = entry
461+
self.__mqtt_client.subscribe(topic)
462+
463+
def validate_protocol(self, registry_type : Registry_Type = Registry_Type.INPUT) -> float:
464+
"""
465+
validate protocol
466+
"""
467+
468+
score : float = 0
469+
info = {}
470+
registry_map : list[registry_map_entry] = self.inverter.protocolSettings.get_registry_map(registry_type)
471+
info = self.inverter.read_registry(registry_type)
472+
473+
for value in registry_map:
474+
if value.variable_name in info:
475+
evaluate = True
476+
477+
if value.concatenate and value.register != value.concatenate_registers[0]: #only eval concated values once
478+
evaluate = False
479+
480+
if evaluate:
481+
score = score + self.inverter.protocolSettings.validate_registry_entry(value, info[value.variable_name])
482+
483+
maxScore = len(registry_map)
484+
percent = score*100/maxScore
485+
print("validation score: " + str(score) + " of " + str(maxScore) + " : " + str(round(percent)) + "%")
486+
return percent
487+
488+
489+
490+
427491
def analyze_protocol(self, settings_dir : str = 'protocols'):
428492
print("=== PROTOCOL ANALYZER ===")
429493
protocol_names : list[str] = []
@@ -602,6 +666,8 @@ def mqtt_discovery(self):
602666
if item.concatenate and item.register != item.concatenate_registers[0]:
603667
continue #skip all except the first register so no duplicates
604668

669+
if item.write_mode == WriteMode.READDISABLED: #disabled
670+
continue
605671

606672
clean_name = item.variable_name.lower().replace(' ', '_')
607673

@@ -620,13 +686,18 @@ def mqtt_discovery(self):
620686
disc_payload['device'] = device
621687
disc_payload['name'] = clean_name
622688
disc_payload['unique_id'] = "hotnoob_" + self.__device_serial_number + "_"+clean_name
623-
disc_payload['state_topic'] = self.__mqtt_topic + "/"+clean_name
689+
690+
writePrefix = ""
691+
if self.__write and item.write_mode == WriteMode.WRITE:
692+
writePrefix = "" #home assistant doesnt like write prefix
693+
694+
disc_payload['state_topic'] = self.__mqtt_topic +writePrefix+ "/"+clean_name
624695

625696
if item.unit:
626697
disc_payload['unit_of_measurement'] = item.unit
627698

628699

629-
discovery_topic = self.__mqtt_discovery_topic+"/sensor/inverter-" + self.__device_serial_number + "/" + disc_payload['name'].replace(' ', '_') + "/config"
700+
discovery_topic = self.__mqtt_discovery_topic+"/sensor/inverter-" + self.__device_serial_number + writePrefix + "/" + disc_payload['name'].replace(' ', '_') + "/config"
630701

631702
self.__mqtt_client.publish(discovery_topic,
632703
json.dumps(disc_payload),qos=1, retain=True)

0 commit comments

Comments
 (0)