diff options
| author | steve <steve@haxors.club> | 2024-07-06 16:29:00 +0100 |
|---|---|---|
| committer | steve <steve@haxors.club> | 2024-07-06 16:29:00 +0100 |
| commit | c48a265bb82fde1403a757ecc4e2daed909dbb9c (patch) | |
| tree | 0722acd71023252c313976dc4f857b6127c58cfc | |
| download | sspmon-master.tar.gz sspmon-master.tar.bz2 sspmon-master.zip | |
| -rw-r--r-- | mqttwarn.ini | 185 | ||||
| -rw-r--r-- | requirements.txt | 17 | ||||
| -rw-r--r-- | sspmon.py | 72 | ||||
| -rw-r--r-- | templates/active.j2 | 8 | ||||
| -rw-r--r-- | udf.py | 92 |
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}} + @@ -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) + |
