From ba8356e5aa2fa99a2c0de3e20ebaf0681f5100c7 Mon Sep 17 00:00:00 2001 From: h2zero <32826625+h2zero@users.noreply.github.com> Date: Wed, 25 May 2022 18:12:10 -0600 Subject: [PATCH] Add Home Assistant auto discovery option. (#30) * Add Home Assistant auto discovery option. Co-authored-by: Tenn0 * Use wildcards in discovery state topic. * Remove check of property in discovery config Co-authored-by: Tenn0 --- TheengsGateway/__init__.py | 37 +++++++++- TheengsGateway/ble_gateway.py | 40 ++++++---- TheengsGateway/discovery.py | 133 ++++++++++++++++++++++++++++++++++ docs/use/use.md | 21 +++++- 4 files changed, 215 insertions(+), 16 deletions(-) create mode 100644 TheengsGateway/discovery.py diff --git a/TheengsGateway/__init__.py b/TheengsGateway/__init__.py index 43f0fbce..6da3f68f 100644 --- a/TheengsGateway/__init__.py +++ b/TheengsGateway/__init__.py @@ -34,7 +34,11 @@ "ble_time_between_scans":5, "publish_topic": "home/TheengsGateway/BTtoMQTT", "subscribe_topic": "home/TheengsGateway/+", - "log_level": "WARNING" + "log_level": "WARNING", + "discovery": 1, + "discovery_topic": "homeassistant/sensor", + "discovery_device_name": "TheengsGateway", + "discovery_filter": ["IBEACON", "GAEN", "MS-CDP"] } conf_path = os.path.expanduser('~') + '/theengsgw.conf' @@ -51,6 +55,11 @@ parser.add_argument('-tb', '--time_between', dest='time_between', type=int, help="Seconds to wait between scans") parser.add_argument('-ll', '--log_level', dest='log_level', type=str, help="TheengsGateway log level", choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']) +parser.add_argument('-Dt', '--discovery-topic', dest='discovery_topic', type=str, help="MQTT Discovery topic") +parser.add_argument('-D', '--discovery', dest='discovery', type=int, help="Enable(1) or disable(0) MQTT discovery") +parser.add_argument('-Dn', '--discovery_name', dest='discovery_device_name', type=str, help="Device name for Home Assistant") +parser.add_argument('-Df', '--discovery_filter', dest='discovery_filter', nargs='+', default=[], + help="Device discovery filter list for Home Assistant") args = parser.parse_args() try: @@ -80,6 +89,32 @@ if args.log_level: config['log_level'] = args.log_level +if args.discovery is not None: + config['discovery'] = args.discovery +elif not 'discovery' in config.keys(): + config['discovery'] = default_config['discovery'] + config['discovery_topic'] = default_config['discovery_topic'] + config['discovery_device_name'] = default_config['discovery_device_name'] + config['discovery_filter'] = default_config['discovery_filter'] + +if args.discovery_topic: + config['discovery_topic'] = args.discovery_topic +elif not 'discovery_topic' in config.keys(): + config['discovery_topic'] = default_config['discovery_topic'] + +if args.discovery_device_name: + config['discovery_device_name'] = args.discovery_device_name +elif not 'discovery_device_name' in config.keys(): + config['discovery_device_name'] = default_config['discovery_device_name'] + +if args.discovery_filter: + config['discovery_filter'] = default_config['discovery_filter'] + if args.discovery_filter[0] != "reset": + for item in args.discovery_filter: + config['discovery_filter'].append(item) +elif not 'discovery_filter' in config.keys(): + config['discovery_filter'] = default_config['discovery_filter'] + if not config['host']: sys.exit('Invalid MQTT host') diff --git a/TheengsGateway/ble_gateway.py b/TheengsGateway/ble_gateway.py index 5098f0ff..aea32f18 100644 --- a/TheengsGateway/ble_gateway.py +++ b/TheengsGateway/ble_gateway.py @@ -73,7 +73,10 @@ def on_message(client_, userdata, msg): address = msg_json["id"] decoded_json = decodeBLE(json.dumps(msg_json)) if decoded_json: - gw.publish(decoded_json, gw.pub_topic + '/' + address.replace(':', '')) + if gw.discovery: + gw.publish_device_info(json.loads(decoded_json)) ## publish sensor data to home assistant mqtt discovery + else: + gw.publish(decoded_json, gw.pub_topic + '/' + address.replace(':', '')) elif gw.publish_all: gw.publish(str(msg.payload.decode()), gw.pub_topic + '/' + address.replace(':', '')) @@ -138,29 +141,22 @@ def detection_callback(device, advertisement_data): decoded_json = decodeBLE(json.dumps(data_json)) if decoded_json: - gw.publish(decoded_json, gw.pub_topic + '/' + device.address.replace(':', '')) + if gw.discovery: + gw.publish_device_info(json.loads(decoded_json)) ## publish sensor data to home assistant mqtt discovery + else: + gw.publish(decoded_json, gw.pub_topic + '/' + device.address.replace(':', '')) elif gw.publish_all: gw.publish(json.dumps(data_json), gw.pub_topic + '/' + device.address.replace(':', '')) def run(arg): global gw + try: with open(arg) as config_file: config = json.load(config_file) except: raise SystemExit(f"Invalid File: {sys.argv[1]}") - try: - gw = gateway(config["host"], int(config["port"]), config["user"], config["pass"]) - except: - raise SystemExit(f"Missing or invalid MQTT host parameters") - - gw.scan_time = config.get("ble_scan_time", 5) - gw.time_between_scans = config.get("ble_time_between_scans", 0) - gw.sub_topic = config.get("subscribe_topic", "gateway_sub") - gw.pub_topic = config.get("publish_topic", "gateway_pub") - gw.publish_all = config.get("publish_all", False) - log_level = config.get("log_level", "WARNING").upper() if log_level == "DEBUG": log_level = logging.DEBUG @@ -175,6 +171,24 @@ def run(arg): else: log_level = logging.WARNING + if config['discovery']: + from .discovery import discovery + gw = discovery(config["host"], int(config["port"]), config["user"], + config["pass"], config['discovery_topic'], + config['discovery_device_name'], config['discovery_filter']) + else: + try: + gw = gateway(config["host"], int(config["port"]), config["user"], config["pass"]) + except: + raise SystemExit(f"Missing or invalid MQTT host parameters") + + gw.discovery = config['discovery'] + gw.scan_time = config.get("ble_scan_time", 5) + gw.time_between_scans = config.get("ble_time_between_scans", 0) + gw.sub_topic = config.get("subscribe_topic", "gateway_sub") + gw.pub_topic = config.get("publish_topic", "gateway_pub") + gw.publish_all = config.get("publish_all", False) + logging.basicConfig() logger.setLevel(log_level) diff --git a/TheengsGateway/discovery.py b/TheengsGateway/discovery.py new file mode 100644 index 00000000..b38d23ba --- /dev/null +++ b/TheengsGateway/discovery.py @@ -0,0 +1,133 @@ +""" + TheengsGateway - Decode things and devices and publish data to an MQTT broker + + Copyright: (c)Florian ROBERT + + This file is part of TheengsGateway. + + TheengsGateway is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + TheengsGateway is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +# python 3.6 +# encoding=utf8 + +import json +import re +from .ble_gateway import gateway, logger +from ._decoder import getProperties + +ha_dev_classes = ["battery", + "carbon_monoxide", + "carbon_dioxide", + "humidity", + "illuminance", + "signal_strength", + "temperature", + "timestamp", + "pressure", + "power", + "current", + "energy", + "power_factor", + "voltage"] + +ha_dev_units = ["W", + "kW", + "V", + "A", + "W", + "°C", + "°F", + "ms", + "s", + "hPa", + "kg", + "lb", + "µS/cm", + "lx", + "%", + "dB", + "B"] + + +class discovery(gateway): + def __init__(self, broker, port, username, password, + discovery_topic, discovery_device_name, discovery_filter): + super().__init__(broker, port, username, password) + self.discovery_topic = discovery_topic + self.discovery_device_name = discovery_device_name + self.discovered_entities = [] + self.discovery_filter = discovery_filter + + def connect_mqtt(self): + super().connect_mqtt() + + def publish(self, msg, pub_topic): + return super().publish(msg, pub_topic) + + # publish sensor directly to home assistant via mqtt discovery + def publish_device_info(self, pub_device): + pub_device_uuid = pub_device['id'].replace(':', '') + device_data = json.dumps(pub_device) + if (pub_device_uuid in self.discovered_entities or + pub_device['model_id'] in self.discovery_filter): + logger.debug("Already discovered or filtered: %s" % + pub_device_uuid) + self.publish(device_data, self.pub_topic + '/' + + pub_device_uuid) + return + + logger.info(f"publishing device `{pub_device}`") + pub_device['properties'] = json.loads( + getProperties(pub_device['model_id']))['properties'] + + hadevice = {} + hadevice['identifiers'] = list({pub_device_uuid}) + hadevice['connections'] = [list(('mac', pub_device_uuid))] + hadevice['manufacturer'] = pub_device['brand'] + hadevice['model'] = pub_device['model_id'] + if 'name' in pub_device: + hadevice['name'] = pub_device['name'] + else: + hadevice['name'] = pub_device['model'] + hadevice['via_device'] = self.discovery_device_name + + discovery_topic = self.discovery_topic + "/" + pub_device_uuid + state_topic = self.pub_topic + "/" + pub_device_uuid + state_topic = re.sub(r'.+?/', '+/', state_topic, + len(re.findall(r'/', state_topic)) - 1) + data = getProperties(pub_device['model_id']) + data = json.loads(data) + data = data['properties'] + + for k in data.keys(): + device = {} + device['stat_t'] = state_topic + if k in pub_device['properties']: + if pub_device['properties'][k]['name'] in ha_dev_classes: + device['dev_cla'] = pub_device['properties'][k]['name'] + if pub_device['properties'][k]['unit'] in ha_dev_units: + device['unit_of_meas'] = pub_device['properties'][k]['unit'] + device['name'] = pub_device['model_id'] + "-" + k + device['uniq_id'] = pub_device_uuid + "-" + k + device['val_tpl'] = "{{ value_json." + k + " | is_defined }}" + device['state_class'] = "measurement" + config_topic = discovery_topic + "-" + k + "/config" + device['device'] = hadevice + self.publish(json.dumps(device), config_topic) + + self.discovered_entities.append(pub_device_uuid) + self.publish(device_data, self.pub_topic + '/' + + pub_device_uuid) + diff --git a/docs/use/use.md b/docs/use/use.md index fee25174..bc716bc9 100644 --- a/docs/use/use.md +++ b/docs/use/use.md @@ -20,7 +20,8 @@ Example payload received: C:\Users\1technophile>python -m TheengsGateway -h usage: -m [-h] [-H HOST] [-P PORT] [-u USER] [-p PWD] [-pt PUB_TOPIC] [-st SUB_TOPIC] [-pa PUBLISH_ALL] [-sd SCAN_DUR] [-tb TIME_BETWEEN] - [-ll {DEBUG,INFO,WARNING,ERROR,CRITICAL}] + [-ll {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [-Dt DISCOVERY_TOPIC] [-D DISCOVERY] + [-Dn DISCOVERY_DEVICE_NAME] [-Df DISCOVERY_FILTER [DISCOVERY_FILTER ...]] optional arguments: -h, --help show this help message and exit @@ -40,6 +41,14 @@ optional arguments: Seconds to wait between scans -ll {DEBUG,INFO,WARNING,ERROR,CRITICAL}, --log_level {DEBUG,INFO,WARNING,ERROR,CRITICAL} TheengsGateway log level + -Dt DISCOVERY_TOPIC, --discovery-topic DISCOVERY_TOPIC + MQTT Discovery topic + -D DISCOVERY, --discovery DISCOVERY + Enable(1) or disable(0) MQTT discovery + -Dn DISCOVERY_DEVICE_NAME, --discovery_name DISCOVERY_DEVICE_NAME + Device name for Home Assistant + -Df DISCOVERY_FILTER [DISCOVERY_FILTER ...], --discovery_filter DISCOVERY_FILTER [DISCOVERY_FILTER ...] + Device discovery filter list for Home Assistant ``` ## Publish to a 2 levels topic @@ -75,5 +84,13 @@ Example message: "txpower":12 } ``` +If possible, the data will be decoded and published. -If possible, the data will be decoded and published. \ No newline at end of file +## Home Assistant auto discovery +If enabled (default), decoded devices will publish their configuration to Home Assistant to be discovered. +- This can be enabled/disabled with the `-D` or `--discovery` command line argument with a value of 1 (enable) or 0 (disable). +- The discovery topic can be set with the `-Dt` or `--discovery_topic` command line argument. +- The discovery name can be set wit the `-Dn` or `--discovery_name` command line argument. +- Devices can be filtered from discovery with the `-Df` or `--discovery_filter` argument which takes a list of device "model_id" to be filtered. + +The `IBEACON`, `GAEN` and `MS-CDP` devices are already filtered as their addresses (id's) change over time resulting in multiple discoveries. \ No newline at end of file