Skip to content

Commit 5e30b04

Browse files
authored
Add option for TLS (#32)
Add support for providing client/CA certs/keys to enable TLS connections to an MQTT broker.
1 parent 4294158 commit 5e30b04

File tree

2 files changed

+67
-24
lines changed

2 files changed

+67
-24
lines changed

.pep8speaks.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pycodestyle: # Same as scanner.linter value. Other option is flake8
2+
max-line-length: 120 # Default is 79 in PEP 8 - sorry IBM 3270 terminal users

modbus4mqtt/modbus4mqtt.py

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,29 @@
1212

1313
MAX_DECIMAL_POINTS = 8
1414

15+
1516
class mqtt_interface():
16-
def __init__(self, hostname, port, username, password, config_file, mqtt_topic_prefix):
17+
def __init__(self, hostname, port, username, password, config_file, mqtt_topic_prefix,
18+
use_tls=True, insecure=False, cafile=None, cert=None, key=None):
1719
self.hostname = hostname
1820
self._port = port
1921
self.username = username
2022
self.password = password
2123
self.config = self._load_modbus_config(config_file)
24+
self.use_tls = use_tls
25+
self.insecure = insecure
26+
self.cafile = cafile
27+
self.cert = cert
28+
self.key = key
2229
if not mqtt_topic_prefix.endswith('/'):
2330
mqtt_topic_prefix = mqtt_topic_prefix + '/'
2431
self.prefix = mqtt_topic_prefix
2532
self.address_offset = self.config.get('address_offset', 0)
2633
self.registers = self.config['registers']
2734
for register in self.registers:
2835
register['address'] += self.address_offset
29-
self.modbus_connect_retries = -1 # Retry forever by default
30-
self.modbus_reconnect_sleep_interval = 5 # Wait this many seconds between modbus connection attempts
36+
self.modbus_connect_retries = -1 # Retry forever by default
37+
self.modbus_reconnect_sleep_interval = 5 # Wait this many seconds between modbus connection attempts
3138

3239
def connect(self):
3340
# Connects to modbus and MQTT.
@@ -65,6 +72,9 @@ def connect_mqtt(self):
6572
self._mqtt_client._on_disconnect = self._on_disconnect
6673
self._mqtt_client._on_message = self._on_message
6774
self._mqtt_client._on_subscribe = self._on_subscribe
75+
if self.use_tls:
76+
self._mqtt_client.tls_set(ca_certs=self.cafile, certfile=self.cert, keyfile=self.key)
77+
self._mqtt_client.tls_insecure_set(self.insecure)
6878
self._mqtt_client.connect(self.hostname, self._port, 60)
6979
self._mqtt_client.loop_start()
7080

@@ -87,8 +97,9 @@ def poll(self):
8797
for register in self._get_registers_with('pub_topic'):
8898
try:
8999
value = self._mb.get_value(register.get('table', 'holding'), register['address'])
90-
except:
91-
logging.warning("Couldn't get value from register {} in table {}".format(register['address'], register.get('table', 'holding')))
100+
except Exception:
101+
logging.warning("Couldn't get value from register {} in table {}".format(register['address'],
102+
register.get('table', 'holding')))
92103
continue
93104
# Filter the value through the mask, if present.
94105
value &= register.get('mask', 0xFFFF)
@@ -157,24 +168,28 @@ def _on_message(self, client, userdata, msg):
157168
if 'value_map' in register:
158169
try:
159170
value = str(value, 'utf-8')
160-
if not value in register['value_map']:
161-
logging.warning("Value not in value_map. Topic: {}, value: {}, valid values: {}".format(topic, value, register['value_map'].keys()))
171+
if value not in register['value_map']:
172+
logging.warning("Value not in value_map. Topic: {}, value: {}, valid values: {}".format(topic,
173+
value, register['value_map'].keys()))
162174
continue
163175
# Map the value from the human-readable form into the raw modbus number
164176
value = register['value_map'][value]
165177
except UnicodeDecodeError:
166-
logging.warning("Failed to decode MQTT payload as UTF-8. Can't compare it to the value_map for register {}".format(register))
178+
logging.warning("Failed to decode MQTT payload as UTF-8. "
179+
"Can't compare it to the value_map for register {}".format(register))
167180
continue
168181
try:
169182
# Scale the value, if required.
170183
value = float(value)
171184
value = round(value/register.get('scale', 1))
172185
except ValueError:
173-
logging.error("Failed to convert register value for writing. Bad/missing value_map? Topic: {}, Value: {}".format(topic, value))
186+
logging.error("Failed to convert register value for writing. "
187+
"Bad/missing value_map? Topic: {}, Value: {}".format(topic, value))
174188
continue
175189
type = register.get('type', 'uint16')
176190
value = modbus_interface._convert_from_type_to_uint16(value, type)
177-
self._mb.set_value(register.get('table', 'holding'), register['address'], int(value), register.get('mask', 0xFFFF))
191+
self._mb.set_value(register.get('table', 'holding'), register['address'], int(value),
192+
register.get('mask', 0xFFFF))
178193

179194
# This throws ValueError exceptions if the imported registers are invalid
180195
@staticmethod
@@ -197,27 +212,34 @@ def _validate_registers(registers):
197212
duplicate_json_keys[register['pub_topic']] = []
198213
retain_setting[register['pub_topic']] = set()
199214
if 'json_key' in register and 'set_topic' in register:
200-
raise ValueError("Bad YAML configuration. Register with set_topic '{}' has a json_key specified. This is invalid. See https://github.com/tjhowse/modbus4mqtt/issues/23 for details.".format(register['set_topic']))
215+
raise ValueError("Bad YAML configuration. Register with set_topic '{}' has a json_key specified. "
216+
"This is invalid. See https://github.com/tjhowse/modbus4mqtt/issues/23 for details."
217+
.format(register['set_topic']))
201218
all_pub_topics.add(register['pub_topic'])
202219

203220
# Check that all registers with duplicate pub topics have json_keys
204221
for register in registers:
205222
if register['pub_topic'] in duplicate_pub_topics:
206223
if 'json_key' not in register:
207-
raise ValueError("Bad YAML configuration. pub_topic '{}' duplicated across registers without json_key field. Registers that share a pub_topic must also have a unique json_key.".format(register['pub_topic']))
224+
raise ValueError("Bad YAML configuration. pub_topic '{}' duplicated across registers without "
225+
"json_key field. Registers that share a pub_topic must also have a unique "
226+
"json_key.".format(register['pub_topic']))
208227
if register['json_key'] in duplicate_json_keys[register['pub_topic']]:
209-
raise ValueError("Bad YAML configuration. pub_topic '{}' duplicated across registers with a duplicated json_key field. Registers that share a pub_topic must also have a unique json_key.".format(register['pub_topic']))
228+
raise ValueError("Bad YAML configuration. pub_topic '{}' duplicated across registers with a "
229+
"duplicated json_key field. Registers that share a pub_topic must also have "
230+
"a unique json_key.".format(register['pub_topic']))
210231
duplicate_json_keys[register['pub_topic']] += [register['json_key']]
211232
if 'retain' in register:
212233
retain_setting[register['pub_topic']].add(register['retain'])
213234
# Check that there are no disagreements as to whether this pub_topic should be retained or not.
214235
for topic, retain_set in retain_setting.items():
215236
if len(retain_set) > 1:
216-
raise ValueError("Bad YAML configuration. pub_topic '{}' has conflicting retain settings.".format(topic))
237+
raise ValueError("Bad YAML configuration. pub_topic '{}' has conflicting retain settings."
238+
.format(topic))
217239

218240
def _load_modbus_config(self, path):
219-
yaml=YAML(typ='safe')
220-
result = yaml.load(open(path,'r').read())
241+
yaml = YAML(typ='safe')
242+
result = yaml.load(open(path, 'r').read())
221243
registers = [register for register in result['registers'] if 'pub_topic' in register]
222244
mqtt_interface._validate_registers(registers)
223245
return result
@@ -228,22 +250,41 @@ def loop_forever(self):
228250
self.poll()
229251
sleep(self.config['update_rate'])
230252

253+
231254
@click.command()
232-
@click.option('--hostname', default='localhost', help='The hostname or IP address of the MQTT server.', show_default=True)
233-
@click.option('--port', default=1883, help='The port of the MQTT server.', show_default=True)
234-
@click.option('--username', default='username', help='The username to authenticate to the MQTT server.', show_default=True)
235-
@click.option('--password', default='password', help='The password to authenticate to the MQTT server.', show_default=True)
236-
@click.option('--config', default='./Sungrow_SH5k_20.yaml', help='The YAML config file for your modbus device.', show_default=True)
237-
@click.option('--mqtt_topic_prefix', default='modbus4mqtt', help='A prefix for published MQTT topics.', show_default=True)
238-
def main(hostname, port, username, password, config, mqtt_topic_prefix):
255+
@click.option('--hostname', default='localhost',
256+
help='The hostname or IP address of the MQTT server.', show_default=True)
257+
@click.option('--port', default=1883,
258+
help='The port of the MQTT server.', show_default=True)
259+
@click.option('--username', default='username',
260+
help='The username to authenticate to the MQTT server.', show_default=True)
261+
@click.option('--password', default='password',
262+
help='The password to authenticate to the MQTT server.', show_default=True)
263+
@click.option('--mqtt_topic_prefix', default='modbus4mqtt',
264+
help='A prefix for published MQTT topics.', show_default=True)
265+
@click.option('--config', default='./Sungrow_SH5k_20.yaml',
266+
help='The YAML config file for your modbus device.', show_default=True)
267+
@click.option('--use_tls', default=False,
268+
help='Configure network encryption and authentication options. Enables SSL/TLS.', show_default=True)
269+
@click.option('--insecure', default=True,
270+
help='Do not check that the server certificate hostname matches the remote hostname.', show_default=True)
271+
@click.option('--cafile', default=None,
272+
help='The path to a file containing trusted CA certificates to enable encryption.', show_default=True)
273+
@click.option('--cert', default=None,
274+
help='Client certificate for authentication, if required by server.', show_default=True)
275+
@click.option('--key', default=None,
276+
help='Client private key for authentication, if required by server.', show_default=True)
277+
def main(hostname, port, username, password, config, mqtt_topic_prefix, use_tls, insecure, cafile, cert, key):
239278
logging.basicConfig(
240279
format='%(asctime)s %(levelname)-8s %(message)s',
241280
level=logging.INFO,
242281
datefmt='%Y-%m-%d %H:%M:%S')
243282
logging.info("Starting modbus4mqtt v{}".format(version.version))
244-
i = mqtt_interface(hostname, port, username, password, config, mqtt_topic_prefix)
283+
i = mqtt_interface(hostname, port, username, password, config, mqtt_topic_prefix,
284+
use_tls, insecure, cafile, cert, key)
245285
i.connect()
246286
i.loop_forever()
247287

288+
248289
if __name__ == '__main__':
249290
main()

0 commit comments

Comments
 (0)