├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── aaisp-to-mqtt.py ├── docs ├── home-assistant-panel.png ├── home-assistant-quota-graph.png └── workflow.png └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | venv/* 2 | *.pyc 3 | config 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.6 2 | MAINTAINER Nat Morris 3 | 4 | COPY requirements.txt /app/ 5 | COPY aaisp-to-mqtt.py /app/ 6 | WORKDIR /app 7 | 8 | RUN apk add --no-cache \ 9 | python \ 10 | ca-certificates \ 11 | && apk add --no-cache --virtual .build-deps \ 12 | py-pip \ 13 | && pip install -r requirements.txt \ 14 | && apk del --no-cache .build-deps \ 15 | && addgroup -g 1000 aaisp \ 16 | && adduser -u 1000 -G aaisp -s /bin/sh -D aaisp \ 17 | && chown aaisp:aaisp -R /app \ 18 | && echo "0 * * * * /usr/bin/python /app/aaisp-to-mqtt.py /app/config.cfg" | crontab -u aaisp - 19 | 20 | CMD ["/usr/sbin/crond", "-f", "-d", "8"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Nat Morris 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AAISP to MQTT Service # 2 | 3 | A script to publish [Andrews & Arnold / AAISP](http://aa.net.uk) broadband quota and sync rates to [MQTT](http://mqtt.org/). 4 | 5 | It uses version 2 of AAISPs [CHAOS](https://support.aa.net.uk/CHAOS) API. 6 | 7 | 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/). 8 | 9 | ![Workflow](https://raw.github.com/natm/aaisp-to-mqtt/master/docs/workflow.png) 10 | 11 | ## Use cases ## 12 | 13 | * Displaying line properties in Home Assistant / openHAB 14 | * Asking Amazon Alexa Echo for the remaining quota 15 | * Flashing a light in the office when the downstream sync rate drops 16 | * Sending line info to [Crouton](https://github.com/edfungus/Crouton) 17 | 18 | Example showing lines in Home Assistant... 19 | 20 | ![Screenshot](https://raw.github.com/natm/aaisp-to-mqtt/master/docs/home-assistant-panel.png) 21 | 22 | ![Screenshot](https://raw.github.com/natm/aaisp-to-mqtt/master/docs/home-assistant-quota-graph.png) 23 | 24 | ## Configuration ## 25 | 26 | Create a config file, for example in /etc/aaisp-mqtt.conf, minimal viable with no MQTT authentication: 27 | 28 | ``` 29 | [aaisp] 30 | username = aa@1 31 | password = LongAccountPassword 32 | 33 | [mqtt] 34 | broker = 127.0.0.1 35 | port = 1883 36 | topic_prefix = aaisp 37 | ``` 38 | 39 | You can also optionally specify MQTT username and password: 40 | 41 | ``` 42 | [aaisp] 43 | username = aa@1 44 | password = LongAccountPassword 45 | 46 | [mqtt] 47 | broker = 127.0.0.1 48 | port = 1883 49 | topic_prefix = aaisp 50 | username = aaisp-service 51 | password = AnotherLongPassword 52 | ``` 53 | 54 | Install the dependencies: 55 | 56 | ``` 57 | $ pip install -r requirements.txt 58 | ``` 59 | 60 | Run the service: 61 | 62 | ``` 63 | $ aaisp-to-mqtt.py /etc/aaisp-mqtt.conf 64 | ``` 65 | 66 | It will display debug output similar to: 67 | 68 | ``` 69 | INFO [2016-11-16 01:24:07,069] Connecting to AAISP CHAOSv2 endpoint 70 | INFO [2016-11-16 01:24:07,338] Got 3 circuits 71 | INFO [2016-11-16 01:24:07,338] * Lines: 32891, 37835, 37964 72 | INFO [2016-11-16 01:24:07,338] * Logins: gb12@a.1, el6@a.1, el6@a.2 73 | INFO [2016-11-16 01:24:07,339] Connecting to MQTT broker mqtt.gorras.hw.esgob.com:1883 74 | INFO [2016-11-16 01:24:07,345] Connected OK to MQTT 75 | INFO [2016-11-16 01:24:07,346] Published version and index messages 76 | INFO [2016-11-16 01:24:07,350] Published details for 3 circuits 77 | INFO [2016-11-16 01:24:07,350] Disconnecting from MQTT 78 | ``` 79 | 80 | Schedule the script via a crontab to run every hour or 30 minutes. 81 | 82 | ## Topics ## 83 | 84 | Single account: 85 | 86 | ``` 87 | aaisp/$lines 32891 88 | aaisp/$logins gb12@a.1 89 | aaisp/$version 0.1 90 | aaisp/login/gb12@a.1/postcode SA65 9RR 91 | aaisp/login/gb12@a.1/quota/monthly 100000000000 92 | aaisp/login/gb12@a.1/quota/monthly/human 100 GB 93 | aaisp/login/gb12@a.1/quota/remaining 84667320096 94 | aaisp/login/gb12@a.1/quota/remaining/human 84.67 GB 95 | aaisp/login/gb12@a.1/syncrate/down 5181000 96 | aaisp/login/gb12@a.1/syncrate/down/human 5.18 MB 97 | aaisp/login/gb12@a.1/syncrate/up 1205000 98 | aaisp/login/gb12@a.1/syncrate/up/human 1.21 MB 99 | ``` 100 | 101 | For multiple accounts: 102 | 103 | ``` 104 | aaisp/$lines 32891,37835,37964 105 | aaisp/$logins gb12@a.1,el6@a.1,el6@a.2 106 | aaisp/$version 0.1 107 | aaisp/login/el6@a.1/postcode SA62 5EY 108 | aaisp/login/el6@a.1/quota/monthly 1000000000000 109 | aaisp/login/el6@a.1/quota/monthly/human 1 TB 110 | aaisp/login/el6@a.1/quota/remaining 752408843915 111 | aaisp/login/el6@a.1/quota/remaining/human 752.41 GB 112 | aaisp/login/el6@a.1/syncrate/down 68083000 113 | aaisp/login/el6@a.1/syncrate/down/human 68.08 MB 114 | aaisp/login/el6@a.1/syncrate/up 19999000 115 | aaisp/login/el6@a.1/syncrate/up/human 20 MB 116 | aaisp/login/el6@a.2/postcode SA62 5EY 117 | aaisp/login/el6@a.2/quota/monthly 1000000000000 118 | aaisp/login/el6@a.2/quota/monthly/human 1 TB 119 | aaisp/login/el6@a.2/quota/remaining 819343151266 120 | aaisp/login/el6@a.2/quota/remaining/human 819.34 GB 121 | aaisp/login/el6@a.2/syncrate/down 74425000 122 | aaisp/login/el6@a.2/syncrate/down/human 74.42 MB 123 | aaisp/login/el6@a.2/syncrate/up 19978000 124 | aaisp/login/el6@a.2/syncrate/up/human 19.98 MB 125 | aaisp/login/gb12@a.1/postcode SA65 9RR 126 | aaisp/login/gb12@a.1/quota/monthly 100000000000 127 | aaisp/login/gb12@a.1/quota/monthly/human 100 GB 128 | aaisp/login/gb12@a.1/quota/remaining 84667320096 129 | aaisp/login/gb12@a.1/quota/remaining/human 84.67 GB 130 | aaisp/login/gb12@a.1/syncrate/down 5181000 131 | aaisp/login/gb12@a.1/syncrate/down/human 5.18 MB 132 | aaisp/login/gb12@a.1/syncrate/up 1205000 133 | aaisp/login/gb12@a.1/syncrate/up/human 1.21 MB 134 | ``` 135 | 136 | ## Docker ## 137 | 138 | Build the Docker image with: 139 | 140 | ``` 141 | docker build -t aaisp-mqtt . 142 | ``` 143 | 144 | Run the container with a volume mounted config file: 145 | 146 | ``` 147 | docker run -d -v :/app/config.cfg --name AAISPmqtt aaisp-mqtt 148 | ``` 149 | 150 | ## Setup ## 151 | 152 | TODO 153 | 154 | ## License ## 155 | 156 | MIT 157 | 158 | ## Contributing guidelines ## 159 | 160 | * Fork the repo 161 | * Create a branch 162 | * Make your changes 163 | * Open a pull request back from your branch to master in this repo 164 | 165 | Found a bug? open an [issue](https://github.com/natm/aaisp-to-mqtt/issues). 166 | -------------------------------------------------------------------------------- /aaisp-to-mqtt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import logging 5 | import json 6 | import urllib 7 | import time 8 | import configparser 9 | import paho.mqtt.client as mqtt 10 | import humanfriendly 11 | 12 | LOG = logging.getLogger(__name__) 13 | VERSION = 0.1 14 | 15 | 16 | def main(): 17 | logging.basicConfig(level=logging.INFO, format='%(levelname)8s [%(asctime)s] %(message)s') 18 | 19 | if len(sys.argv) != 2: 20 | LOG.fatal("Config file not supplied") 21 | sys.exit(1) 22 | cfgfile = sys.argv[1] 23 | # load the config 24 | config = configparser.ConfigParser() 25 | config.read(cfgfile) 26 | 27 | # check it has the correct sections 28 | for section in ["aaisp", "mqtt"]: 29 | if section not in config.sections(): 30 | LOG.fatal("%s section not found in config file %s", section, cfgfile) 31 | 32 | aaisp_username = config.get("aaisp", "username") 33 | aaisp_password = config.get("aaisp", "password") 34 | 35 | # attempt to get details from aaisp 36 | LOG.info("Connecting to AAISP CHAOSv2 endpoint") 37 | post_params = "control_login=%s&control_password=%s" % (aaisp_username, aaisp_password) 38 | url = "https://chaos2.aa.net.uk/broadband/info" 39 | response = urllib.urlopen(url, data=post_params) 40 | data = json.loads(response.read()) 41 | if "info" not in data: 42 | LOG.fatal("info section not found in AAISP CHAOSv2 response") 43 | sys.exit(1) 44 | circuits = data["info"] 45 | LOG.info("Got %s circuits", len(circuits)) 46 | if len(circuits) == 0: 47 | LOG.fatal("No circuits returned from AAISP CHAOSv2") 48 | 49 | # work out unique line IDs and logins 50 | logins = [] 51 | lines = [] 52 | for circuit in circuits: 53 | if circuit["login"] not in logins: 54 | logins.append(circuit["login"]) 55 | if circuit["ID"] not in lines: 56 | lines.append(circuit["ID"]) 57 | LOG.info("* Lines: %s", ', '.join(lines)) 58 | LOG.info("* Logins: %s", ', '.join(logins)) 59 | 60 | 61 | # get MQTT config 62 | mqtt_broker = config.get("mqtt", "broker") 63 | mqtt_port = int(config.get("mqtt", "port")) 64 | mqtt_username = config.get("mqtt", "username") 65 | mqtt_password = config.get("mqtt", "password") 66 | mqtt_topic_prefix = config.get("mqtt", "topic_prefix") 67 | # connect to the broker 68 | LOG.info("Connecting to MQTT broker %s:%s", mqtt_broker, mqtt_port) 69 | client = mqtt.Client() 70 | # do auth? 71 | if mqtt_username is not None and mqtt_password is not None: 72 | client.username_pw_set(mqtt_username, mqtt_password) 73 | client.max_inflight_messages_set(100) 74 | client.connect(mqtt_broker, mqtt_port, 60) 75 | LOG.info("Connected OK to MQTT") 76 | 77 | # version and indexes 78 | publish(client=client, topic="%s/$version" % (mqtt_topic_prefix), payload=VERSION) 79 | publish(client=client, topic="%s/$lines" % (mqtt_topic_prefix), payload=','.join(lines)) 80 | publish(client=client, topic="%s/$logins" % (mqtt_topic_prefix), payload=','.join(logins)) 81 | LOG.info("Published version and index messages") 82 | 83 | # publish per circuit 84 | for circuit in circuits: 85 | publish_per_circuit(client=client, circuit=circuit, mqtt_topic_prefix=mqtt_topic_prefix) 86 | LOG.info("Published details for %s circuits", len(circuits)) 87 | # disconnect 88 | LOG.info("Disconnecting from MQTT") 89 | client.disconnect() 90 | 91 | 92 | sys.exit(0) 93 | 94 | def publish_per_circuit(client, circuit, mqtt_topic_prefix): 95 | quota_remaining = int(circuit["quota_remaining"]) 96 | quota_remaining_gb = quota_remaining / 1000000000 97 | quota_monthly = int(circuit["quota_monthly"]) 98 | quota_monthly_gb = quota_monthly / 1000000000 99 | up = float(circuit["rx_rate"]) 100 | up_mb = round(up / 1000000, 2) 101 | down = float(circuit["tx_rate"]) 102 | down_mb = round(down / 1000000, 2) 103 | 104 | # line_prefix = "%s/line/%s" % (mqtt_topic_prefix, circuit["ID"]) 105 | login_prefix = "%s/login/%s" % (mqtt_topic_prefix, circuit["login"]) 106 | for prefix in [login_prefix]: # , line_prefix]: 107 | for metric in [ 108 | ("quota/remaining", quota_remaining), 109 | ("quota/remaining/gb", quota_remaining_gb), 110 | ("quota/remaining/human", humanfriendly.format_size(quota_remaining)), 111 | ("quota/monthly", quota_monthly), 112 | ("quota/monthly/gb", quota_monthly_gb), 113 | ("quota/monthly/human", humanfriendly.format_size(quota_monthly)), 114 | ("syncrate/up", up), 115 | ("syncrate/up/mb", up_mb), 116 | ("syncrate/up/human", humanfriendly.format_size(up)), 117 | ("syncrate/down", down), 118 | ("syncrate/down/mb", down_mb), 119 | ("syncrate/down/human", humanfriendly.format_size(down)), 120 | ("postcode", str(circuit["postcode"].strip())) 121 | ]: 122 | topic = "%s/%s" % (prefix, metric[0]) 123 | publish(client=client, topic=topic, payload=metric[1]) 124 | return 125 | 126 | def publish(client, topic, payload): 127 | time.sleep(0.1) 128 | result = client.publish(topic=topic, payload=payload, qos=1) 129 | if result[0] != 0: 130 | LOG.fail("MQTT publish failure: %s %s" , topic, payload) 131 | 132 | if __name__ == "__main__": 133 | main() 134 | -------------------------------------------------------------------------------- /docs/home-assistant-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natm/aaisp-to-mqtt/193ab5fb57404830f9278b1cfd8de0eed49817d8/docs/home-assistant-panel.png -------------------------------------------------------------------------------- /docs/home-assistant-quota-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natm/aaisp-to-mqtt/193ab5fb57404830f9278b1cfd8de0eed49817d8/docs/home-assistant-quota-graph.png -------------------------------------------------------------------------------- /docs/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natm/aaisp-to-mqtt/193ab5fb57404830f9278b1cfd8de0eed49817d8/docs/workflow.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | paho-mqtt>=1.2 2 | configparser>=3.5.0 3 | humanfriendly>=2.1 4 | --------------------------------------------------------------------------------