11 Commits
0.6 ... main

Author SHA1 Message Date
geoffwhittington
d4a17de70a Merge pull request #5 from jp-bennett/main
Fix Docker image and work on Owntracks plugin
2024-09-24 10:54:50 -04:00
Jonathan Bennett
b23b82173c Allow Owntracks plugin to handle negative and hex node numbers 2023-09-26 20:28:58 -05:00
Jonathan Bennett
61116198fc Bump Dockerfile Python version to fix crash 2023-09-26 17:20:09 -05:00
geoffwhittington
5ce046de13 Merge pull request #2 from jp-bennett/main
Adds owntracks-plugin
2023-06-16 09:43:25 -04:00
Jonathan Bennett
0eea64f2e4 Add more Owntracks_plugin documentation 2023-06-11 18:55:03 -05:00
Jonathan Bennett
6d186ea339 Add documentation to the use-cases 2023-06-11 18:48:23 -05:00
Jonathan Bennett
7d20077166 Remove wait for mqtt, fixes deadlock 2023-06-11 18:42:31 -05:00
Jonathan Bennett
5a4ca73e8c Adds plugin to push location messages to an Owntracks mqtt instance 2023-06-11 18:31:56 -05:00
Jonathan Bennett
42a4e4a76a Add support for MQTT-only operation 2023-06-11 14:48:29 -05:00
Geoff Whittington
170d52ef14 Add support for NoStr 2023-02-11 22:46:06 -05:00
Geoff Whittington
0fccf6ea9e Add support for NoStr 2023-02-11 22:43:51 -05:00
7 changed files with 277 additions and 38 deletions

View File

@@ -1,5 +1,5 @@
# set base image (host OS)
FROM python:3.8
FROM python:3.9
# set the working directory in the container
WORKDIR /code

View File

@@ -111,6 +111,8 @@ The following plugins can be used in the `pipelines` section of `config.yaml`:
| `encrypt_filter` | Encrypt a packet for a desired MQTT recipient |
| `decrypt_filter` | Decrypt a packet originating from MQTT |
| `radio_message_plugin` | Send a packet to a specified `device` |
| `nostr_plugin` | Send a NoStr event to a relay |
| `owntracks_plugin` | Send location data to MQTT server for Owntracks |
### debugger - Output the contents of a packet
@@ -238,6 +240,49 @@ decrypt_filter:
key: '/home/user/keys/key.pem'
```
### nostr_plugin - Send a NoStr event
- **log_level** `debug` or `info`. Default `info`
- **private_key** The private key for a NoStr user. Secrets can be passed using ENV variables
- **public_key** The public key for the NoStr user associated with the private key.
- **message** A specific message (Optional)
- **relays** List of NoStr relays. Default `wss://nostr-pub.wellorder.net`, and `wss://relay.damus.io`
For example:
```
nostr_plugin:
private_key: "{NOSTR_PRIVATE_KEY}"
public_key: "npub1d0ja5d.......xw7jys4eqnk0"
relays:
- "wss://nostr-pub.wellorder.net"
```
Placeholders can be used with the **message** value:
- `{MSG}` - Packet text
### owntracks_plugin - Send location data to MQTT server for Owntracks
- **log_level** `debug` or `info`. Default `info`
- **server_name** The mqtt server to send owntracks messages to
- **tid_table** Table of the numeric from IDs of each node, mapped to an Owntracks name and TID
For example:
```
owntracks_plugin:
server_name: external
tid_table:
"1234": ["Van", "GV"]
"-5678": ["Home", "HR"]
```
Placeholders can be used with the **message** value:
- `{MSG}` - Packet text
### radio_message_plugin - Send a packet to a radio
- **log_level** `debug` or `info`. Default `info`

70
main.py
View File

