Last active
February 11, 2024 00:57
-
-
Save taylorfinnell/87c79939a63ec2cb607ed2ebe28db5ce to your computer and use it in GitHub Desktop.
hai-smart-showerhead.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import asyncio | |
import struct | |
from bleak import BleakClient, BleakScanner | |
from dataclasses import dataclass | |
from datetime import datetime | |
XOR_DECRYPTION_KEY = [1, 2, 3, 4, 5, 6] # Yes, for real | |
@dataclass(eq=True, frozen=True) | |
class Characteristic: | |
type: str | |
desc: str | |
uuid: str | |
transform: callable | |
encrypted: bool | |
notifies: bool | |
def duration_transform(val): | |
from datetime import timedelta | |
delta = timedelta(seconds=val) | |
hours, remainder = divmod(delta.total_seconds(), 3600) | |
minutes, seconds = divmod(remainder, 60) | |
return f"{int(minutes)}:{int(seconds)}" | |
def temp_transform(val): | |
temp = val | |
temp /= 100 | |
return (1.8 * temp) + 32 | |
def flow_transform(val): | |
return float(val) * 0.000264172 | |
def consumption_transform(val): | |
return float(val) * 0.000264172 | |
def timestamp_transform(val): | |
return datetime.fromtimestamp(val) | |
def battery_transform(val): | |
return val / 1000.0 | |
SESSION_ID_CHAR = Characteristic( | |
desc="Session ID", | |
notifies=True, | |
encrypted=False, | |
transform=None, | |
uuid="e6221401-e12f-40f2-b0f5-aaa011c0aa8d", | |
type="i", | |
) | |
WATER_TEMP_CHAR = Characteristic( | |
desc="Water Temperature (F)", | |
notifies=True, | |
encrypted=False, | |
transform=temp_transform, | |
uuid="e6221402-e12f-40f2-b0f5-aaa011c0aa8d", | |
type="h", | |
) | |
AVG_WATER_TEMP_CHAR = Characteristic( | |
desc="Avg Shower Water Temperature (F)", | |
notifies=True, | |
encrypted=False, | |
transform=temp_transform, | |
uuid="e6221403-e12f-40f2-b0f5-aaa011c0aa8d", | |
type="h", | |
) | |
WATER_CONSUMPTION_CHAR = Characteristic( | |
desc="Water Consumption (gal)", | |
notifies=True, | |
encrypted=True, | |
transform=consumption_transform, | |
uuid="e6221404-e12f-40f2-b0f5-aaa011c0aa8d", | |
type="i", | |
) | |
SHOWER_FLOW_CHAR = Characteristic( | |
desc="Shower Water Flow Unit (gal/sec)", | |
notifies=True, | |
encrypted=True, | |
transform=flow_transform, | |
uuid="e6221405-e12f-40f2-b0f5-aaa011c0aa8d", | |
type="i", | |
) | |
SHOWER_DURATION_CHAR = Characteristic( | |
desc="Shower Duration", | |
notifies=True, | |
encrypted=True, | |
transform=duration_transform, | |
uuid="e6221406-e12f-40f2-b0f5-aaa011c0aa8d", | |
type="h", | |
) | |
SHOWER_TIMESTAMP_CHAR = Characteristic( | |
desc="Start Time", | |
notifies=True, | |
transform=timestamp_transform, | |
encrypted=True, | |
uuid="e6221407-e12f-40f2-b0f5-aaa011c0aa8d", | |
type="i", | |
) | |
SHOWER_LIFETIME_CONSUMPTION_CHAR = Characteristic( | |
desc="Lifetime Consumption (gal)", | |
notifies=True, | |
transform=consumption_transform, | |
uuid="e6221408-e12f-40f2-b0f5-aaa011c0aa8d", | |
type="i", | |
encrypted=True, | |
) | |
SHOWER_LIFETIME_WATER_TEMP_CHAR = Characteristic( | |
transform=temp_transform, | |
notifies=True, | |
encrypted=False, | |
uuid="e6221409-e12f-40f2-b0f5-aaa011c0aa8d", | |
type="h", | |
desc="Lifetime Water Temperature Unit: F", | |
) | |
SHOWER_BATTERY_CHAR = Characteristic( | |
desc="Battery Voltage (V)", | |
notifies=True, | |
encrypted=False, | |
transform=battery_transform, | |
uuid="e622140c-e12f-40f2-b0f5-aaa011c0aa8d", | |
type="h", | |
) | |
ALL_CHARACTERISTICS = [ | |
SESSION_ID_CHAR, | |
WATER_TEMP_CHAR, | |
AVG_WATER_TEMP_CHAR, | |
WATER_CONSUMPTION_CHAR, | |
SHOWER_FLOW_CHAR, | |
SHOWER_DURATION_CHAR, | |
SHOWER_TIMESTAMP_CHAR, | |
SHOWER_LIFETIME_CONSUMPTION_CHAR, | |
SHOWER_LIFETIME_WATER_TEMP_CHAR, | |
SHOWER_BATTERY_CHAR, | |
] | |
def decrypt(data, key=XOR_DECRYPTION_KEY): | |
return bytes([b ^ key[i % len(key)] for i, b in enumerate(data)]) | |
def print_reading(char: Characteristic, val): | |
print(f"{char.desc}:", val) | |
def build_notify_callback(char): | |
def cb(sender, data): | |
data = data if not char.encrypted else decrypt(data) | |
val = struct.unpack(char.type, data)[0] | |
if char.transform: | |
val = char.transform(val) | |
print_reading(char, val) | |
return cb | |
disconnected_event = asyncio.Event() | |
def disconnected_callback(client): | |
disconnected_event.set() | |
async def main(): | |
print("Connecting...") | |
device = await BleakScanner.find_device_by_name( | |
"haiSmartShower", cb={"use_bdaddr": False} | |
) | |
async with BleakClient(device) as client: | |
print("Connected...") | |
for c in filter(lambda c: c.notifies, ALL_CHARACTERISTICS): | |
print("Subscribing to changes for:", c.desc) | |
await client.start_notify(c.uuid, build_notify_callback(c)) | |
await disconnected_event.wait() | |
print("Disconnected") | |
asyncio.run(main()) | |
# 2024-02-09 20:37:27,881 __main__ INFO: starting scan... | |
# 2024-02-09 20:37:33,962 __main__ INFO: connecting to device... | |
# 2024-02-09 20:37:37,383 __main__ INFO: connected | |
# 2024-02-09 20:37:37,383 __main__ INFO: [Service] e6221400-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 14): Unknown | |
# 2024-02-09 20:37:37,414 __main__ INFO: [Characteristic] e6221401-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 15): Unknown (read,notify), Value: '00000000' | |
# 2024-02-09 20:37:37,443 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 17): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:37,504 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 18): Characteristic User Description, Value: bytearray(b'Current Shower ID') | |
# 2024-02-09 20:37:37,533 __main__ INFO: [Characteristic] e6221402-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 19): Unknown (read,notify), Value: '0000' | |
# 2024-02-09 20:37:37,564 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 21): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:37,624 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 22): Characteristic User Description, Value: bytearray(b'Real Time Water Temperature\nUnit: cCelsius') | |
# 2024-02-09 20:37:37,653 __main__ INFO: [Characteristic] e6221403-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 23): Unknown (read,notify), Value: '0000' | |
# 2024-02-09 20:37:37,683 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 25): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:37,759 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 26): Characteristic User Description, Value: bytearray(b'Average Shower Water Temperature\nUnit: cCelsius') | |
# 2024-02-09 20:37:37,788 __main__ INFO: [Characteristic] e6221404-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 27): Unknown (read,notify), Value: '00000000' | |
# 2024-02-09 20:37:37,833 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 29): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:37,908 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 30): Characteristic User Description, Value: bytearray(b'Total Water consumption\nUnit: mL') | |
# 2024-02-09 20:37:37,939 __main__ INFO: [Characteristic] e6221405-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 31): Unknown (read,notify), Value: '00000000' | |
# 2024-02-09 20:37:37,968 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 33): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:38,059 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 34): Characteristic User Description, Value: bytearray(b'Current Shower Water Flow\nUnit: mL/sec') | |
# 2024-02-09 20:37:38,088 __main__ INFO: [Characteristic] e6221406-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 35): Unknown (read,notify), Value: '0000' | |
# 2024-02-09 20:37:38,118 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 37): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:38,178 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 38): Characteristic User Description, Value: bytearray(b'Current Shower Duration\nUnit: sec') | |
# 2024-02-09 20:37:38,223 __main__ INFO: [Characteristic] e6221407-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 39): Unknown (read,notify), Value: '00000000' | |
# 2024-02-09 20:37:38,253 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 41): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:38,314 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 42): Characteristic User Description, Value: bytearray(b'Current Shower Timestamp (epoch)\nUnit: sec') | |
# 2024-02-09 20:37:38,344 __main__ INFO: [Characteristic] e6221408-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 43): Unknown (read,notify), Value: '00000000' | |
# 2024-02-09 20:37:38,373 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 45): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:38,448 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 46): Characteristic User Description, Value: bytearray(b"Device\'s Lifetime Water consumption\nUnit: mL") | |
# 2024-02-09 20:37:38,478 __main__ INFO: [Characteristic] e6221409-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 47): Unknown (read,notify), Value: '0000' | |
# 2024-02-09 20:37:38,508 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 49): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:38,569 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 50): Characteristic User Description, Value: bytearray(b'Average Lifetime Water Temperature\nUnit: cCelsius') | |
# 2024-02-09 20:37:38,599 __main__ INFO: [Characteristic] e622140a-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 51): Unknown (read,notify), Value: '1f020304600af5006f9b0406a5cdc561c20d' | |
# 2024-02-09 20:37:38,628 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 53): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:38,659 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 54): Characteristic User Description, Value: bytearray(b'Binary data of completed shower') | |
# 2024-02-09 20:37:38,689 __main__ INFO: [Characteristic] e622140b-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 55): Unknown (read), Value: 'a172203101f9' | |
# 2024-02-09 20:37:38,719 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 57): Characteristic User Description, Value: bytearray(b'Product ID') | |
# 2024-02-09 20:37:38,748 __main__ INFO: [Characteristic] e622140c-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 58): Unknown (read,notify), Value: 'd10d' | |
# 2024-02-09 20:37:38,793 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 60): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:38,853 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 61): Characteristic User Description, Value: bytearray(b'Current battery voltage\nUnit: mV') | |
# 2024-02-09 20:37:38,853 __main__ INFO: [Service] e6221500-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 62): Unknown | |
# 2024-02-09 20:37:38,883 __main__ INFO: [Characteristic] e6221501-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 63): Unknown (read,write), Value: '00000000' | |
# 2024-02-09 20:37:38,943 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 65): Characteristic User Description, Value: bytearray(b'First level threshold value\nUnit: mL') | |
# 2024-02-09 20:37:38,973 __main__ INFO: [Characteristic] e6221502-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 66): Unknown (read,write), Value: '00000000' | |
# 2024-02-09 20:37:39,033 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 68): Characteristic User Description, Value: bytearray(b'Second level threshold value\nUnit: mL') | |
# 2024-02-09 20:37:39,064 __main__ INFO: [Characteristic] e6221503-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 69): Unknown (read,write), Value: 'bd250204' | |
# 2024-02-09 20:37:39,138 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 71): Characteristic User Description, Value: bytearray(b'Third level threshold value\nUnit: mL') | |
# 2024-02-09 20:37:39,138 __main__ INFO: [Characteristic] e6221504-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 72): Unknown (write) | |
# 2024-02-09 20:37:39,169 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 74): Characteristic User Description, Value: bytearray(b'Current device time (epoch)') | |
# 2024-02-09 20:37:39,198 __main__ INFO: [Characteristic] e6221505-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 75): Unknown (read,write), Value: '000000' | |
# 2024-02-09 20:37:39,229 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 77): Characteristic User Description, Value: bytearray(b'First level RGB LED color') | |
# 2024-02-09 20:37:39,258 __main__ INFO: [Characteristic] e6221506-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 78): Unknown (read,write), Value: '000000' | |
# 2024-02-09 20:37:39,289 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 80): Characteristic User Description, Value: bytearray(b'Second level RGB LED color') | |
# 2024-02-09 20:37:39,319 __main__ INFO: [Characteristic] e6221507-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 81): Unknown (read,write), Value: '000000' | |
# 2024-02-09 20:37:39,349 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 83): Characteristic User Description, Value: bytearray(b'Third level RGB LED color') | |
# 2024-02-09 20:37:39,379 __main__ INFO: [Characteristic] e6221508-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 84): Unknown (read,write), Value: 'fe2203' | |
# 2024-02-09 20:37:39,409 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 86): Characteristic User Description, Value: bytearray(b'Fourth level RGB LED color') | |
# 2024-02-09 20:37:39,439 __main__ INFO: [Characteristic] e6221509-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 87): Unknown (read,write), Value: '01fd03' | |
# 2024-02-09 20:37:39,469 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 89): Characteristic User Description, Value: bytearray(b'Temperature level RGB LED color') | |
# 2024-02-09 20:37:39,469 __main__ INFO: [Characteristic] e622150a-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 90): Unknown (write) | |
# 2024-02-09 20:37:39,499 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 92): Characteristic User Description, Value: bytearray(b'Shower history erase') | |
# 2024-02-09 20:37:39,528 __main__ INFO: [Characteristic] e622150b-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 93): Unknown (read), Value: '6302' | |
# 2024-02-09 20:37:39,559 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 95): Characteristic User Description, Value: bytearray(b'Application version') | |
# 2024-02-09 20:37:39,603 __main__ INFO: [Characteristic] e622150c-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 96): Unknown (read), Value: '01' | |
# 2024-02-09 20:37:39,634 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 98): Characteristic User Description, Value: bytearray(b'Bootloader version') | |
# 2024-02-09 20:37:39,679 __main__ INFO: [Characteristic] e622150d-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 99): Unknown (read,write), Value: '01020304050601024c410406970e0f04050601020304050601fd038005a4' | |
# 2024-02-09 20:37:39,739 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 101): Characteristic User Description, Value: bytearray(b'Level Configuration (thresholds and RGB LED colors)') | |
# 2024-02-09 20:37:39,739 __main__ INFO: [Characteristic] e622150e-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 102): Unknown (write) | |
# 2024-02-09 20:37:39,768 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 104): Characteristic User Description, Value: bytearray(b'Factory reset') | |
# 2024-02-09 20:37:39,768 __main__ INFO: [Service] e6221600-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 105): Unknown | |
# 2024-02-09 20:37:39,769 __main__ INFO: [Characteristic] e6221601-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 106): Unknown (write) | |
# 2024-02-09 20:37:40,012 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 108): Characteristic User Description, Value: bytearray(b'Write any value from 0 to 255 to this characteristic to trigger a shower history read-out') | |
# 2024-02-09 20:37:42,005 __main__ INFO: [Characteristic] e6221602-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 109): Unknown (read,notify), Value: '1a020304' | |
# 2024-02-09 20:37:46,993 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 111): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:50,983 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 112): Characteristic User Description, Value: bytearray(b'Number of showers available in memory') | |
# 2024-02-09 20:37:50,983 __main__ INFO: [Characteristic] e6221603-e12f-40f2-b0f5-aaa011c0aa8d (Handle: 113): Unknown (notify) | |
# 2024-02-09 20:37:52,978 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 115): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:56,969 __main__ INFO: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 116): Characteristic User Description, Value: bytearray(b'Holds the data of a shower stored in the flash of the device') | |
# 2024-02-09 20:37:56,969 __main__ INFO: [Service] 0000fe59-0000-1000-8000-00805f9b34fb (Handle: 117): Nordic Semiconductor ASA | |
# 2024-02-09 20:37:56,969 __main__ INFO: [Characteristic] 8ec90003-f315-4f60-9fb8-838830daea50 (Handle: 118): Buttonless DFU (write,indicate) | |
# 2024-02-09 20:37:58,963 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 120): Client Characteristic Configuration, Value: bytearray(b'') | |
# 2024-02-09 20:37:58,963 __main__ INFO: disconnecting... | |
# 2024-02-09 20:37:58,964 __main__ INFO: disconnected |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment