Skip to content

Commit

Permalink
Add Home Assistant auto discovery option. (#30)
Browse files Browse the repository at this point in the history
* Add Home Assistant auto discovery option.

Co-authored-by: Tenn0 <[email protected]>

* Use wildcards in discovery state topic.

* Remove check of property in discovery config

Co-authored-by: Tenn0 <[email protected]>
  • Loading branch information
h2zero and Tenn0 authored May 26, 2022
1 parent 2872f23 commit ba8356e
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 16 deletions.
37 changes: 36 additions & 1 deletion TheengsGateway/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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:
Expand Down Expand Up @@ -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')

Expand Down
40 changes: 27 additions & 13 deletions TheengsGateway/ble_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(':', ''))

Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down
133 changes: 133 additions & 0 deletions TheengsGateway/discovery.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
"""

# 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)

21 changes: 19 additions & 2 deletions docs/use/use.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
## 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.

0 comments on commit ba8356e

Please sign in to comment.