From c622cc059b42d6237aff39998227eae10abecf02 Mon Sep 17 00:00:00 2001 From: Hans Verrier Date: Tue, 16 Sep 2025 21:12:07 +0200 Subject: [PATCH 1/8] feat: add option mqtt_replace_spaces to replace spaces in topics --- MQTTClient.py | 4 ++++ config.json.sample | 1 + 2 files changed, 5 insertions(+) diff --git a/MQTTClient.py b/MQTTClient.py index 74e38fc..c4abe5b 100644 --- a/MQTTClient.py +++ b/MQTTClient.py @@ -84,6 +84,10 @@ def _on_message(self, client, userdata, message) -> None: def publish(self, task) -> None: topic = "%s/%s" % (self.config['mqtt_prefix'], task['topic']) + + if self.config.get('mqtt_replace_spaces', False): + topic = topic.replace(" ", "_") + if self.mqttDataFormat == 'json': if is_number(task['payload']): task['payload'] = '{"value": ' + str(task['payload']) + '}' diff --git a/config.json.sample b/config.json.sample index ca15cf3..eeb50c1 100644 --- a/config.json.sample +++ b/config.json.sample @@ -6,6 +6,7 @@ "mqtt_message_timeout": 60, "mqtt_user":"your_mqtt_user", "mqtt_password":"your_mqtt_password", + "mqtt_replace_spaces": true, "rflink_tty_device": "/dev/ttyUSB0", "rflink_direct_output_params": [ "BAT", From 9203d5e570ac1f1894058eff9de8e6c7c0a2e7fc Mon Sep 17 00:00:00 2001 From: Hans Verrier Date: Wed, 28 Jan 2026 20:42:28 +0100 Subject: [PATCH 2/8] feat: add ignores devices --- .DS_Store | Bin 0 -> 6148 bytes SerialProcess.py | 22 ++++++++++++++++++++++ config.json.sample | 4 ++++ 3 files changed, 26 insertions(+) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..960108d0f45f70f36b3410e747ce012b2bde5c42 GIT binary patch literal 6148 zcmeHK%We}f6g^HxB@`hAq)OdXnlGr-4PwD!NT;nTQ3)FMEzJxeFv)~u(#LN0{RjF9 zd;x2|fPFurn@W}7T-(4rk}j))d?WjK?0bB#&m?vXKy_x(2G9o3ViEL~S25Yc0H`4j{&mM?^iwV6vu{X7#=0*5&K$8KaM*$ zFTTGlk4LwTk3SUlhUO)yc)+;Fn4*Ui5#myGG$ne)qnN|9Zx`N`$)V=;H13ab97xHRCg8j3Zp(Nwg{1 z8BUQ^_}4Z5KDP0S>=Dt|vHHoL@y5c^Ad``1p?*&up>QBZIDX3OEIv z0)G^c{lRAu3@qjb_13|v!ABYQ1jX2vV^Pqz7+A~=a)hP~m1wBSUNMxRv){LVfyLaQ zp+o7&oX191_J*Q#boTq&9V#&BYNvoxpsYaCJhtWh-&%bCFO%GpQ@| None: self.processing_wdir = config['rflink_wdir_output_params'] + self.ignored_devices = config.get('rflink_ignored_devices', []) + self.logger.debug("Ignored devices config: %s", self.ignored_devices) + def close(self) -> None: self.sp.close() self.logger.debug('Serial closed') @@ -36,6 +39,12 @@ def prepare_output(self, data_in) -> list: if len(data) > 3 and data[0] == '20': family = data[2] deviceId = data[3].split("=")[1] + if self.is_device_ignored(family, deviceId): + self.logger.debug( + "Ignoring RFLink device %s/%s" % (family, deviceId) + ) + return [] + switch = data[4].split("=")[1] d = {} for t in data[4:]: @@ -112,3 +121,16 @@ def run(self): except Exception as e: self.logger.error('Receive error: %s' % (e)) self.connect() + + def is_device_ignored(self, family, deviceId) -> bool: + family = family.lower() + deviceId = deviceId.lower() + for entry in self.ignored_devices: + if "/" in entry: + fam, dev = entry.split("/", 1) + if fam.lower() == family and dev.lower() == deviceId: + return True + else: + if entry.lower() == family: + return True + return False \ No newline at end of file diff --git a/config.json.sample b/config.json.sample index eeb50c1..c7b0214 100644 --- a/config.json.sample +++ b/config.json.sample @@ -25,5 +25,9 @@ ], "rflink_wdir_output_params": [ "WINDIR" + ], + "rflink_ignored_devices": [ + "Friedland", + "Alecto v1/FE07" ] } From 57d1f833bee22ecc0da51fb21c85fbc5743020d5 Mon Sep 17 00:00:00 2001 From: Hans Verrier Date: Wed, 28 Jan 2026 20:45:36 +0100 Subject: [PATCH 3/8] feat: support environment overrides for config.json --- RFLinkGateway.py | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/RFLinkGateway.py b/RFLinkGateway.py index fd63b74..3b6fbdb 100644 --- a/RFLinkGateway.py +++ b/RFLinkGateway.py @@ -1,3 +1,4 @@ +import os import json import logging import multiprocessing @@ -20,21 +21,48 @@ ch.setLevel(logging.DEBUG) logger.addHandler(ch) +def load_config(): + # load config.json and overrides with environment variables + config = {} + + try: + with open('config.json') as f: + config = json.load(f) + logger.info("Configuration loaded from config.json") + except Exception as e: + logger.error("Failed to load config.json: %s", e) + exit(1) + + env = {k.lower(): v for k, v in os.environ.items()} + for key in list(config.keys()): + key_lower = key.lower() + if key_lower in env: + raw_value = env[key_lower] + + # Tentative de parsing JSON (list, dict, bool, int…) + try: + value = json.loads(raw_value) + except Exception: + value = raw_value + + logger.info( + "Config override: %s = %s (env)", + key, + value + ) + config[key] = value + + return config def main(): + # load configuration + config = load_config() + # messages read from device messageQ = multiprocessing.Queue() # messages written to device commandQ = multiprocessing.Queue() - config = {} - try: - with open('config.json') as json_data: - config = json.load(json_data) - except Exception as e: - logger.error("Config load failed") - exit(1) - sp = SerialProcess.SerialProcess(messageQ, commandQ, config) sp.daemon = True sp.start() From f84ef0e273bc4f65bfcc7cd52276ec6db1f6da6c Mon Sep 17 00:00:00 2001 From: Hans Verrier Date: Wed, 28 Jan 2026 21:00:33 +0100 Subject: [PATCH 4/8] feat: add configurable log level with environment overrides --- RFLinkGateway.py | 37 ++++++++++++++++++++++--------------- config.json.sample | 1 + 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/RFLinkGateway.py b/RFLinkGateway.py index 3b6fbdb..7eb9e9d 100644 --- a/RFLinkGateway.py +++ b/RFLinkGateway.py @@ -13,16 +13,9 @@ import SerialProcess logger = logging.getLogger('RFLinkGW') -formatter = logging.Formatter('%(asctime)s - %(name)s - %(funcName)s - %(levelname)s - %(message)s') -logger.setLevel(logging.DEBUG) - -ch = logging.StreamHandler() -ch.setFormatter(formatter) -ch.setLevel(logging.DEBUG) -logger.addHandler(ch) def load_config(): - # load config.json and overrides with environment variables + # load config.json and overrides with environnment variables config = {} try: @@ -39,25 +32,39 @@ def load_config(): if key_lower in env: raw_value = env[key_lower] - # Tentative de parsing JSON (list, dict, bool, int…) + # parsing JSON (list, dict, bool, int…) try: value = json.loads(raw_value) except Exception: value = raw_value - logger.info( - "Config override: %s = %s (env)", - key, - value - ) + logger.info("Config override: %s = %s (env)", key, value) config[key] = value return config +def setup_logger(config): + level_str = config.get("log_level", "DEBUG").upper() + level = getattr(logging, level_str, logging.DEBUG) + + logger.setLevel(level) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(funcName)s - %(levelname)s - %(message)s') + + ch = logging.StreamHandler() + ch.setFormatter(formatter) + ch.setLevel(level) + + # remove previous handlers to avoid duplicates (if reload) + if logger.hasHandlers(): + logger.handlers.clear() + logger.addHandler(ch) + def main(): # load configuration config = load_config() - + setup_logger(config) + logger.info("Starting RFLinkGateway with log_level=%s", config.get("log_level")) + # messages read from device messageQ = multiprocessing.Queue() # messages written to device diff --git a/config.json.sample b/config.json.sample index c7b0214..b4b22aa 100644 --- a/config.json.sample +++ b/config.json.sample @@ -7,6 +7,7 @@ "mqtt_user":"your_mqtt_user", "mqtt_password":"your_mqtt_password", "mqtt_replace_spaces": true, + "log_level": "DEBUG", "rflink_tty_device": "/dev/ttyUSB0", "rflink_direct_output_params": [ "BAT", From 9af8462a82624a46ae96a807534af67720442d8c Mon Sep 17 00:00:00 2001 From: Heliospeed <33458266+Heliospeed@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:09:47 +0100 Subject: [PATCH 5/8] Delete .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 960108d0f45f70f36b3410e747ce012b2bde5c42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%We}f6g^HxB@`hAq)OdXnlGr-4PwD!NT;nTQ3)FMEzJxeFv)~u(#LN0{RjF9 zd;x2|fPFurn@W}7T-(4rk}j))d?WjK?0bB#&m?vXKy_x(2G9o3ViEL~S25Yc0H`4j{&mM?^iwV6vu{X7#=0*5&K$8KaM*$ zFTTGlk4LwTk3SUlhUO)yc)+;Fn4*Ui5#myGG$ne)qnN|9Zx`N`$)V=;H13ab97xHRCg8j3Zp(Nwg{1 z8BUQ^_}4Z5KDP0S>=Dt|vHHoL@y5c^Ad``1p?*&up>QBZIDX3OEIv z0)G^c{lRAu3@qjb_13|v!ABYQ1jX2vV^Pqz7+A~=a)hP~m1wBSUNMxRv){LVfyLaQ zp+o7&oX191_J*Q#boTq&9V#&BYNvoxpsYaCJhtWh-&%bCFO%GpQ@| Date: Thu, 29 Jan 2026 08:41:31 +0100 Subject: [PATCH 6/8] docs: update README with new config options --- config.json.sample | 5 +---- readme.md | 9 +++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/config.json.sample b/config.json.sample index b4b22aa..914d6e2 100644 --- a/config.json.sample +++ b/config.json.sample @@ -27,8 +27,5 @@ "rflink_wdir_output_params": [ "WINDIR" ], - "rflink_ignored_devices": [ - "Friedland", - "Alecto v1/FE07" - ] + "rflink_ignored_devices": [] } diff --git a/readme.md b/readme.md index 67343fa..c431e1c 100644 --- a/readme.md +++ b/readme.md @@ -53,6 +53,8 @@ Whole configuration is located in config.json file. You can copy and edit `confi "mqtt_message_timeout": 60, "mqtt_user":"your_mqtt_user", "mqtt_password":"your_mqtt_password", + "mqtt_replace_spaces": true, + "log_level": "DEBUG", "rflink_tty_device": "/dev/ttyUSB0", "rflink_direct_output_params": [ "BAT", @@ -71,6 +73,10 @@ Whole configuration is located in config.json file. You can copy and edit `confi ], "rflink_wdir_output_params": [ "WINDIR" + ], + "rflink_ignored_devices": [ + "RTS", + "Alecto v1/FE07" ] } ``` @@ -81,10 +87,13 @@ config param | meaning mqtt_port | MQTT broker port| mqtt_prefix | prefix for publish and subscribe topic| mqtt_format | publish and subscribe topic as `json` or `ascii` | + mqtt_replace_spaces | replace spaces in MQTT topics with `_` (`true` to enable, default: `false`) | + log_level | Logging level (`DEBUG`, `INFO`, `WARNING`, `ERROR`, default: `DEBUG`) | rflink_tty_device | Serial device | rflink_direct_output_params | Parameters transferred to MQTT without any processing | rflink_signed_output_params | Parameters with signed values | rflink_wdir_output_params | Parameters with wind direction values | + rflink_ignored_devices | List of RFLink device families or specific devices to ignore (e.g. `RTS` or `RTS/AX67`) | From 46e8c753c86a44a50b2acde4f2d0d2903ac6ce54 Mon Sep 17 00:00:00 2001 From: Hans Verrier Date: Thu, 29 Jan 2026 08:41:35 +0100 Subject: [PATCH 7/8] chore: ignore .DS_Store files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 585c2ec..6a798f1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ bin lib include .Python +.DS_Store From 1bbd3ba1f8856236f4b04c6b96c0555ea9221c74 Mon Sep 17 00:00:00 2001 From: Hans Verrier Date: Sun, 8 Feb 2026 19:58:09 +0100 Subject: [PATCH 8/8] feat(mqtt): add TLS and mTLS support with optional CA validation --- MQTTClient.py | 24 +++++++++++++++++++++++- config.json.sample | 5 +++++ readme.md | 10 ++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/MQTTClient.py b/MQTTClient.py index c4abe5b..0e17ef3 100644 --- a/MQTTClient.py +++ b/MQTTClient.py @@ -1,6 +1,7 @@ import logging import multiprocessing import time +import ssl import paho.mqtt.client as mqtt @@ -35,12 +36,33 @@ def __init__(self, messageQ, commandQ, config) -> None: self._mqttConn = mqtt.Client(client_id='RFLinkGateway') self._mqttConn.username_pw_set(self.config['mqtt_user'], self.config['mqtt_password']) + if self.config.get('mqtt_tls', False): + self.logger.info("Enabling MQTT TLS") + + cafile = self.config.get('mqtt_ca') or None + if not cafile: + raise ValueError("mqtt_tls is enabled but mqtt_ca is not set") + certfile = self.config.get('mqtt_cert') or None + keyfile = self.config.get('mqtt_key') or None + + self._mqttConn.tls_set( + ca_certs=cafile, + certfile=certfile, + keyfile=keyfile , + tls_version=ssl.PROTOCOL_TLS_CLIENT + ) + reject = self.config.get('mqtt_reject_unauthorized', False) + self._mqttConn.tls_insecure_set(not reject) + + self.logger.info( + "TLS reject_unauthorized=%s", reject + ) + self._mqttConn.on_disconnect = self._on_disconnect self._mqttConn.on_publish = self._on_publish self._mqttConn.on_message = self._on_message self._mqttConn.on_connect = self._on_connect self.connect(self.config) - def connect (self,config) -> None: try: diff --git a/config.json.sample b/config.json.sample index 914d6e2..9f4b35a 100644 --- a/config.json.sample +++ b/config.json.sample @@ -6,6 +6,11 @@ "mqtt_message_timeout": 60, "mqtt_user":"your_mqtt_user", "mqtt_password":"your_mqtt_password", + "mqtt_tls": false, + "mqtt_reject_unauthorized": false, + "mqtt_ca": "", + "mqtt_cert": "", + "mqtt_key": "", "mqtt_replace_spaces": true, "log_level": "DEBUG", "rflink_tty_device": "/dev/ttyUSB0", diff --git a/readme.md b/readme.md index c431e1c..905b01b 100644 --- a/readme.md +++ b/readme.md @@ -53,6 +53,11 @@ Whole configuration is located in config.json file. You can copy and edit `confi "mqtt_message_timeout": 60, "mqtt_user":"your_mqtt_user", "mqtt_password":"your_mqtt_password", + "mqtt_tls": false, + "mqtt_reject_unauthorized": false, + "mqtt_ca": "", + "mqtt_cert": "", + "mqtt_key": "", "mqtt_replace_spaces": true, "log_level": "DEBUG", "rflink_tty_device": "/dev/ttyUSB0", @@ -87,6 +92,11 @@ config param | meaning mqtt_port | MQTT broker port| mqtt_prefix | prefix for publish and subscribe topic| mqtt_format | publish and subscribe topic as `json` or `ascii` | + mqtt_tls | enable MQTT over TLS (mqtts) (`true` / `false`, default: `false`) | + mqtt_ca | path to CA certificate file used to validate MQTT broker | + mqtt_cert | path to client certificate file for mutual TLS authentication (optional) | + mqtt_key | path to client private key file for mutual TLS authentication (optional) | + mqtt_reject_unauthorized | reject invalid or untrusted server certificates (`true` = strict validation, default: `false`) | mqtt_replace_spaces | replace spaces in MQTT topics with `_` (`true` to enable, default: `false`) | log_level | Logging level (`DEBUG`, `INFO`, `WARNING`, `ERROR`, default: `DEBUG`) | rflink_tty_device | Serial device |