@@ -31,33 +31,33 @@ class CustomTCPInterface(meshtastic.tcp_interface.TCPInterface):
def onReceive(packet, interface): # called when a packet arrives
nodeInfo = interface.getMyNodeInfo()
for pipeline, pipeline_plugins in bridge_config["pipelines"].items():
logger.debug(f"Pipeline {pipeline} initiated")
if "pipelines" in bridge_config:
for pipeline, pipeline_plugins in bridge_config["pipelines"].items():
logger.debug(f"Pipeline {pipeline} initiated")
p = plugins["packet_filter"]
pipeline_packet = p.do_action(packet)
p = plugins["packet_filter"]
pipeline_packet = p.do_action(packet)
for plugin in pipeline_plugins:
if not pipeline_packet:
continue
for plugin_key, plugin_config in plugin.items():
logger.debug(f"Processing plugin: {pipeline}/{plugin_key}")
for plugin in pipeline_plugins:
if not pipeline_packet:
logger.debug("Skipping since the packet is null")
continue
if plugin_key not in plugins:
logger.error(f"No such plugin: {plugin_key}. Skipping")
continue
for plugin_key, plugin_config in plugin.items():
logger.debug(f"Processing plugin: {pipeline}/{plugin_key}")
if not pipeline_packet:
logger.debug("Skipping since the packet is null")
continue
p = plugins[plugin_key]
p.configure(devices, mqtt_servers, plugin_config)
if plugin_key not in plugins:
logger.error(f"No such plugin: {plugin_key}. Skipping")
continue
pipeline_packet = p.do_action(pipeline_packet)
p = plugins[plugin_key]
p.configure(devices, mqtt_servers, plugin_config)
logger.debug(f"Pipeline {pipeline} completed")
pipeline_packet = p.do_action(pipeline_packet)
logger.debug(f"Pipeline {pipeline} completed")
def onConnection(
@@ -87,22 +87,23 @@ with open("config.yaml") as f:
devices = {}
mqtt_servers = {}
for device in bridge_config["devices"]:
if "active" in device and not device["active"]:
continue
if "devices" in bridge_config:
for device in bridge_config["devices"]:
if "active" in device and not device["active"]:
continue
if "serial" in device:
devices[device["name"]] = meshtastic.serial_interface.SerialInterface(
devPath=device["serial"]
)
elif "tcp" in device:
logger.debug(f"Connecting to {device['tcp']} ...")
devices[device["name"]] = CustomTCPInterface(
hostname=device["tcp"], device_name=device["name"]
)
logger.debug(f"Connected to {device['tcp']}")
else:
devices[device["name"]] = meshtastic.serial_interface.SerialInterface()
if "serial" in device:
devices[device["name"]] = meshtastic.serial_interface.SerialInterface(
devPath=device["serial"]
)
elif "tcp" in device:
logger.debug(f"Connecting to {device['tcp']} ...")
devices[device["name"]] = CustomTCPInterface(
hostname=device["tcp"], device_name=device["name"]
)
logger.debug(f"Connected to {device['tcp']}")
else:
devices[device["name"]] = meshtastic.serial_interface.SerialInterface()
if "mqtt_servers" in bridge_config:
for config in bridge_config["mqtt_servers"]:
@@ -146,7 +147,6 @@ if "mqtt_servers" in bridge_config:
pipeline_packet = p.do_action(orig_packet)
for pipeline, pipeline_plugins in config["pipelines"].items():
packet = pipeline_packet
logger.debug(f"MQTT {config['name']} pipeline {pipeline} initiated")

View File

@@ -6,7 +6,7 @@ import json
import logging
import os
import re
import ssl
plugins = {}
@@ -318,11 +318,102 @@ class MQTTPlugin(Plugin):
plugins["mqtt_plugin"] = MQTTPlugin()
class OwntracksPlugin(Plugin):
logger = logging.getLogger(name="meshtastic.bridge.plugin.Owntracks")
def do_action(self, packet):
required_options = ["tid_table", "server_name"]
for option in required_options:
if option not in self.config:
self.logger.warning(f"Missing config: {option}")
return packet
#tid_table = self.config["tid_table"]
tid_table = {}
for tid_entry in self.config["tid_table"]: # We want to check for a key with an ! and convert to string
if "!" in tid_entry:
tid_table[str(int(tid_entry[1:], 16))] = self.config["tid_table"][tid_entry]
else:
tid_table[tid_entry] = self.config["tid_table"][tid_entry]
if not "from" in packet:
self.logger.warning("Missing from: field")
return packet
if packet["from"] < 0:
packet["from"] = packet["from"] +(1 << 32)
if not str(packet["from"]) in tid_table:
self.logger.warning(f"Sender not in tid_table: {packet}")
return packet
from_str = str(packet["from"])
message = json.loads('{"_type":"location", "bs":0}')
message["tid"] = tid_table[from_str][1]
self.logger.debug(f"processing packet {packet}")
#Packet direct from radio
if (
"decoded" in packet
and "position" in packet["decoded"]
and "latitude" in packet["decoded"]["position"]
and packet["decoded"]["position"]["latitude"] != 0
):
message["lat"] = packet["decoded"]["position"]["latitude"]
message["lon"] = packet["decoded"]["position"]["longitude"]
message["tst"] = packet["decoded"]["position"]["time"]
message["created_at"] = packet["rxTime"]
if "altitude" in packet["decoded"]["position"]:
message["alt"] = packet["decoded"]["position"]["altitude"]
#packet from mqtt
elif (
"type" in packet
and packet["type"] == "position"
and "payload" in packet
and "latitude_i" in packet["payload"]
and packet["payload"]["latitude_i"] != 0
):
message["lat"] = packet["payload"]["latitude_i"]/10000000
message["lon"] = packet["payload"]["longitude_i"]/10000000
message["tst"] = packet["timestamp"]
if ("time" in packet["payload"]):
message["created_at"] = packet["payload"]["time"]
else:
message["created_at"] = packet["timestamp"]
if "altitude" in packet["payload"]:
message["alt"] = packet["payload"]["altitude"]
else:
self.logger.debug("Not a location packet")
return packet
if self.config["server_name"] not in self.mqtt_servers:
self.logger.warning(f"No server established: {self.config['server_name']}")
return packet
mqtt_server = self.mqtt_servers[self.config["server_name"]]
if not mqtt_server.is_connected():
self.logger.error("Not sent, not connected")
return
self.logger.debug("Sending owntracks message")
info = mqtt_server.publish("owntracks/user/" + tid_table[from_str][0], json.dumps(message))
#info.wait_for_publish()
self.logger.debug("Message sent")
return packet
plugins["owntracks_plugin"] = OwntracksPlugin()
class EncryptFilter(Plugin):
logger = logging.getLogger(name="meshtastic.bridge.filter.encrypt")
def do_action(self, packet):
if "key" not in self.config:
return None
@@ -453,3 +544,75 @@ class RadioMessagePlugin(Plugin):
plugins["radio_message_plugin"] = RadioMessagePlugin()
import time
from nostr.event import Event
from nostr.relay_manager import RelayManager
from nostr.message_type import ClientMessageType
from nostr.key import PrivateKey, PublicKey
class NoStrPlugin(Plugin):
logger = logging.getLogger(name="meshtastic.bridge.plugin.nostr_send")
def do_action(self, packet):
relays = ["wss://nostr-pub.wellorder.net", "wss://relay.damus.io"]
for config_value in ["private_key", "public_key"]:
if config_value not in self.config:
self.logger.debug(f"Missing {config_value}")
return packet
# configure relays
if "relays" in self.config:
for relay in self.config["relays"]:
relays.append(relay)
relay_manager = RelayManager()
for relay in relays:
relay_manager.add_relay(relay)
self.logger.debug(f"Opening connection to NoStr relays...")
relay_manager.open_connections(
{"cert_reqs": ssl.CERT_NONE}
) # NOTE: This disables ssl certificate verification
time.sleep(
self.config["startup_wait"] if "startup_wait" in self.config else 1.25
) # allow the connections to open
# Opportunistically use environment variable
for ek, ev in os.environ.items():
needle = "{" + ek + "}"
if needle in self.config["private_key"]:
self.config["private_key"] = self.config["private_key"].replace(
needle, ev
)
private_key = PrivateKey.from_nsec(self.config["private_key"])
public_key = PublicKey.from_npub(self.config["public_key"])
if "message" in self.config:
message = self.config["message"].replace("{MSG}", packet["decoded"]["text"])
else:
message = packet["decoded"]["text"]
event = Event(content=message, public_key=public_key.hex())
private_key.sign_event(event)
self.logger.debug(f"Sending message to NoStr ...")
relay_manager.publish_event(event)
self.logger.info(f"Sent message to NoStr")
time.sleep(
self.config["publish_wait"] if "publish_wait" in self.config else 1
) # allow the messages to send
relay_manager.close_connections()
return packet
plugins["nostr_plugin"] = NoStrPlugin()

View File

@@ -4,3 +4,4 @@ requests
pyyaml
paho-mqtt
jwcrypto
nostr

View File

@@ -0,0 +1,12 @@
mqtt_servers:
- name: local
server: localhost
port: 1883
topic: msh/2/json/#
pipelines:
owntrack:
- owntracks_plugin:
server_name: local
tid_table:
"12345": ["Van", "GV"]
"-6789": ["Home", "HR"]

View File

@@ -0,0 +1,18 @@
devices:
- name: radio1
tcp: 192.168.1.110
mqtt_servers:
- name: external
server: localhost
port: 1883
pipelines:
owntracks:
- owntracks_plugin:
server_name: external
tid_table:
"1234": ["Van", "GV"]
"-5678": ["Home", "HR"]