summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsteve <steve@haxors.club>2024-07-06 16:29:00 +0100
committersteve <steve@haxors.club>2024-07-06 16:29:00 +0100
commitc48a265bb82fde1403a757ecc4e2daed909dbb9c (patch)
tree0722acd71023252c313976dc4f857b6127c58cfc
downloadsspmon-c48a265bb82fde1403a757ecc4e2daed909dbb9c.tar.gz
sspmon-c48a265bb82fde1403a757ecc4e2daed909dbb9c.tar.bz2
sspmon-c48a265bb82fde1403a757ecc4e2daed909dbb9c.zip
Initial commitHEADmaster
-rw-r--r--mqttwarn.ini185
-rw-r--r--requirements.txt17
-rw-r--r--sspmon.py72
-rw-r--r--templates/active.j28
-rw-r--r--udf.py92
5 files changed, 374 insertions, 0 deletions
diff --git a/mqttwarn.ini b/mqttwarn.ini
new file mode 100644
index 0000000..cdf7a76
--- /dev/null
+++ b/mqttwarn.ini
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+# (c) 2014-2022 The mqttwarn developers
+#
+# mqttwarn example configuration file "mqttwarn.ini"
+#
+
+; ------------------------------------------
+; Base configuration
+; ------------------------------------------
+
+[defaults]
+
+
+; ----
+; MQTT
+; ----
+
+hostname = '192.168.1.3'
+port = 1883
+username = None
+password = None
+clientid = 'mqttwarn'
+lwt = 'clients/mqttwarn'
+skipretained = False
+cleansession = False
+
+# MQTTv31 = 3 (default)
+# MQTTv311 = 4
+protocol = 3
+
+
+; -------
+; Logging
+; -------
+
+; Send log output to STDERR
+logfile = 'stream://sys.stderr'
+
+; Send log output to file
+;logfile = 'mqttwarn.log'
+
+; one of: CRITICAL, DEBUG, ERROR, INFO, WARN
+loglevel = DEBUG
+
+; optionally set the log level for filtered messages, defaults to INFO
+;filteredmessagesloglevel = DEBUG
+
+;logformat = '%(asctime)-15s %(levelname)-8s [%(name)-25s] %(message)s'
+
+
+; --------
+; Services
+; --------
+
+; path to file containing self-defined functions like `format` or `alldata`
+functions = 'udf.py'
+
+; name the service providers you will be using.
+launch = file, log, smtp
+
+; Publish mqttwarn status information (retained)
+status_publish = True
+; status_topic = mqttwarn/$SYS
+
+
+
+; -------
+; Targets
+; -------
+
+[config:file]
+append_newline = True
+targets = {
+ 'f01' : ['/tmp/f.01'],
+ 'log-me' : ['/tmp/log.me'],
+ 'mqttwarn' : ['/tmp/mqttwarn.err'],
+ }
+
+[config:log]
+targets = {
+ 'debug' : [ 'debug' ],
+ 'info' : [ 'info' ],
+ 'warn' : [ 'warn' ],
+ 'crit' : [ 'crit' ],
+ 'error' : [ 'error' ]
+ }
+
+; special config for 'failover' events
+[failover]
+targets = log:error, file:mqttwarn
+
+
+[config:smtp]
+server = 'mail.bleg.com:587'
+sender = "MQTTwarn <digest@bleg.com>"
+username = digest@bleg.com
+password = CHANGE_ME
+starttls = True
+# Optional send msg as html or only plain text
+htmlmsg = False
+targets = {
+ 'digest' : [ 'digest@bleg.com' ],
+ }
+
+
+; ------------------------------------------
+; Basic
+; ------------------------------------------
+
+[hello/1]
+; echo '{"name": "temperature", "number": 42.42}' | mosquitto_pub -h localhost -t hello/1 -l
+targets = log:info
+format = '{name}: {number} => {_dthhmm}'
+
+
+; ------------------------------------------
+; OwnTracks
+; ------------------------------------------
+
+[owntracks-location]
+topic = owntracks/+/+
+targets = log:info, file:f01
+datamap = OwnTracksTopic2Data()
+format = OwnTracksConvert()
+
+[owntracks-battery]
+topic = owntracks/+/+
+targets = log:info, file:f01
+datamap = OwnTracksTopic2Data()
+filter = OwnTracksBattFilter()
+format = {username}'s phone battery is getting low ({batt}%)
+
+
+; ------------------------------------------
+; Dynamic targets
+; ------------------------------------------
+
+[robustness-1]
+; even if "foo" is considered an invalid service or
+; "log:baz" is considered an invalid service target,
+; mqttwarn should keep calm and carry on
+topic = test/robustness-1
+targets = foo:bar, log:baz
+
+[topic-targets-dynamic]
+; interpolate transformation data values into topic target, example:
+; mosquitto_pub -t test/topic-targets-dynamic -m '{"loglevel": "crit", "message": "Nur Döner macht schöner!"}'
+topic = test/topic-targets-dynamic
+format = Something {loglevel} happened! {message}
+targets = log:{loglevel}
+
+[topic-targets-func]
+; use functions for computing topic targets, example:
+; mosquitto_pub -t test/topic-targets-func -m '{"condition": "sunny", "remark": "This should go to a file"}'
+; mosquitto_pub -t test/topic-targets-func -m '{"condition": "rainy", "remark": "This should go to the log"}'
+topic = test/topic-targets-func
+format = Weather conditions changed: It's {condition}. Remark: {remark}
+targets = TopicTargetList()
+
+
+; ------------------------------------------
+; Periodic tasks
+; ------------------------------------------
+
+[cron]
+; Demonstrate periodic task feature.
+; Define a function for publishing your public ip address to the MQTT bus each minute.
+; mosquitto_sub -t 'test/ip/#' -v
+#publish_public_ip_address = 60; now=true
+
+[sspmon/ssps/active]
+; ----
+; SSPMon
+; ----
+topic = sspmon/ssps/active
+targets = smtp:digest
+template = active.j2
+
+[sspmon/ssps/expired]
+; ----
+; SSPMon
+; ----
+topic = sspmon/ssps/expired
+targets = smtp:digest
+template = active.j2
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..5627eb0
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,17 @@
+attrs==22.2.0
+beautifulsoup4==4.12.3
+certifi==2024.6.2
+charset-normalizer==3.3.2
+diskcache==5.6.3
+docopt==0.6.2
+funcy==2.0
+future==0.18.3
+idna==3.7
+Jinja2==3.1.4
+MarkupSafe==2.1.5
+mqttwarn==0.35.0
+paho-mqtt==1.6.1
+requests==2.32.3
+six==1.16.0
+soupsieve==2.5
+urllib3==2.2.1
diff --git a/sspmon.py b/sspmon.py
new file mode 100644
index 0000000..29e91e6
--- /dev/null
+++ b/sspmon.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+
+import requests, json
+
+from diskcache import Index
+from bs4 import BeautifulSoup
+from unicodedata import normalize
+import pprint
+import paho.mqtt.publish as publish
+
+DEBUG = False
+
+site = 'https://www.nhsbsa.nhs.uk'
+base_url = f"{site}/pharmacies-gp-practices-and-appliance-contractors/serious-shortage-protocols-ssps"
+active_ssps = Index('ssps/active')
+expired_ssps = Index('ssps/expired')
+
+def extract_table(table, ssp_list):
+ new_ssps = []
+ for row in table.tbody.find_all('tr'):
+ columns = row.find_all('td')
+ if(columns != []):
+ # SSP Name/Ref
+ ssp_name = normalize('NFKD',columns[0].text.strip())
+ ssp_link = normalize('NFKD',f"{site}{columns[0].find('a').get('href')}")
+ # Start and End date
+ dates = normalize('NFKD',columns[1].text.strip())
+ ds = dates.split('\n')[0].split('to')
+ #print(f"Splitting dates: [{dates}] | ds len: {len(ds)}")
+ start_date = ds[0].strip()
+ end_date = ds[1].strip()
+ # Guidance
+ guidance_name = normalize('NFKD',columns[2].text.strip())
+ guidance_link = normalize('NFKD',f"{site}{columns[2].find('a').get('href')}")
+ support = columns[2].text.strip()
+ item = {
+ 'name': ssp_name,
+ 'url': ssp_link,
+ 'start_date': start_date,
+ 'end_date': end_date,
+ 'guidance': guidance_name,
+ 'guidance_url': guidance_link,
+ }
+ if not ssp_link in ssp_list:
+ ssp_list[ssp_link] = item
+ new_ssps.append(item)
+ #print(item)
+ return new_ssps
+
+
+
+
+# Only ever one request so no hassles here
+request = requests.get(base_url, headers = {'User-agent': 'friendly_python'})
+
+data = request.text
+soup = BeautifulSoup(data, 'html.parser')
+tables = soup.find_all('table')
+
+actives = extract_table(tables[0], active_ssps)
+if len(actives) > 0:
+ print("New Active SSPs")
+ for i in actives:
+ publish.single("sspmon/ssps/active", json.dumps(i), hostname="192.168.1.3")
+ pprint.pp(i)
+
+expireds = extract_table(tables[1], expired_ssps)
+if len(expireds) > 0:
+ print("Newly Expired SSPs")
+ for i in expireds:
+ publish.single("sspmon/ssps/expired", json.dumps(i), hostname="192.168.1.3")
+ pprint.pp(i)
diff --git a/templates/active.j2 b/templates/active.j2
new file mode 100644
index 0000000..83d525a
--- /dev/null
+++ b/templates/active.j2
@@ -0,0 +1,8 @@
+A new SSP Notification has been published.
+
+An SSP is in effect from {{start_date}} to {{end_date}} for {{name}}. See {{url}} for info.
+
+Supporting guidance for {{guidance}} can be found at:
+
+{{guidance_url}}
+
diff --git a/udf.py b/udf.py
new file mode 100644
index 0000000..45a8540
--- /dev/null
+++ b/udf.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+# mqttwarn example function extensions
+import time
+import copy
+
+try:
+ import json
+except ImportError:
+ import simplejson as json # type: ignore[no-redef]
+
+def OwnTracksTopic2Data(topic):
+ try:
+ # owntracks/username/device
+ parts = topic.split('/')
+ username = parts[1]
+ deviceid = parts[2]
+ except:
+ deviceid = 'unknown'
+ username = 'unknown'
+ return dict(username=username, device=deviceid)
+
+def OwnTracksConvert(data):
+ if type(data) == dict:
+ # Better safe than sorry: Clone transformation dictionary to prevent leaking local modifications
+ # See also https://github.com/jpmens/mqttwarn/issues/219#issuecomment-271815495
+ data = copy.copy(data)
+ tst = data.get('tst', int(time.time()))
+ data['tst'] = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(int(tst)))
+ # Remove these elements to eliminate warnings
+ for k in ['_type', 'desc']:
+ data.pop(k, None)
+
+ return "{username} {device} {tst} at location {lat},{lon}".format(**data)
+
+# custom function to filter out any OwnTracks notifications which do
+# not contain the 'batt' parameter
+def OwnTracksBattFilter(topic, message):
+ data = dict(json.loads(message).items())
+ if 'batt' in data:
+ if data['batt'] is not None:
+ return int(data['batt']) > 20
+
+ return True # Suppress message because no 'batt'
+
+def TopicTargetList(topic=None, data=None, srv=None):
+ """
+ Custom function to compute list of topic targets based on MQTT topic and/or transformation data.
+ Obtains MQTT topic, transformation data and service object.
+ Returns list of topic target identifiers.
+ """
+
+ # optional debug logger
+ if srv is not None:
+ srv.logging.debug('topic={topic}, data={data}, srv={srv}'.format(**locals()))
+
+ # Use a fixed list of topic targets for demonstration purposes.
+ targets = ['log:debug']
+
+ # In the real world, you would compute proper topic targets based on information
+ # derived from transformation data, which in turn might have been enriched
+ # by ``datamap`` or ``alldata`` transformation functions before, like that:
+ if 'condition' in data:
+
+ if data['condition'] == 'sunny':
+ targets.append('file:mqttwarn')
+
+ elif data['condition'] == 'rainy':
+ targets.append('log:warn')
+
+ return targets
+
+def publish_public_ip_address(srv=None):
+ """
+ Custom function used as a periodic task for publishing your public ip address to the MQTT bus.
+ Obtains service object.
+ Returns None.
+ """
+
+ import socket
+ import requests
+
+ hostname = socket.gethostname()
+ ip_address = requests.get('https://httpbin.org/ip').json().get('origin')
+
+ if srv is not None:
+
+ # optional debug logger
+ srv.logging.debug('Publishing public ip address "{ip_address}" of host "{hostname}"'.format(**locals()))
+
+ # publish ip address to mqtt
+ srv.mqttc.publish('test/ip/{hostname}'.format(**locals()), ip_address)
+