from __future__ import unicode_literals from . import packetutils as pckt from os import urandom from bluepy import btle import logging import struct import time # Commands : #: Set mesh groups. #: Data : 3 bytes C_MESH_GROUP = 0xd7 #: Set the mesh id. The light will still answer to the 0 mesh id. Calling the #: command again replaces the previous mesh id. #: Data : the new mesh id, 2 bytes in little endian order C_MESH_ADDRESS = 0xe0 #: C_MESH_RESET = 0xe3 #: On/Off command. Data : one byte 0, 1 C_POWER = 0xd0 #: Data : one byte C_LIGHT_MODE = 0x33 #: Data : one byte 0 to 6 C_PRESET = 0xc8 #: White temperature. one byte 0 to 0x7f C_WHITE_TEMPERATURE = 0xf0 #: one byte 1 to 0x7f C_WHITE_BRIGHTNESS = 0xf1 #: 4 bytes : 0x4 red green blue C_COLOR = 0xe2 #: one byte : 0xa to 0x64 .... C_COLOR_BRIGHTNESS = 0xf2 #: Data 4 bytes : How long a color is displayed in a sequence in milliseconds as #: an integer in little endian order C_SEQUENCE_COLOR_DURATION = 0xf5 #: Data 4 bytes : Duration of the fading between colors in a sequence, in #: milliseconds, as an integer in little endian order C_SEQUENCE_FADE_DURATION = 0xf6 #: 7 bytes C_TIME = 0xe4 #: 10 bytes C_ALARMS = 0xe5 PAIR_CHAR_UUID = '00010203-0405-0607-0809-0a0b0c0d1914' COMMAND_CHAR_UUID = '00010203-0405-0607-0809-0a0b0c0d1912' STATUS_CHAR_UUID = '00010203-0405-0607-0809-0a0b0c0d1911' OTA_CHAR_UUID = '00010203-0405-0607-0809-0a0b0c0d1913' logger = logging.getLogger (__name__) class Delegate(btle.DefaultDelegate): def __init__(self, light): self.light = light btle.DefaultDelegate.__init__(self) def handleNotification(self, cHandle, data): char = self.light.btdevice.getCharacteristics (cHandle)[0] if char.uuid == STATUS_CHAR_UUID: logger.info ("Notification on status char.") message = pckt.decrypt_packet (self.light.session_key, self.light.mac, data) else : logger.info ("Receiced notification from characteristic %s", char.uuid.getCommonName ()) message = pckt.decrypt_packet (self.light.session_key, self.light.mac, data) logger.info ("Received message : %s", repr (message)) class AwoxMeshLight: def __init__ (self, mac, mesh_name = "unpaired", mesh_password = "1234"): """ Args : mac: The light's MAC address as a string in the form AA:BB:CC:DD:EE:FF mesh_name: The mesh name as a string. mesh_password: The mesh password as a string. """ self.mac = mac self.mesh_id = 0 self.btdevice = btle.Peripheral () self.session_key = None self.mesh_name = mesh_name.encode () self.mesh_password = mesh_password.encode () # Light status self.white_brightness = None self.white_temp = None self.red = None self.green = None self.blue = None def connect(self, mesh_name = None, mesh_password = None): """ Args : mesh_name: The mesh name as a string. mesh_password: The mesh password as a string. """ if mesh_name : self.mesh_name = mesh_name.encode () if mesh_password : self.mesh_password = mesh_password.encode () self.btdevice.connect (self.mac) self.btdevice.setDelegate (Delegate (self)) pair_char = self.btdevice.getCharacteristics (uuid = PAIR_CHAR_UUID)[0] self.session_random = urandom(8) message = pckt.make_pair_packet (self.mesh_name, self.mesh_password, self.session_random) pair_char.write (message) status_char = self.btdevice.getCharacteristics (uuid = STATUS_CHAR_UUID)[0] status_char.write (b'\x01') reply = bytearray (pair_char.read ()) if reply[0] == 0xd : self.session_key = pckt.make_session_key (self.mesh_name, self.mesh_password, \ self.session_random, reply[1:9]) logger.info ("Connected.") return True else : if reply[0] == 0xe : logger.info ("Auth error : check name and password.") else : logger.info ("Unexpected pair value : %s", repr (reply)) self.disconnect () return False def setMesh (self, new_mesh_name, new_mesh_password, new_mesh_long_term_key): """ Sets or changes the mesh network settings. Args : new_mesh_name: The new mesh name as a string, 16 bytes max. new_mesh_password: The new mesh password as a string, 16 bytes max. new_mesh_long_term_key: The new long term key as a string, 16 bytes max. Returns : True on success. """ assert (self.session_key) pair_char = self.btdevice.getCharacteristics (uuid = PAIR_CHAR_UUID)[0] # FIXME : Removing the delegate as a workaround to a bluepy.btle.BTLEException # similar to https://github.com/IanHarvey/bluepy/issues/182 That may be # a bluepy bug or I'm using it wrong or both ... self.btdevice.setDelegate (None) message = pckt.encrypt (self.session_key, new_mesh_name.encode ()) message.insert (0, 0x4) pair_char.write (message) message = pckt.encrypt (self.session_key, new_mesh_password.encode ()) message.insert (0, 0x5) pair_char.write (message) message = pckt.encrypt (self.session_key, new_mesh_long_term_key.encode ()) message.insert (0, 0x6) pair_char.write (message) time.sleep (1) reply = bytearray (pair_char.read ()) self.btdevice.setDelegate (Delegate (self)) if reply[0] == 0x7 : self.mesh_name = new_mesh_name.encode () self.mesh_password = new_mesh_password.encode () logger.info ("Mesh network settings accepted.") return True else: logger.info ("Mesh network settings change failed : %s", repr(reply)) return False def setMeshId (self, mesh_id): """ Sets the mesh id. Args : mesh_id: as a number. """ data = struct.pack ("<H", mesh_id) self.writeCommand (C_MESH_ADDRESS, data) self.mesh_id = mesh_id def writeCommand (self, command, data, dest = None): """ Args: command: The command, as a number. data: The parameters for the command, as bytes. dest: The destination mesh id, as a number. If None, this lightbulb's mesh id will be used. """ assert (self.session_key) if dest == None: dest = self.mesh_id packet = pckt.make_command_packet (self.session_key, self.mac, dest, command, data) command_char = self.btdevice.getCharacteristics (uuid=COMMAND_CHAR_UUID)[0] logger.info ("Writing command %i data %s", command, repr (data)) command_char.write (packet) def resetMesh (self): """ Restores the default name and password. Will disconnect the device. """ self.writeCommand (C_MESH_RESET, b'\x00') def readStatus (self): status_char = self.btdevice.getCharacteristics (uuid = STATUS_CHAR_UUID)[0] packet = status_char.read () return pckt.decrypt_packet (self.session_key, self.mac, packet) def setColor (self, red, green, blue): """ Args : red, green, blue: between 0 and 0xff """ data = struct.pack ('BBBB', 0x04, red, green, blue) self.writeCommand (C_COLOR, data) def setColorBrightness (self, brightness): """ Args : brightness: a value between 0xa and 0x64 ... """ data = struct.pack ('B', brightness) self.writeCommand (C_COLOR_BRIGHTNESS, data) def setSequenceColorDuration (self, duration): """ Args : duration: in milliseconds. """ data = struct.pack ("<I", duration) self.writeCommand (C_SEQUENCE_COLOR_DURATION, data) def setSequenceFadeDuration (self, duration): """ Args: duration: in milliseconds. """ data = struct.pack ("<I", duration) self.writeCommand (C_SEQUENCE_FADE_DURATION, data) def setPreset (self, num): """ Set a preset color sequence. Args : num: number between 0 and 6 """ data = struct.pack('B', num) self.writeCommand (C_PRESET, data) def setWhite (self, temp, brightness): """ Args : temp: between 0 and 0x7f brightness: between 1 and 0x7f """ data = struct.pack ('B', temp) self.writeCommand (C_WHITE_TEMPERATURE, data) data = struct.pack ('B', brightness) self.writeCommand (C_WHITE_BRIGHTNESS, data) def on (self): """ Turns the light on. """ self.writeCommand (C_POWER, b'\x01') def off (self): """ Turns the light off. """ self.writeCommand (C_POWER, b'\x00') def disconnect (self): logger.info ("Disconnecting.") self.btdevice.disconnect () self.session_key = None def getFirmwareRevision (self): """ Returns : The firmware version as a null terminated utf-8 string. """ char = self.btdevice.getCharacteristics (uuid=btle.AssignedNumbers.firmwareRevisionString)[0] return char.read () def getHardwareRevision (self): """ Returns : The hardware version as a null terminated utf-8 string. """ char = self.btdevice.getCharacteristics (uuid=btle.AssignedNumbers.hardwareRevisionString)[0] return char.read () def getModelNumber (self): """ Returns : The model as a null terminated utf-8 string. """ char = self.btdevice.getCharacteristics (uuid=btle.AssignedNumbers.modelNumberString)[0] return char.read () def sendFirmware (self, firmware_path): """ Updates the light bulb's firmware. The light will blink green after receiving the new firmware. Args: firmware_path: The path of the firmware file. """ assert (self.session_key) with open (firmware_path, 'rb') as firmware_file : firmware_data = firmware_file.read() if not firmware_data : return ota_char = self.btdevice.getCharacteristics (uuid=OTA_CHAR_UUID)[0] count = 0 for i in range (0, len (firmware_data), 0x10): data = struct.pack ('<H', count) + firmware_data [i:i+0x10].ljust (0x10, b'\xff') crc = pckt.crc16 (data) packet = data + struct.pack ('<H', crc) logger.debug ("Writing packet %i of %i : %s", count + 1, len(firmware_data)/0x10 + 1, repr(packet)) ota_char.write (packet) # FIXME : When calling write with withResponse=True bluepy hangs after a few packets. # Without any delay the light blinks once without accepting the firmware. # The choosen value is arbitrary. time.sleep (0.01) count += 1 data = struct.pack ('<H', count) crc = pckt.crc16 (data) packet = data + struct.pack ('<H', crc) logger.debug ("Writing last packet : %s", repr(packet)) ota_char.write (packet)