From time to time people ask me how to do the reverse engineering of a device to integrate it into Home Assistant. Sure, reverse engineering sounds fancy and will get you a better position in the search engine’s results but let’s be honest it’s not much magic or rocket science and most of the time it’s just putting the pieces together you found. I prefer to talk about it as “using devices beyond the original purpose”.
audius provided me with a Troika SELMATE (KR15-03/WH). It’s a Bluetooth Key/Smartphone Finder. The haptics are excellent and the quality is higher than the standard for such devices.
I usually don’t need to find my keys because my Yubikey is attached and without that I’m not able to unlock my computer. Same for my Smartphone which contains the 2FA app. Both things are most of the time pretty close to me.
The Troika SELMATE can be used in two ways if the device is paired with your smartphone: One press would create a selfie and two presses are activating the alarm on the smartphone. A click on the “Alert” button in the Troika Find app let the device blink and beep. Thus, there are multiple ways to use the device beside the Troika app: simple monitoring of its state, perhaps switching a light on, use it as an alarm device, as physical part of a self-made alarm clock and so on and on 🙂
First let’s see what we can find out. There is no special hardware required. I’m using a Lenovo T460 with a built-in Bluetooth adapter and a Fedora 27 installation. The first tool we are going to use is hcitool which is part of bluez. Perform the following command to install it.
1 |
$ sudo dnf -y install bluez |
Switch the Troika SELMATE on and run a scan with hcitool .
1 2 3 4 5 |
$ sudo hcitool lescan LE Scan ... 08:7C:BE:78:C9:A0 SELFMATE 08:7C:BE:78:C9:A0 (unknown) ... |
If you have other Bluetooth device like your smartphone or our mouse then they will show up as well. Make sure that the SELFMATE is not paired with your smartphone anymore. Let’s see what else is available.
1 2 3 4 5 6 |
$ sudo hcitool leinfo 08:7C:BE:78:C9:A0 Requesting information ... Handle: 3586 (0x0e02) LMP Version: 4.0 (0x6) LMP Subversion: 0x400 Manufacturer: Quintic Corp. (142) Features: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 |
Not much so far. Ok, next tool. gatttool is also part of bluez . Establishing a connection can be done in an interactive way.
1 2 3 4 |
$ sudo gatttool -b 08:7C:BE:78:C9:A0 -I [08:7C:BE:78:C9:A0][LE]> connect Attempting to connect to 08:7C:BE:78:C9:A0 Connection successful |
The first ting we are going to do is to the characteristic handles and service UUIDs implemented by the device.
1 2 3 4 5 6 7 |
[08:7C:BE:78:C9:A0][LE]> primary attr handle: 0x0010, end grp handle: 0x0012 uuid: 00001803-0000-1000-8000-00805f9b34fb attr handle: 0x0013, end grp handle: 0x0015 uuid: 00001802-0000-1000-8000-00805f9b34fb attr handle: 0x0016, end grp handle: 0x0018 uuid: 00001804-0000-1000-8000-00805f9b34fb attr handle: 0x0019, end grp handle: 0x001c uuid: 0000ffe0-0000-1000-8000-00805f9b34fb attr handle: 0x001d, end grp handle: 0x001f uuid: 000018f0-0000-1000-8000-00805f9b34fb attr handle: 0x0020, end grp handle: 0x0023 uuid: 0000180f-0000-1000-8000-00805f9b34fb |
Looking them up with the help of the Bluetooth services list will give us an idea about what the device is supporting.
- 00001803-: Link loss
- 00001802-: Immediate Alert
- 00001804-: Tx Power
- 0000180f-: Battery service Battery Level
Those two handles which are not documented.
- 0000ffe0-0000-1000-8000-00805f9b34fb
- 000018f0-0000-1000-8000-00805f9b34fb
The most import one seems to be “Immediate Alert”. This is the trigger to let the device beep and blink.
0x0015 is the group handle for the UUID 0x1802. Limiting the output to 0x0015 will give us the characteristic for the alarm.
1 2 |
[08:7C:BE:78:C9:A0][LE]> char-desc 0x0015 0x0015 handle: 0x0015, uuid: 00002a06-0000-1000-8000-00805f9b34fb |
0x2a06 is the UUID for the Alert Level. There are three levels available but only two are useful with a device like a keyfinder.
- 0x00 (No alert): Nothing
- 0x01 (Mild alert): Beeping
- 0x02 (High alert): Beeping and Blinking
Now, we know tht handle and the possible values. This means that we control the device in the way we want.
1 2 |
[08:7C:BE:78:C9:A0][LE]> char-write-cmd 0x0015 01 [08:7C:BE:78:C9:A0][LE]> char-write-cmd 0x0015 02 |
Like for the app…with a press you can confirm the alarm.
You can also get the battery level (Battery Service 0x180f, Battery Level 0x2a19)
1 2 3 4 |
[08:7C:BE:78:C9:A0][LE]> char-read-uuid 0desc 0x0022 0x0022 handle: 0x0022, uuid: 00002a19-0000-1000-8000-00805f9b34fb [08:7C:BE:78:C9:A0][LE]> char-read-uuid 00002a19-0000-1000-8000-00805f9b34fb handle: 0x0022 value: 63 |
Keep in mind that the value is HEX.
1 2 |
$ python3 -c "print(int('63', 16))" 99 |
Nice, the battery level is still at 99 %. We don’t care about Link Loss and Tx Power for now.
Next, get the button press. There are two UUID left. At the moment we don’t know much about the remain services or their settings. Without a third-party tool it’s try-and-error with going through possible values. gatttool needs be in listen mode to receive the signal.
The first two approaches don’t work.
1 2 3 |
$ gatttool -b 08:7C:BE:78:C9:A0 --char-write-req --handle=0x001c --value=0000 --listen $ gatttool -b 08:7C:BE:78:C9:A0 --char-write-req --handle=0x001c --value=0010 --listen |
Here we go:
1 2 3 |
$ gatttool -b 08:7C:BE:78:C9:A0 --char-write-req --handle=0x001c --value=0100 --listen Characteristic value was written successfully Notification handle = 0x001b value: 01 |
As an example we are going to integrate the Battery level into Home Assistant. Battery level sound like a sensor. Create a file called troika.py in your configuration directory for your custom components. It will become a sensor thus the path will something like that .homeassistant/custom_components/sensor/troika.py . To make the Home Assistant implementation simple, the subprocess module is executing the gatttool . This is not for production usage but for quick tests it sufficient. Also, if multiple platform using the same base then it should become a component.
Copy this code to your troika.py file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
"""Support for Troika keyfinder's Battery status.""" import logging import subprocess import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (CONF_MAC, CONF_NAME) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) BLE_BATTERY_HANDLE = '0x0022' DEFAULT_NAME = 'Troika Battery' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Troika Battery sensor.""" name = config.get(CONF_NAME) mac = config.get(CONF_MAC) add_devices([TroikaBattery(name, mac)], True) class TroikaBattery(Entity): """Representation of a Troika Battery sensor.""" def __init__(self, name, mac): """Initialize a Troika Battery sensor.""" self._state = None self._mac = mac self._name = name @property def name(self): """Return the name of the sensor.""" return self._name @property def state(self): """Return the state of the device.""" return self._state @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return "%" def update(self): """Get the latest data and updates the states.""" self._state = self.get_battery() def get_battery(self): """Get the battery level from the SELFMATE.""" command = ['gatttool', '-b', self._mac, '--char-read', '-a', BLE_BATTERY_HANDLE] battery = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) try: out, _ = battery.communicate() return int(out.decode().split()[2], 16) except subprocess.CalledProcessError: return None |
Don’t forget to add the new sensor to your configuration.yaml file:
1 2 3 |
sensor: - platform: troika mac: "08:7C:BE:78:C9:A0" |
After a restart of Home Assistant the new sensor should show up. It seems that the battery is draining quickly.
Let the SELFMATE start beeping is like switching something on or off. For a home automation solution this can be done with a switch. Create a file again in the custom_components directory but this time in the switch folder, e.g., .homeassistant/custom_components/switch/troika.py
The code for the switch is uning pretty much the same elements as the sensor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
"""Support for Troika keyfinder's Beeping mode.""" import logging import subprocess import voluptuous as vol from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_MAC, CONF_NAME) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) BLE_ALERT_HANDLE = '0x0015' DEFAULT_NAME = 'Troika Beeping' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Troika SELFMATE.""" name = config.get(CONF_NAME) mac = config.get(CONF_MAC) add_devices([TroikaBeep(name, mac)], True) class TroikaBeep(SwitchDevice): """Representation of a Troika Battery sensor.""" def __init__(self, name, mac): """Initialize a Troika Battery sensor.""" self._state = None self._mac = mac self._name = name @property def name(self): """Return the name of the sensor.""" return self._name @property def is_on(self): """Return true if device is on.""" return self._state def update(self): """Get the latest data and updates the states.""" return self._state def turn_on(self, **kwargs): """Turn the device on.""" self._state = True alert_off = ['gatttool', '-b', self._mac, '--char-write-req', '-a', BLE_ALERT_HANDLE, '-n', '01'] alert = subprocess.Popen( alert_off, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) try: alert.communicate() return alert.returncode == 0 except subprocess.CalledProcessError: return None def turn_off(self, **kwargs): """Turn the device off.""" self._state = False alert_off = ['gatttool', '-b', self._mac, '--char-write-req', '-a', BLE_ALERT_HANDLE, '-n', '00'] alert = subprocess.Popen( alert_off, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) try: alert.communicate() return alert.returncode == 0 except subprocess.CalledProcessError: return None |
To use the switch add it to your configuration.yaml file:
1 2 3 |
switch: - platform: troika mac: "08:7C:BE:78:C9:A0" |
Implementing the button press is a bit trickier than just monitoring a value. If you want to react on a button press then Home Assistant needs to listen all the time like gatttool with --listen . If you are interested then I suggest that you take a look at the existing platforms for a proper integration of Bluetooth devices.