diff --git a/README.md b/README.md index 71c5ba1..14ebbc6 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,29 @@ await aioble.advertise(BLE_ADV_INTERVAL_uS, adv_data=advert, connectable=False) See [main.py](src/main.py) for a more robust example. +Values can also be set directly in the `pack_advertisement()` call by using a +tuple, useful for sending multiple values of the same type, for example a +device with two buttons could send the state like this: + +``` +advert = beacon.pack_advertisement( + (BTHome.BUTTON_UINT8, BTHome.BUTTON_EVENT_NONE), + (BTHome.BUTTON_UINT8, BTHome.BUTTON_EVENT_PRESS) +) +``` + +[Note](https://bthome.io/format/#:~:text=Multiple%20events%20of%20the%20same%20type) +that the state for all duplicated objects must always be sent and always in the +same order. Buttons and dimmer events have a `0` state to represent no change. + +For buttons and dimmers it is advisable to set the trigger based device flag as +a hint to the receiver that the device might not be sending advertisements for +a long time: + +``` +beacon = BTHome("myBeacon", trigger_based=True) +``` + ## Will it run on Microcontroller X? If the device has Bluetooth and can run recent versions of MicroPython, it should work. diff --git a/src/bthome.py b/src/bthome.py index db99d6c..e8f4463 100644 --- a/src/bthome.py +++ b/src/bthome.py @@ -29,6 +29,10 @@ class BTHome: # Device name used in BLE advertisements. _local_name = "" + # Whether the device sends updates at regular intervals or on a trigger like a button. + _trigger_based = False + _TRIGGER_BASED_FLAG = 0x4 + # For most sensors defined below, the naming convention is: # ::= "_" "_x" # Example, temperature sint16 0.01 becomes: @@ -94,7 +98,7 @@ class BTHome: HUMIDITY_UINT8_X1 = const(0x2E) # % MOISTURE_UINT8_X1 = const(0x2F) # % BUTTON_UINT8 = const(0x3A) # 01 = press, 02 = long press, etc. - DIMMER_UINT16 = const(0x03A) # 01xx = rotate left xx steps, 02xx = rotate right xx steps + DIMMER_UINT16 = const(0x3C) # 01xx = rotate left xx steps, 02xx = rotate right xx steps COUNT_UINT16_X1 = const(0x3D) COUNT_UINT32_X1 = const(0x3E) ROTATION_SINT16_X10 = const(0x3F) # ° @@ -132,6 +136,19 @@ class BTHome: PRECIPITATION_UINT16_X1 = const(0x5F) # mm CHANNEL_UINT8_X1 = const(0x60) + # Button events + BUTTON_EVENT_NONE = 0 + BUTTON_EVENT_PRESS = 0x1 + BUTTON_EVENT_DOUBLE_PRESS = 0x2 + BUTTON_EVENT_TRIPLE_PRESS = 0x3 + BUTTON_EVENT_LONG_PRESS = 0x4 + BUTTON_EVENT_LONG_DOUBLE_PRESS = 0x5 + BUTTON_EVENT_LONG_TRIPLE_PRESS = 0x6 + BUTTON_EVENT_HOLD_PRESS = 0x80 + + # Dimmer events are represented as number of steps: negative values for + # left/counterclockwise and positive for right/clockwise. + # There is more than one way to represent most sensor properties. This # dictionary maps the object id to the property name. _object_id_properties = { @@ -294,10 +311,14 @@ class BTHome: volume_storage = 0 water = 0 window = False + button = 0 + dimmer = 0 - def __init__(self, local_name="BTHome", debug=False): + def __init__(self, local_name="BTHome", trigger_based=False, debug=False): local_name = local_name[:10] # Truncate to fit [^4] self._local_name = local_name + self._packet_id = 0 + self._trigger_based = trigger_based self.debug = debug @property @@ -323,6 +344,14 @@ def _pack_binary(self, object_id, value): def _pack_int8_x1(self, object_id, value): return pack("BB", object_id, round(value)) + def _pack_dimmer(self, object_id, value): + if value == 0: + return pack("BBB", object_id, 0, 0) + elif value > 0: # positive, clockwise, right + return pack("BBB", object_id, 2, round(value)) + else: # negative, counterclockwise, left + return pack("BBB", object_id, 1, round(-value)) + # 8-bit integer with scaling of 10 (1 decimal place) def _pack_int8_x10(self, object_id, value): return pack("BB", object_id, round(value * 10)) @@ -418,7 +447,7 @@ def _pack_raw_text(self, object_id, value): HUMIDITY_UINT8_X1: _pack_int8_x1, MOISTURE_UINT8_X1: _pack_int8_x1, BUTTON_UINT8: _pack_int8_x1, - DIMMER_UINT16: _pack_int16_x1, + DIMMER_UINT16: _pack_dimmer, COUNT_UINT16_X1: _pack_int16_x1, COUNT_UINT32_X1: _pack_int32_x1, ROTATION_SINT16_X10: _pack_int16_x10, @@ -456,6 +485,14 @@ def _pack_raw_text(self, object_id, value): CHANNEL_UINT8_X1: _pack_int8_x1 } + # Sort the packet object ids or tuples while preserving original order + def _sort_packet_objects(self, args): + args = sorted(enumerate(args), key=lambda ix: ( + ix[1] if isinstance(ix[1], int) else ix[1][0], + ix[0] + )) + return [x[1] for x in args] + # Concatenate an arbitrary number of sensor readings using parameters # of sensor data constants to indicate what's to be included. def _pack_service_data(self, *args): @@ -463,11 +500,21 @@ def _pack_service_data(self, *args): "B", BTHome._SERVICE_DATA_UUID16 ) # indicates a 16-bit service UUID follows service_data_bytes += pack("