diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3b70711 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.7-alpine + +RUN apk add --no-cache gcc musl-dev + +WORKDIR /usr/src/app + +COPY requirements.txt ./ +RUN pip install -r requirements.txt --force-reinstall --no-cache-dir + +COPY ./entrypoint.sh . + +COPY . . + +RUN ["chmod", "+x", "./entrypoint.sh"] + +RUN python setup.py install + +CMD [ "./entrypoint.sh" ] \ No newline at end of file diff --git a/README.rst b/README.rst index 074f0c4..64526ae 100644 --- a/README.rst +++ b/README.rst @@ -41,6 +41,20 @@ Print help -j, --json Json output -t TIMEOUT, --timeout TIMEOUT Request timeout + +MQTT_deamon +####### +:: +cp config.yaml.sample config.yaml +:: +docker run -e PYEBOX_MYACCOUNT=*** -e PYEBOX_MYPASSWORD=*** -e PYEBOX_OUTPUT=MQTT -e MQTT_USERNAME=mqtt_username -e MQTT_PASSWORD=mqtt_password -e MQTT_HOST=mqtt_ip -e MQTT_PORT=mqtt_port -e ROOT_TOPIC=homeassistant -e MQTT_NAME=ebox pyebox + +Docker +####### +:: +docker build -t pyebox . +:: +docker run -e PYEBOX_MYACCOUNT=*** -e PYEBOX_MYPASSWORD=*** pyebox Dev env ####### diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..692d08b --- /dev/null +++ b/config.yaml @@ -0,0 +1,8 @@ +# THIS YAML CAN CHANGE IN THE FUTURE +timeout: 30 +# If frequency is not set the "daemon" will collect the data only one time and stop +# 24 hours +frequency: 86400 +accounts: +- username: USERNAME + password: PASSWORD diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..90db2b5 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/sh +set -e + +# Check user and password +if [ -z "$PYEBOX_MYACCOUNT" ] || [ -z "$PYEBOX_MYPASSWORD" ] && [ "$PYEBOX_OUTPUT" != "MQTT" ] +then + echo 'Error: No user or password. Set both environnement variables PYEBOX_MYACCOUNT and PYEBOX_MYPASSWORD or PYEBOX_OUTPUT=MQTT' + exit 1 +fi + +# Config +if [ -z "$CONFIG" ] +then + export CONFIG="/usr/src/app/config.yaml" +fi + +if [ "$PYEBOX_OUTPUT" == "MQTT" ] +then + mqtt_pyebox +else + pyebox -u $PYEBOX_MYACCOUNT -p $PYEBOX_MYPASSWORD +fi + diff --git a/pyebox/__main__.py b/pyebox/__main__.py index a1dc912..d360e7d 100644 --- a/pyebox/__main__.py +++ b/pyebox/__main__.py @@ -4,6 +4,7 @@ import sys from pyebox import EboxClient, REQUESTS_TIMEOUT, PyEboxError +from pyebox.mqtt_daemon import MqttEbox def _format_output(account, data): @@ -39,7 +40,7 @@ def _format_output(account, data): Limit: {d[limit]:.2f} Gb """) print(output.format(d=data)) - + def main(): """Main function""" @@ -72,6 +73,10 @@ def main(): _format_output(args.username, client.get_data()) client.close_session() +def mqtt_daemon(): + """Entrypoint function.""" + dev = MqttEbox() + asyncio.run(dev.async_run()) if __name__ == '__main__': sys.exit(main()) diff --git a/pyebox/client.py b/pyebox/client.py index 349c2a2..59b8914 100644 --- a/pyebox/client.py +++ b/pyebox/client.py @@ -189,7 +189,7 @@ def get_data(self): """Return collected data""" return self._data - def close_session(self): + async def close_session(self): """Close current session.""" if not self._session.closed: if self._session._connector_owner: diff --git a/pyebox/mqtt_daemon.py b/pyebox/mqtt_daemon.py new file mode 100644 index 0000000..8d71e7d --- /dev/null +++ b/pyebox/mqtt_daemon.py @@ -0,0 +1,150 @@ +"""MQTT Daemon which collected Ebox Data. +And send it to MQTT using Home-Assistant format. +""" +import asyncio +import json +import os +import uuid + +from yaml import load +try: + from yaml import CLoader as Loader +except ImportError: + from yaml import Loader +import mqtt_hass_base + +from pyebox.client import EboxClient, USAGE_MAP + +def get_mac(): + """Get mac address.""" + mac_addr = (':'.join(['{:02x}'.format((uuid.getnode() >> ele) & 0xff) + for ele in range(0, 8 * 6, 8)][::-1])) + return mac_addr + + +class MqttEbox(mqtt_hass_base.MqttDevice): + """MQTT MqttEbox.""" + + timeout = None + frequency = None + + def __init__(self): + """Create new MqttEbox Object.""" + mqtt_hass_base.MqttDevice.__init__(self, "mqtt-ebox") + + def read_config(self): + """Read config from yaml file.""" + with open(os.environ['CONFIG']) as fhc: + self.config = load(fhc, Loader=Loader) + self.timeout = self.config.get('timeout', 30) + # 6 hours + self.frequency = self.config.get('frequency', None) + + async def _init_main_loop(self): + """Init before starting main loop.""" + + def _publish_sensor(self, sensor_type, contract_id, + unit=None, device_class=None, icon=None): + """Publish a Home-Assistant MQTT sensor.""" + mac_addr = get_mac() + + base_topic = ("{}/sensor/ebox_{}".format(self.mqtt_root_topic, + contract_id)) + + sensor_config = {} + sensor_config["device"] = {"connections": [["mac", mac_addr]], + "name": "ebox_{}".format(contract_id), + "identifiers": ['ebox', contract_id], + "manufacturer": "mqtt-ebox", + "sw_version": "0"} + + sensor_state_config = "{}/{}/state".format(base_topic, sensor_type) + sensor_config.update({ + "state_topic": sensor_state_config, + "name": "ebox_{}_{}".format(contract_id, sensor_type), + "unique_id": "{}_{}".format(contract_id, sensor_type), + "force_update": True, + "expire_after": 0, + }) + + sensor_config_topic = "{}/{}/config".format(base_topic, sensor_type) + + self.mqtt_client.publish(topic=sensor_config_topic, + retain=True, + payload=json.dumps(sensor_config)) + + return sensor_state_config + + async def _main_loop(self): + """Run main loop.""" + self.logger.debug("Get Data") + for account in self.config['accounts']: + client = EboxClient(account['username'], + account['password'], + self.timeout) + await client.fetch_data() + fetched_data = client.get_data() + + # Balance + # Publish sensor + balance_topic = self._publish_sensor('balance', account['username'], + unit="$", device_class=None, + icon="mdi:currency-usd") + # Send sensor data + self.mqtt_client.publish( + topic=balance_topic, + payload=fetched_data['balance']) + + # Usage + # Publish sensor + usage_topic = self._publish_sensor('usage', account['username'], + unit="%", device_class=None, + icon="mdi:currency-usd") + # Send sensor data + self.mqtt_client.publish( + topic=usage_topic, + payload=fetched_data['usage']) + + # Before offpeak and offpeak data + for data_name in USAGE_MAP: + # Publish sensor + sensor_topic = self._publish_sensor(data_name, + account['username'], + unit='Gb', + icon=None, + device_class=None) + # Send sensor data + self.mqtt_client.publish( + topic=sensor_topic, + payload=fetched_data[data_name]) + + await client.close_session() + + if self.frequency is None: + self.logger.info("Frequency is None, so it's a one shot run") + self.must_run = False + return + + self.logger.info("Waiting for %d seconds before the next check", self.frequency) + i = 0 + while i < self.frequency and self.must_run: + await asyncio.sleep(1) + i += 1 + + def _on_connect(self, client, userdata, flags, rc): + """On connect callback method.""" + + def _on_publish(self, client, userdata, mid): + """MQTT on publish callback.""" + + def _mqtt_subscribe(self, client, userdata, flags, rc): + """Subscribe to all needed MQTT topic.""" + + def _on_message(self, client, userdata, msg): + """MQTT on message callback.""" + + def _signal_handler(self, signal_, frame): + """Handle SIGKILL.""" + + async def _loop_stopped(self): + """Run after the end of the main loop.""" diff --git a/requirements.txt b/requirements.txt index 39529a1..db88a18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ aiohttp bs4 +mqtt-hass-base==0.1.4 +PyYAML==5.1.2 diff --git a/setup.py b/setup.py index 1b0d3a6..1475a47 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,8 @@ packages=['pyebox'], entry_points={ 'console_scripts': [ - 'pyebox = pyebox.__main__:main' + 'pyebox = pyebox.__main__:main', + 'mqtt_pyebox = pyebox.__main__:mqtt_daemon' ] }, license='Apache 2.0',