Added support for HA discovery

This commit is contained in:
2020-05-16 16:05:51 +01:00
parent cf021a393d
commit b34d2543e4
11 changed files with 273 additions and 191 deletions

View File

@@ -1,9 +1,9 @@
FROM python:3.8-alpine FROM python:3.8-alpine
COPY requirements.txt /app/ COPY requirements.txt /app/
COPY aaisp-to-mqtt.py /app/ COPY aaisp2mqtt.py /app/
WORKDIR /app WORKDIR /app
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
CMD ["python", "/app/aaisp-to-mqtt.py"] CMD ["python", "/app/aaisp2mqtt.py"]

View File

@@ -2,6 +2,7 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Nat Morris Copyright (c) 2016 Nat Morris
Copyright (c) 2020 Andrew Williams
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,12 +1,13 @@
# AAISP to MQTT Service # # AAISP to MQTT Service #
A script to publish [Andrews & Arnold / AAISP](http://aa.net.uk) broadband quota and sync rates to [MQTT](http://mqtt.org/). A script to publish [Andrews & Arnold / AAISP](http://aa.net.uk) broadband quota and sync rates to [MQTT](http://mqtt.org/). It uses version 2 of AAISPs [CHAOS](https://support.aa.net.uk/CHAOS) API. Useful for integrating and displaying AAISP line properties in home automation applications, such as [Home Assistant](https://home-assistant.io/) or [openHAB](http://www.openhab.org/).
It uses version 2 of AAISPs [CHAOS](https://support.aa.net.uk/CHAOS) API. This is a fork of the original [aaisp2mqtt project](https://github.com/natm/aaisp-to-mqtt) by [natm](http://github.com/natm) with the aim to increase integration with Home Assistant and fixing a few minor issues.
Useful for integrating and displaying AAISP line properties in home automation applications, such as [Home Assistant](https://home-assistant.io/) or [openHAB](http://www.openhab.org/). ## Features ##
* Home Assistant auto discovery
![Workflow](https://raw.github.com/natm/aaisp-to-mqtt/master/docs/workflow.png)
## Use cases ## ## Use cases ##
@@ -15,19 +16,13 @@ Useful for integrating and displaying AAISP line properties in home automation a
* Flashing a light in the office when the downstream sync rate drops * Flashing a light in the office when the downstream sync rate drops
* Sending line info to [Crouton](https://github.com/edfungus/Crouton) * Sending line info to [Crouton](https://github.com/edfungus/Crouton)
Example showing lines in Home Assistant...
![Screenshot](https://raw.github.com/natm/aaisp-to-mqtt/master/docs/home-assistant-panel.png)
![Screenshot](https://raw.github.com/natm/aaisp-to-mqtt/master/docs/home-assistant-quota-graph.png)
## Configuration ## ## Configuration ##
Create a config file, for example in /etc/aaisp-mqtt.conf, minimal viable with no MQTT authentication: Create a config file, for example in /etc/aaisp-mqtt.conf, minimal viable with no MQTT authentication:
``` ```
[aaisp] [aaisp]
username = aa@1 username = aa000@x.a
password = LongAccountPassword password = LongAccountPassword
[mqtt] [mqtt]
@@ -40,7 +35,7 @@ You can also optionally specify MQTT username and password:
``` ```
[aaisp] [aaisp]
username = aa@1 username = aa000@x.a
password = LongAccountPassword password = LongAccountPassword
[mqtt] [mqtt]
@@ -60,21 +55,21 @@ $ pip install -r requirements.txt
Run the service: Run the service:
``` ```
$ aaisp-to-mqtt.py /etc/aaisp-mqtt.conf $ aaisp2mqtt.py /etc/aaisp-mqtt.conf
``` ```
It will display debug output similar to: It will display debug output similar to:
``` ```
INFO [2016-11-16 01:24:07,069] Connecting to AAISP CHAOSv2 endpoint INFO [2020-05-16 14:49:05,142] Connecting to AAISP CHAOSv2 endpoint as xx000@x.a
INFO [2016-11-16 01:24:07,338] Got 3 circuits INFO [2020-05-16 14:49:06,002] Got 1 circuits
INFO [2016-11-16 01:24:07,338] * Lines: 32891, 37835, 37964 INFO [2020-05-16 14:49:06,003] * Lines: 41429
INFO [2016-11-16 01:24:07,338] * Logins: gb12@a.1, el6@a.1, el6@a.2 INFO [2020-05-16 14:49:06,004] * Logins: xx000@x.0
INFO [2016-11-16 01:24:07,339] Connecting to MQTT broker mqtt.gorras.hw.esgob.com:1883 INFO [2020-05-16 14:49:06,005] Connecting to MQTT broker 127.0.0.1:1883
INFO [2016-11-16 01:24:07,345] Connected OK to MQTT INFO [2020-05-16 14:49:06,016] Connected to MQTT Server 127.0.0.1
INFO [2016-11-16 01:24:07,346] Published version and index messages INFO [2020-05-16 14:49:06,023] Published version and index messages
INFO [2016-11-16 01:24:07,350] Published details for 3 circuits INFO [2020-05-16 14:49:06,031] Published details for 1 circuits
INFO [2016-11-16 01:24:07,350] Disconnecting from MQTT INFO [2020-05-16 14:49:06,033] Disconnecting from MQTT
``` ```
Schedule the script via a crontab to run every hour or 30 minutes. Schedule the script via a crontab to run every hour or 30 minutes.
@@ -158,6 +153,8 @@ Or you can pass the configuration values as environment variables:
* MQTT_USERNAME * MQTT_USERNAME
* MQTT_PASSWORD * MQTT_PASSWORD
* MQTT_TOPIC_PREFIX * MQTT_TOPIC_PREFIX
* HOMEASSISTANT_ENABLED
* HOMEASSISTANT_DISCOVERY_PREFIX
## Setup ## ## Setup ##
@@ -174,4 +171,4 @@ MIT
* Make your changes * Make your changes
* Open a pull request back from your branch to master in this repo * Open a pull request back from your branch to master in this repo
Found a bug? open an [issue](https://github.com/natm/aaisp-to-mqtt/issues). Found a bug? open an [issue](https://github.com/nikdoof/aaisp2mqtt/issues).

View File

@@ -1,165 +0,0 @@
#!/usr/bin/env python
import os
import sys
import logging
import json
import time
import configparser
import paho.mqtt.client as mqtt
import humanfriendly
import requests
import argparse
LOG = logging.getLogger(__name__)
VERSION = '0.2.3'
AAISP_INFO_URL = 'https://chaos2.aa.net.uk/broadband/info'
def main():
logging.basicConfig(level=logging.INFO,
format='%(levelname)8s [%(asctime)s] %(message)s')
if len(sys.argv) > 1:
cfgfile = sys.argv[1]
# load the config
config = configparser.ConfigParser()
config.read(cfgfile)
# check it has the correct sections
for section in ['aaisp', 'mqtt']:
if section not in config.sections():
LOG.fatal('%s section not found in config file %s',
section, cfgfile)
aaisp_username = config.get('aaisp', 'username')
aaisp_password = config.get('aaisp', 'password')
mqtt_broker = config.get('mqtt', 'broker')
mqtt_port = int(config.get('mqtt', 'port', fallback='1883'))
mqtt_username = config.get('mqtt', 'username', fallback=None)
mqtt_password = config.get('mqtt', 'password', fallback=None)
mqtt_topic_prefix = config.get('mqtt', 'topic_prefix', fallback='aaisp')
else:
# Use the environment
aaisp_username = os.environ.get('AAISP_USERNAME')
aaisp_password = os.environ.get('AAISP_PASSWORD')
mqtt_broker = os.environ.get('MQTT_BROKER') or 'localhost'
mqtt_port = int(os.environ.get('MQTT_PORT') or '1883')
mqtt_username = os.environ.get('MQTT_USERNAME')
mqtt_password = os.environ.get('MQTT_PASSWORD')
mqtt_topic_prefix = os.environ.get('MQTT_TOPIC_PREFIX') or 'aaisp'
if aaisp_username is None or aaisp_password is None:
LOG.fatal('Username or Password missing for AAISP')
return 1
# attempt to get details from aaisp
LOG.info('Connecting to AAISP CHAOSv2 endpoint as %s/%s', aaisp_username, '*' * len(aaisp_password))
response = requests.get(AAISP_INFO_URL, params={
'control_login': aaisp_username.encode('ascii'),
'control_password': aaisp_password.encode('ascii')
})
if not response.status_code == requests.codes.ok:
LOG.error('Error connecting to AAISP CHAOSv2 endpoint: %s' % response.body)
return 1
data = response.json()
# Check for response errors
if 'info' not in data:
if 'error' in data:
LOG.fatal('Error encounted: %s' % data['error'])
else:
LOG.fatal('info section not found in AAISP CHAOSv2 response')
return 1
circuits = data['info']
LOG.info('Got %s circuits', len(circuits))
if len(circuits) == 0:
LOG.fatal('No circuits returned from AAISP CHAOSv2')
# work out unique line IDs and logins
logins = set(c['login'] for c in circuits)
lines = set(c['ID'] for c in circuits)
LOG.info('* Lines: %s', ', '.join(lines))
LOG.info('* Logins: %s', ', '.join(logins))
# connect to the broker
LOG.info('Connecting to MQTT broker %s:%s', mqtt_broker, mqtt_port)
client = mqtt.Client()
client.max_inflight_messages_set(100)
# do auth?
if mqtt_username is not None and mqtt_password is not None:
client.username_pw_set(mqtt_username, mqtt_password)
try:
client.connect(mqtt_broker, mqtt_port, 60)
except Exception:
LOG.exception('Error connecting to MQTT')
return 1
else:
LOG.info('Connected to MQTT Server %s', mqtt_broker)
# version and indexes
publish(client=client, topic='%s/$version' %
(mqtt_topic_prefix), payload=VERSION)
publish(client=client, topic='%s/$lines' %
(mqtt_topic_prefix), payload=','.join(lines))
publish(client=client, topic='%s/$logins' %
(mqtt_topic_prefix), payload=','.join(logins))
LOG.info('Published version and index messages')
# publish per circuit
for circuit in circuits:
publish_per_circuit(client=client, circuit=circuit,
mqtt_topic_prefix=mqtt_topic_prefix)
LOG.info('Published details for %s circuits', len(circuits))
# disconnect
LOG.info('Disconnecting from MQTT')
client.disconnect()
def publish_per_circuit(client, circuit, mqtt_topic_prefix):
quota_remaining = int(circuit['quota_remaining'])
quota_remaining_gb = quota_remaining / 1000000000
quota_monthly = int(circuit['quota_monthly'])
quota_monthly_gb = quota_monthly / 1000000000
up = float(circuit['rx_rate'])
up_mb = round(up / 1000000, 2)
down = float(circuit['tx_rate'])
down_mb = round(down / 1000000, 2)
# line_prefix = '%s/line/%s' % (mqtt_topic_prefix, circuit['ID'])
login_prefix = '%s/login/%s' % (mqtt_topic_prefix, circuit['login'])
for prefix in [login_prefix]: # , line_prefix]:
for metric in [
('quota/remaining', quota_remaining),
('quota/remaining/gb', quota_remaining_gb),
('quota/remaining/human', humanfriendly.format_size(quota_remaining)),
('quota/monthly', quota_monthly),
('quota/monthly/gb', quota_monthly_gb),
('quota/monthly/human', humanfriendly.format_size(quota_monthly)),
('syncrate/up', up),
('syncrate/up/mb', up_mb),
('syncrate/up/human', humanfriendly.format_size(up)),
('syncrate/down', down),
('syncrate/down/mb', down_mb),
('syncrate/down/human', humanfriendly.format_size(down)),
('postcode', str(circuit['postcode'].strip()))
]:
topic = '%s/%s' % (prefix, metric[0])
publish(client=client, topic=topic, payload=metric[1])
return
def publish(client, topic, payload):
result = client.publish(topic=topic, payload=payload, qos=1)
if result[0] != 0:
LOG.fail('MQTT publish failure: %s %s', topic, payload)
if __name__ == '__main__':
sys.exit(main())

12
aaisp.cfg-example Normal file
View File

@@ -0,0 +1,12 @@
[aaisp]
username = xx000@x
password = PasswordPassword
[mqtt]
broker = localhost
port = 1883
topic_prefix = aaisp
[homeassistant]
enabled = false
discovery_prefix = homeassistant

203
aaisp2mqtt.py Normal file
View File

@@ -0,0 +1,203 @@
#!/usr/bin/env python
import os
import sys
import logging
import json
from datetime import datetime
import configparser
import paho.mqtt.client as mqtt
import humanfriendly
import requests
LOG = logging.getLogger(__name__)
VERSION = '0.3.0'
AAISP_INFO_URL = 'https://chaos2.aa.net.uk/broadband/info'
def b_to_gb(value):
"""Bytes to Gibibytes"""
return round(int(value) / (1000**3), 3)
def bps_to_mbps(value):
"""Bits per second to Mbit/sec"""
return round(int(value) / (1000**2), 3)
def to_human(value):
"""Human readable value"""
return humanfriendly.format_size(int(value))
# Name, Topic, Key, Formatter, HA Unit Type
VALUES_MAP = [
('quota_remaining', 'quota/remaining', 'quota_remaining', int, 'B', 'mdi:gauge'),
('quota_remaining_gb', 'quota/remaining/gb', 'quota_remaining', b_to_gb, 'GB', 'mdi:gauge'),
('quota_remaining_human', 'quota/remaining/human', 'quota_remaining', to_human, '', 'mdi:gauge'),
('quota_monthly', 'quota/monthly', 'quota_monthly', int, 'B', 'mdi:gauge'),
('quota_monthly_gb', 'quota/monthly/gb', 'quota_monthly', b_to_gb, 'GB', 'mdi:gauge'),
('quota_monthly_human', 'quota/monthly/human', 'quota_monthly', to_human, '', 'mdi:gauge'),
('syncrate_up', 'syncrate/up', 'rx_rate', float, 'bit/s', 'mdi:speedometer'),
('syncrate_up_mbps', 'syncrate/up/mb', 'rx_rate', bps_to_mbps, 'Mbit/s', 'mdi:speedometer'),
('syncrate_up_human', 'syncrate/up/human', 'rx_rate', to_human, '', 'mdi:speedometer'),
('syncrate_down', 'syncrate/down', 'tx_rate', float, 'bit/s', 'mdi:speedometer'),
('syncrate_down_mbps', 'syncrate/down/mb', 'tx_rate', bps_to_mbps, 'Mbit/s', 'mdi:speedometer'),
('syncrate_down_human', 'syncrate/down/human', 'tx_rate', to_human, '', 'mdi:speedometer'),
('postcode', 'postcode', 'postcode', str, '', 'mdi:tag-text'),
]
def main():
logging.basicConfig(level=logging.INFO,
format='%(levelname)8s [%(asctime)s] %(message)s')
if len(sys.argv) > 1:
filename = os.path.abspath(os.path.expandvars(sys.argv[1]))
# load the config
if os.path.exists(filename):
config = configparser.ConfigParser()
config.read(filename)
else:
LOG.fatal('Configuration file %s does not exist', filename)
# check it has the correct sections
diff = set(['aaisp', 'mqtt']) - set(config.sections())
if len(diff) > 0:
LOG.fatal('Sections are missing from the configuration file: %s', ','.join(diff))
aaisp_username = config.get('aaisp', 'username')
aaisp_password = config.get('aaisp', 'password')
mqtt_broker = config.get('mqtt', 'broker')
mqtt_port = int(config.get('mqtt', 'port', fallback='1883'))
mqtt_username = config.get('mqtt', 'username', fallback=None)
mqtt_password = config.get('mqtt', 'password', fallback=None)
mqtt_topic_prefix = config.get('mqtt', 'topic_prefix', fallback='aaisp')
homeassistant_enabled = config.get('homeassistant', 'enabled', fallback='false') == 'true'
homeassistant_discovery_prefix = config.get('homeassistant', 'discovery_prefix', fallback='homeassistant')
else:
# Use the environment
aaisp_username = os.environ.get('AAISP_USERNAME')
aaisp_password = os.environ.get('AAISP_PASSWORD')
mqtt_broker = os.environ.get('MQTT_BROKER') or 'localhost'
mqtt_port = int(os.environ.get('MQTT_PORT') or '1883')
mqtt_username = os.environ.get('MQTT_USERNAME')
mqtt_password = os.environ.get('MQTT_PASSWORD')
mqtt_topic_prefix = os.environ.get('MQTT_TOPIC_PREFIX') or 'aaisp'
homeassistant_enabled = (os.environ.get('HOMEASSISTANT_ENABLED') or 'false') == 'true'
homeassistant_discovery_prefix = os.environ.get('HOMEASSISTANT_DISCOVERY_PREFIX') or 'homeassistant'
if aaisp_username is None or aaisp_password is None:
LOG.fatal('Username or Password missing for AAISP')
return 1
# attempt to get details from aaisp
LOG.info('Connecting to AAISP CHAOSv2 endpoint as %s', aaisp_username)
response = requests.get(AAISP_INFO_URL, params={
'control_login': aaisp_username,
'control_password': aaisp_password
})
if not response.status_code == requests.codes.ok:
LOG.error('Error connecting to AAISP CHAOSv2 endpoint: %s' % response.body)
return 1
data = response.json()
# Check for response errors
if 'info' not in data:
if 'error' in data:
LOG.fatal('Error encounted: %s' % data['error'])
else:
LOG.fatal('info section not found in AAISP CHAOSv2 response')
return 1
circuits = data['info']
LOG.info('Got %s circuits', len(circuits))
if len(circuits) == 0:
LOG.fatal('No circuits returned from AAISP CHAOSv2')
# work out unique line IDs and logins
logins = set(c['login'] for c in circuits)
lines = set(c['ID'] for c in circuits)
LOG.info('* Lines: %s', ', '.join(lines))
LOG.info('* Logins: %s', ', '.join(logins))
# connect to the broker
LOG.info('Connecting to MQTT broker %s:%s', mqtt_broker, mqtt_port)
client = mqtt.Client()
client.max_inflight_messages_set(100)
# do auth?
if mqtt_username is not None and mqtt_password is not None:
client.username_pw_set(mqtt_username, mqtt_password)
try:
client.connect(mqtt_broker, mqtt_port, 60)
except Exception:
LOG.exception('Error connecting to MQTT')
return 1
else:
LOG.info('Connected to MQTT Server %s', mqtt_broker)
# version and indexes
publish(client=client, topic='%s/$version' % (mqtt_topic_prefix), payload=VERSION)
publish(client=client, topic='%s/$lines' % (mqtt_topic_prefix), payload=','.join(lines))
publish(client=client, topic='%s/$logins' % (mqtt_topic_prefix), payload=','.join(logins))
publish(client=client, topic='%s/last_update' % (mqtt_topic_prefix), payload=datetime.now().timestamp())
LOG.info('Published version and index messages')
# publish per circuit
for circuit in circuits:
# If homeassistant is enabled, publish the sensor configs
if homeassistant_enabled:
LOG.debug('Publishing Homeassistant configuration.')
publish_circuit_config(client, circuit, mqtt_topic_prefix, homeassistant_discovery_prefix)
# Publish the states
publish_circuit_state(client, circuit, mqtt_topic_prefix)
LOG.info('Published details for %s circuits', len(circuits))
# disconnect
LOG.info('Disconnecting from MQTT')
client.disconnect()
def publish_circuit_state(client, circuit, mqtt_topic_prefix):
prefix = '%s/login/%s' % (mqtt_topic_prefix, circuit['login'])
for _, topic, key, formatter, _, _ in VALUES_MAP:
topic = '%s/%s' % (prefix, topic)
publish(client=client, topic=topic, payload=formatter(circuit[key]))
def publish_circuit_config(client, circuit, mqtt_topic_prefix, mqtt_discovery_prefix):
for name, topic, _, _, unit, icon in VALUES_MAP:
login = circuit['login'].replace('@', '_').replace('.', '_')
config_topic = '%s/sensor/%s/%s/config' % (mqtt_discovery_prefix, login, name)
unique_id = '%s_%s' % (login, name)
data = {
'name': '%s %s' % (circuit['login'], name),
'icon': icon,
'state_topic': '%s/login/%s/%s' % (mqtt_topic_prefix, circuit['login'], topic),
'unique_id': unique_id,
'unit_of_measurement': unit,
'device': {
'identifiers': circuit['login'],
'name': 'AAISP Circuit %s (%s)' % (circuit['login'], circuit['postcode']),
'sw_version': VERSION,
}
}
publish(client, config_topic, payload=json.dumps(data), retain=True)
def publish(client, topic, payload, retain=False):
result = client.publish(topic=topic, payload=payload, qos=0, retain=retain)
if result[0] != 0:
LOG.fail('MQTT publish failure: %s %s', topic, payload)
if __name__ == '__main__':
sys.exit(main())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -1,4 +1,3 @@
paho-mqtt>=1.2 paho-mqtt>=1.2
configparser>=3.5.0
humanfriendly>=2.1 humanfriendly>=2.1
requests>=2.23.0 requests>=2.23.0

35
setup.py Normal file
View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
with open("README.md", "r") as fh:
long_description = fh.read()
setup(
name='aaisp2mqtt',
version='0.3.0',
description='A script to publish Andrews & Arnold / AAISP broadband quota and sync rates to MQTT',
long_description=long_description,
long_description_content_type='text/markdown',
license='MIT',
author='Nat Morris, Andrew Williams',
author_email='nat@nuqe.net, andy@tensixtyone.com',
url='https://github.com/nikdoof/aaisp2mqtt',
packages=find_packages(),
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3',
],
python_requires='>=3.7',
entry_points={
'console_scripts': [
'aaisp2mqtt = aaisp2mqtt:main',
],
},
install_requires=[
'paho-mqtt>=1.2',
'humanfriendly>=2.1',
'requests>=2.23.0',
]
)