diff --git a/existing.py b/existing.py deleted file mode 100644 index 98e920c..0000000 --- a/existing.py +++ /dev/null @@ -1,554 +0,0 @@ -#!/usr/bin/env python3 - -""" -Reading values from thingy device and send them as mqtt messages. -Handles automatic reconnection if bluetooth connection is lost. - -Source is derived from the bluepy library from Ian Harvey and Nordic Semiconductor - -Bluepy repository: https://github.com/IanHarvey/bluepy/blob/master/bluepy/thingy52.py -Nordic Semiconductor Python: https://devzone.nordicsemi.com/b/blog/posts/nordic-thingy52-raspberry-pi-python-interface -Nordic Semiconductor NodeJS: https://github.com/NordicPlayground/Nordic-Thingy52-Thingyjs - -dependencies installation on raspberry pi: -pip3 install bluepy -pip3 install paho-mqtt - -to find the MAC address: -sudo hcitool lescan - -Usage: -thingy52mqtt.py C2:9E:52:63:18:8A --no-mqtt --gas --temperature --humidity --pressure --battery --orientation --keypress --tap --sleep 5 -v -v -v -v -v - -""" - -import paho.mqtt.publish as publish -import paho.mqtt.client as mqtt_client -from bluepy import btle, thingy52 -import time -import os -import argparse -import binascii -import logging -import signal, sys -import random - -# constants for MQTT automatic reconnection -FIRST_RECONNECT_DELAY = 1 -RECONNECT_RATE = 2 -MAX_RECONNECT_COUNT = 12 -MAX_RECONNECT_DELAY = 60 - -next_event_second = 0 -args = None -thingy = None - -# last values from received from notification: -temperature = None -pressure = None -humidity = None -eco2 = None -tvoc = None -color = None -button = None -tapDirection = None -tapCount = None -orientation = None -battery = None - - -def setupSignalHandler(): - signal.signal(signal.SIGINT, _sigIntHandler) - signal.signal(signal.SIGTERM, _sigIntHandler) - logging.debug('Installed signal handlers') - -def _sigIntHandler(signum, frame): - global thingy - - logging.info('Received signal to exit') - if thingy: - thingy.disconnect() - disconnectMqtt() - exit(0) - -def setupLogging(): - '''Sets up logging''' - global args - if args.v > 5: - verbosityLevel = 5 - else: - verbosityLevel = args.v - # https://docs.python.org/2/library/logging.html#logging-levels - verbosityLevel = (5 - verbosityLevel)*10 - - # print('loglevel %d v:%d' % (verbosityLevel, args.v)) - - format = '%(asctime)s %(levelname)-8s %(message)s' - if args.logfile is not None: - logging.basicConfig(filename=args.logfile, level=verbosityLevel, format=format) - else: - logging.basicConfig(level=verbosityLevel, format=format) - - #logging.debug('debug') # 10 - #logging.info('info') # 20 - #logging.warn('warn') # 30 - #logging.error('error') # 40 - #logging.critical('critical') # 50 - - # logger = logging.getLogger(os.path.basename(__file__)) - - # logging.basicConfig(level=logging.DEBUG, - # format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', - # datefmt='%yyyy%m%d-%H:%M:%S') - -def connectMqtt(): - - def on_connect(client, userdata, flags, rc): - if rc == 0: - logging.info("Connected to MQTT broker") - else: - logging.error("Failed to connect to MQTT broker, return code %d\n", rc) - - def on_disconnect(client, userdata, rc): - logging.info("Disconnected with result code: %s", rc) - reconnect_count, reconnect_delay = 0, FIRST_RECONNECT_DELAY - while reconnect_count < MAX_RECONNECT_COUNT: - logging.info("Reconnecting in %d seconds...", reconnect_delay) - time.sleep(reconnect_delay) - - try: - client.reconnect() - logging.info("Reconnected successfully!") - return - except Exception as err: - logging.error("%s. Reconnect failed. Retrying...", err) - - reconnect_delay *= RECONNECT_RATE - reconnect_delay = min(reconnect_delay, MAX_RECONNECT_DELAY) - reconnect_count += 1 - logging.info("Reconnect failed after %s attempts. Exiting...", reconnect_count) - - # Set Connecting Client ID - client = mqtt_client.Client(args.clientid) - client.username_pw_set(args.username, args.password) - client.tls_set(ca_certs="/home/service/mg36-ca-opnsense2.crt") - client.on_connect = on_connect - client.on_disconnect = on_disconnect - client.connect(args.hostname, args.port) - return client - -def disconnectMqtt(): - client.loop_stop() - mqttSend("available", "offline", '') - -def sendMqttOnline(): - mqttSend("available", "online", '') - -def subscribeToNotifyAvailableWhenOnline(topic: str): - def on_message(client, userdata, msg): - logging.info(f"Received {msg.payload.decode()} from topic {msg.topic}") - if (msg.payload.decode() == "online"): - sendMqttOnline() - - logging.info(f"Subscribing to topic {topic}") - client.subscribe(topic) - client.on_message = on_message - -def mqttSendValues(notificationDelegate): - global temperature - global pressure - global humidity - global eco2 - global tvoc - global color - global button - global tapDirection - global tapCount - global orientation - global battery - - if args.temperature: - mqttSend('temperature', temperature, '°C') - temperature = None - if args.pressure: - mqttSend('pressure', pressure, 'hPa') - pressure = None - if args.humidity: - mqttSend('humidity', humidity, '%') - humidity = None - if args.gas: - mqttSend('eco2', eco2, 'ppm') - mqttSend('tvoc', tvoc, 'ppb') - eco2 = None - tvoc = None - if args.color: - mqttSend('color', color, '') - color = None - if args.tap: - mqttSend('tapdirection', tapDirection, '') - mqttSend('tapcount', tapCount, '') - tapDirection = None - tapCount = None - if args.orientation: - mqttSend('orientation', orientation, '') - orientation = None - if args.battery: - mqttSend('battery', battery, '%') - battery = None - -def mqttSend(key, value, unit): - global args - - if value is None: - logging.debug('no value given, do nothing for key %s' % key) - return - - if isinstance(value, int): - logging.debug('Sending MQTT messages key %s value %d%s' % (key, value, unit)) - elif isinstance(value, float) | isinstance(value, int): - logging.debug('Sending MQTT messages key %s value %.2f%s' % (key, value, unit)) - elif isinstance(value, str): - logging.debug('Sending MQTT messages key %s value %s%s' % (key, value, unit)) - else: - logging.debug('Sending MQTT messages key %s value %s%s' % (key, value, unit)) - - if args.mqttdisabled: - logging.debug('MQTT disabled, not sending message') - else: - try: - topic = args.topicprefix + key - payload = value - logging.debug('MQTT message topic %s, payload %s' % (topic, str(payload))) - client.publish(topic, payload) - except: - logging.error("Failed to publish message, details follow") - logging.error("hostname=%s topic=%s payload=%s" % (args.hostname, topic, payload)) - logging.error(sys.exc_info()[0]) - -class MQTTDelegate(btle.DefaultDelegate): - - def handleNotification(self, hnd, data): - global temperature - global pressure - global humidity - global eco2 - global tvoc - global color - global button - global tapDirection - global tapCount - global orientation - - #Debug print repr(data) - if (hnd == thingy52.e_temperature_handle): - teptep = binascii.b2a_hex(data) - value = self._str_to_int(teptep[:-2]) + int(teptep[-2:], 16) / 100.0 - temperature = value - - elif (hnd == thingy52.e_pressure_handle): - pressure_int, pressure_dec = self._extract_pressure_data(data) - value = pressure_int + pressure_dec / 100.0 - pressure = value - - elif (hnd == thingy52.e_humidity_handle): - teptep = binascii.b2a_hex(data) - value = self._str_to_int(teptep) - humidity = value - - elif (hnd == thingy52.e_gas_handle): - eco2, tvoc = self._extract_gas_data(data) - eco2 = eco2 - tvoc = tvoc - - elif (hnd == thingy52.e_color_handle): - teptep = binascii.b2a_hex(data) - red, green, blue, clear = self._extract_color_data(data) - color = "0x%0.2X%0.2X%0.2X" %(red, green, blue) - # logging.debug('color %s red %d, green %d, blue %d, clear %d' % (color, red, green, blue, clear)) - - elif (hnd == thingy52.ui_button_handle): - teptep = binascii.b2a_hex(data) - value = int(teptep) # 1 = pressed, 0 = released - button = value - # send button press instantly without waiting for timeout: - mqttSend('button', button, '') - - elif (hnd == thingy52.m_tap_handle): - direction, count = self._extract_tap_data(data) - match direction: - case 1: - tapDirection = "Bottom" - case 2: - tapDirection = "Left" - case 3: - tapDirection = "Top" - case 4: - tapDirection = "Right" - case 5: - tapDirection = "Front" - case 6: - tapDirection = "Back" - case _: - tapDirection = f"Unknown, {value}" - tapCount = count - - elif (hnd == thingy52.m_orient_handle): - teptep = binascii.b2a_hex(data) - value = int(teptep) - # 1 = led top left - # 2 = led top right / left side up - # 3 = led bottom right/bottom up - # 0 = led bottom left/ right side up - match value: - case 0: - orientation = "LED Bottom Left" - case 1: - orientation = "LED Top Left" - case 2: - orientation = "LED Top Right" - case 3: - orientation = "LED Bottom Right" - case _: - orientation = f"Unknown, {value}" - # orientation = value - - # elif (hnd == thingy52.m_heading_handle): - # teptep = binascii.b2a_hex(data) - # #value = int (teptep) - # logging.debug('Notification: Heading: {}'.format(teptep)) - # #self.mqttSend('heading', value, 'degrees') - - # elif (hnd == thingy52.m_gravity_handle): - # teptep = binascii.b2a_hex(data) - # logging.debug('Notification: Gravity: {}'.format(teptep)) - - # elif (hnd == thingy52.s_speaker_status_handle): - # teptep = binascii.b2a_hex(data) - # logging.debug('Notification: Speaker Status: {}'.format(teptep)) - - # elif (hnd == thingy52.s_microphone_handle): - # teptep = binascii.b2a_hex(data) - # logging.debug('Notification: Microphone: {}'.format(teptep)) - - else: - teptep = binascii.b2a_hex(data) - logging.debug('Notification: UNKOWN: hnd {}, data {}'.format(hnd, teptep)) - - def _str_to_int(self, s): - """ Transform hex str into int. """ - i = int(s, 16) - if i >= 2**7: - i -= 2**8 - return i - - def _extract_pressure_data(self, data): - """ Extract pressure data from data string. """ - teptep = binascii.b2a_hex(data) - pressure_int = 0 - for i in range(0, 4): - pressure_int += (int(teptep[i*2:(i*2)+2], 16) << 8*i) - pressure_dec = int(teptep[-2:], 16) - return (pressure_int, pressure_dec) - - def _extract_gas_data(self, data): - """ Extract gas data from data string. """ - teptep = binascii.b2a_hex(data) - eco2 = int(teptep[:2], 16) + (int(teptep[2:4], 16) << 8) - tvoc = int(teptep[4:6], 16) + (int(teptep[6:8], 16) << 8) - return eco2, tvoc - - def _extract_color_data(self, data): - """ Extract color data from data string. """ - teptep = binascii.b2a_hex(data) - red = int(teptep[:2], 16) - green = int(teptep[2:4], 16) - blue = int(teptep[4:6], 16) - clear = int(teptep[6:8], 16) - return red, green, blue, clear - - def _extract_tap_data(self, data): - """ Extract tap data from data string. """ - teptep = binascii.b2a_hex(data) - direction = int(teptep[0:2]) - count = int(teptep[2:4]) - return (direction, count) - - -def parseArgs(): - parser = argparse.ArgumentParser() - parser.add_argument('mac_address', action='store', help='MAC address of BLE peripheral') - parser.add_argument('-n', action='store', dest='count', default=0, - type=int, help="Number of times to loop data, if set to 0, loop endlessly") - parser.add_argument('-t', action='store', dest='timeout', type=float, default=2.0, help='time between polling') - parser.add_argument('--temperature', action="store_true",default=False) - parser.add_argument('--pressure', action="store_true",default=False) - parser.add_argument('--humidity', action="store_true",default=False) - parser.add_argument('--gas', action="store_true",default=False) - parser.add_argument('--color', action="store_true",default=False) - parser.add_argument('--keypress', action='store_true', default=False) - parser.add_argument('--battery', action='store_true', default=False) - parser.add_argument('--tap', action='store_true', default=False) - parser.add_argument('--orientation', action='store_true', default=False) - - # mqtt arguments - parser.add_argument('--no-mqtt', dest='mqttdisabled', action='store_true', default=False) - parser.add_argument('--host', dest='hostname', default='localhost', help='MQTT hostname') - parser.add_argument('--port', dest='port', default=1883, type=int, help='MQTT port') - parser.add_argument('--topic-prefix', dest='topicprefix', default="/home/thingy/", help='MQTT topic prefix to post the values, prefix + key is used as topic') - - parser.add_argument('--username', dest='username', default='mqttuser', help='MQTT username') - parser.add_argument('--password', dest='password', default='password', help='MQTT password') - parser.add_argument('--client-id', dest='clientid', default=f"python-mqtt-{random.randint(0, 1000)}", help='MQTT client ID') - - parser.add_argument('--sleep', dest='sleep', default=60, type=int, help='Interval to publish values.') - - parser.add_argument("--logfile", help="If specified, will log messages to the given file (default log to terminal)", default=None) - parser.add_argument("-v", help="Increase logging verbosity (can be used up to 5 times)", action="count", default=0) - - args = parser.parse_args() - return args - -def setNotifications(enable): - global thingy - global args - - if args.temperature: - thingy.environment.set_temperature_notification(enable) - if args.pressure: - thingy.environment.set_pressure_notification(enable) - if args.humidity: - thingy.environment.set_humidity_notification(enable) - if args.gas: - thingy.environment.set_gas_notification(enable) - if args.color: - thingy.environment.set_color_notification(enable) - if args.tap: - thingy.motion.set_tap_notification(enable) - if args.orientation: - thingy.motion.set_orient_notification(enable) - -def enableSensors(): - global thingy - global args - - # Enabling selected sensors - logging.debug('Enabling selected sensors...') - - if args.temperature: - thingy.environment.enable() - thingy.environment.configure(temp_int=1000) - if args.pressure: - thingy.environment.enable() - thingy.environment.configure(press_int=1000) - if args.humidity: - thingy.environment.enable() - thingy.environment.configure(humid_int=1000) - if args.gas: - thingy.environment.enable() - thingy.environment.configure(gas_mode_int=1) - if args.color: - thingy.environment.enable() - thingy.environment.configure(color_int=1000) - thingy.environment.configure(color_sens_calib=[0,0,0]) - # User Interface Service - if args.keypress: - thingy.ui.enable() - thingy.ui.set_btn_notification(True) - if args.battery: - thingy.battery.enable() - # Motion Service - if args.tap: - thingy.motion.enable() - thingy.motion.configure(motion_freq=200) - thingy.motion.set_tap_notification(True) - -def connect(notificationDelegate): - global args - global thingy - - connected = False - while not connected: - try: - logging.info('Try to connect to ' + args.mac_address) - thingy = thingy52.Thingy52(args.mac_address) - connected = True - logging.info('Connected...') - thingy.setDelegate(notificationDelegate) - sendMqttOnline() - except btle.BTLEException as ex: - connected = False - logging.debug('Could not connect, sleeping a while before retry') - time.sleep(args.sleep) # FIXME: use different sleep value?? - - -def main(): - global args - global thingy - global battery - global client - - args = parseArgs() - - setupLogging() - - setupSignalHandler() - - client = connectMqtt() - client.loop_start() - - subscribeToNotifyAvailableWhenOnline("homeassistant/status") - - notificationDelegate = MQTTDelegate() - - connectAndReadValues = True - while connectAndReadValues: - connect(notificationDelegate) - - #print("# Setting notification handler to default handler...") - #thingy.setDelegate(thingy52.MyDelegate()) - - try: - # Set LED so that we know we are connected - thingy.ui.enable() - #thingy.ui.set_led_mode_breathe(0x01, 1, 3000) # color 0x01 = RED, intensity, delay between breathes - thingy.ui.set_led_mode_constant(1,0,0) - logging.debug('LED set to breathe mode...') - - enableSensors() - setNotifications(True) - - counter = args.count - timeNextSend = time.time() - while connectAndReadValues: - # logging.debug('Loop start') - - if args.battery: - value = thingy.battery.read() - battery = value - - thingy.waitForNotifications(timeout = args.timeout) - - counter -= 1 - if counter == 0: - logging.debug('count reached, exiting...') - connectAndReadValues = False - - if time.time() > timeNextSend: - mqttSendValues(notificationDelegate) - timeNextSend = time.time() + args.sleep - - except btle.BTLEDisconnectError as e: - logging.debug('BTLEDisconnectError %s' % str(e)) - logging.info('Disconnected...') - disconnectMqtt() - thingy = None - - if thingy: - try: - thingy.disconnect() - #del thingy - finally: - disconnectMqtt() - -if __name__ == "__main__": - main()