# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2014, Oscar Acena <oscar.acena@uclm.es>
# This software is under the terms of GPLv3 or later.
import time
from datetime import datetime
import struct
import gattlib
[docs]class DiscoveryService(object):
"""Service to discover Mi Band devices nearby using Bluetooth LE.
``device`` is the name of your Bluetooth hardware (could be get
using ``hciconfig``). If not given, the first device is used:
*hci0*. ::
>>> sd = mibanda.DiscoveryService()
.. note:: It may require superuser privileges (run as root, or using sudo).
"""
def __init__(self, device="hci0"):
self.service = gattlib.DiscoveryService(device)
[docs] def discover(self, timeout=3):
"""Launch the discovery process. It runs a LE scan to discover new
devices, up to ``timeout`` seconds. It returns a list with newly
created :class:`mibanda.BandDevice` objects, one for each discovered
Mi Band. ::
>>> sd.discover()
[<mibanda.BandDevice at 0x7f03fcd54c50>]
"""
bands = []
for addr, name in self.service.discover(timeout).items():
band = BandDevice(addr, name)
bands.append(band)
return bands
[docs]class BandDevice(object):
"""This is the main object of mibanda library. It represents a Mi
Band device, and has those methods needed to read its state, and control/change
its behaviour.
``address`` is its MAC address, in the form of a string like:
``00:11:22:33:44:55``. Name is the advertised Bluetooth name. It
could be empty. ::
>>> device = mibanda.BandDevice("88:0f:10:00:01:02", "MI")
"""
def __init__(self, address, name=""):
self.address = address
self.name = name
self.requester = gattlib.GATTRequester(address, False)
[docs] def connect(self):
"""Used to create a connection with the Mi Band. It may take some
time, depending on many factors as LE params, channel usage,
etc. ::
>>> device.connect()
.. note:: This method needs to be called before almost any
other command. Also note that the connection will be
closed after some period of inactivity. See
Bluetooth LE params for more info.
"""
self.requester.connect(True)
[docs] def getAddress(self):
"""Get device's MAC address. Is the same value given at construction
time. It's a fixed param. ::
>>> device.getAddress()
'88:0F:10:00:01:02'
"""
return self.address
[docs] def getName(self, cached=True):
"""Get device's Bluetooth name. Usually the string 'MI', but it may be
changed. It will return the name given at construction time,
or read the device's name. This name will be cached, but you
can force the read passing *False* on ``cached`` argument. ::
>>> device.getName()
'MI'
"""
if cached and self.name:
return self.name
self.name = self.requester.read_by_uuid(UUID.DEVICE_NAME)[0]
return self.name
[docs] def getBatteryInfo(self):
"""Get information about device battery. Returns a
:class:`mibanda.BatteryInfo` instance. ::
>>> info = device.getBatteryInfo()
>>> info.status
'not charging'
>>> info.level
86
>>> info.charge_counter
6
>>> info.last_charged
datetime.datetime(2015, 1, 11, 22, 36)
"""
data = self.requester.read_by_uuid(UUID.BATTERY)
return BatteryInfo(data[0])
[docs] def getDeviceInfo(self):
"""Get device information: firmware version, etc.
::
>>> info = device.getDeviceInfo()
>>> info.firmware_version
'1.0.6.2'
"""
data = self.requester.read_by_uuid(UUID.DEVICE_INFO)[0]
return DeviceInfo(data)
[docs] def getSteps(self):
"""Get current step counter value. This is the device counter, as
shown in the official application. Returns an integer. ::
>>> device.getSteps()
4730
"""
data = self.requester.read_by_uuid(UUID.STEPS)[0]
return ord(data[0]) + (ord(data[1]) << 8)
[docs] def getLEParams(self):
"""Get Bluetooth LE connection parameters. This parameters are
negotiated at connection stablishment, and can be updated
later through a connection update. Returns a new
:class:`mibanda.LEParams` instance. ::
>>> le = device.getLEParams()
>>> le.minimum_connection_interval
460
>>> le.maximum_connection_interval
500
>>> le.latency
0
>>> le.timeout
500
>>> le.connection_interval
500
>>> le.advertisement_interval
2400
"""
data = self.requester.read_by_uuid(UUID.LE_PARAMS)[0]
return LEParams(data)
[docs] def setDateTime(self, dt=None):
"""Set current device date and time, ``dt`` is a ``datetime.datetime`` object.
.. note:: This method requires the device to be configured
with UserInfo. Otherwise, it will raise an ``Application error:
I/O``. See :meth:`mibanda.BandDevice.setUserInfo` for more info.
::
>>> from datetime import datetime
>>> now = datetime.now()
>>> device.setDateTime(now)
"""
if dt is None:
dt = datetime.now()
data = [(dt.year - 2000) & 0xff, dt.month - 1, dt.day,
dt.hour, dt.minute, dt.second]
self.requester.write_by_handle(Handle.DATE_TIME, str(bytearray(data)))
[docs] def getDateTime(self):
"""Get current device date and time. This characteristic could not be
read directly; it need to be writen first, and then read. This
process adds a bit of lag: the time of traveling packets. It
returns a ``datetime.datetime`` object.
.. note:: This method requires the device to be configured
with UserInfo. Otherwise, it will raise an ``Application error:
I/O``. See :meth:`mibanda.BandDevice.setUserInfo` for more info.
::
>>> device.getDateTime()
datetime.datetime(2015, 1, 20, 21, 28, 30, 0)
"""
start = datetime.now()
self.setDateTime(start)
c = 4
try:
data = self.requester.read_by_uuid(UUID.DATE_TIME)[0]
except RuntimeError:
c -= 1
if not c:
raise
time.sleep(.2)
fields = map(ord, data)[6:]
device_dt = datetime(fields[0] + 2000, fields[1] + 1, *fields[2:])
elapsed = datetime.now() - start
self.setDateTime(device_dt + elapsed)
return device_dt
[docs] def selfTest(self):
"""Perform an internat test: it will vibrate and flash leds.
.. warning:: This action will erase status information on your device.
::
>>> device.selfTest()
"""
self.requester.write_by_handle(Handle.TEST, str(bytearray([2])))
[docs] def pair(self):
"""Make a bluetooth pair between this host's hardware and the Mi Band
device. ::
>>> device.pair()
"""
self.requester.write_by_handle(Handle.PAIR, str(bytearray([2])))
counter = 5
while counter:
time.sleep(0.1)
try:
data = self.requester.read_by_handle(Handle.PAIR)[0]
except RuntimeError:
continue
if ord(data[0]) == 0x2:
break
counter -= 1
[docs] def setUserInfo(self, uid, male, age, height, weight, type_, alias=None):
"""Set user information on device. This params as a whole are needed
to avoid a status reset on the Mi Band. If you change any of
this params, the Mi Band will erase all previous capture data,
and initiate a new pairing process.
``uid`` is a number that identifies the relationship between
this host and this Mi Band. It may be any number, but it must
have 10 or less digits.
``male`` is a boolean param to set the user gender.
``age`` is your age in years.
``height`` is your height in centimeters.
``weight`` is your weight in kilograms.
``type_`` is a binary int (0 or 1) that specify if this
relationship should be rebuilt (1) or not (0). If 1, all saved
data will be lost.
``alias`` this is a stringfied version of the ``uid``. You can
safely left it blank, as it will be computed from the given
``uid``. ::
>>> device.setUserInfo(1563037356, True, 25, 180, 80, 0)
"""
seq = bytearray(20)
seq[:4] = [ord(i) for i in struct.pack("<I", uid)]
seq[4] = bool(male)
seq[5] = age & 0xff
seq[6] = height & 0xff
seq[7] = weight & 0xff
seq[8] = type_ & 0xff
if alias is None:
alias = str(uid)
alias = "0" * (10 - len(alias)) + alias
assert len(alias) == 10, "'alias' size must be 10 chars"
seq[9:19] = alias
addr = self.getAddress()
crc = self._getCRC8(seq[:19])
crc = (crc ^ int(addr[-2:], 16)) & 0xff
seq[19] = crc
self.requester.write_by_handle(Handle.USER_INFO, str(seq))
[docs] def flashLeds(self, r, g, b):
"""Toggle LED status for a few seconds, using values for red (``r``),
green (``g``) and blue (``b``), levels range from 0 (LED off)
to 6 (max bright). You can use the :class:`mibanda.Colors`
predefined colors.
.. note:: This method requires the device to be configured
with UserInfo. Otherwise, it will raise an ``Application error:
I/O``. See :meth:`mibanda.BandDevice.setUserInfo` for more info.
::
>>> # set red color
>>> device.flashLeds(6, 0, 0)
>>> # set orange
>>> device.flashLeds(*miband.Colors.ORANGE)
"""
self.requester.write_by_handle(
Handle.CONTROL_POINT, str(bytearray([0x0e, r, g, b, 0x01])))
[docs] def startVibration(self):
"""Put the band in vibration mode up to 10 seconds, or until you
tap the band.
.. note:: This method requires the device to be configured
with UserInfo. Otherwise, it will raise an ``Application error:
I/O``. See :meth:`mibanda.BandDevice.setUserInfo` for more info.
::
>>> device.startVibration()
"""
self.requester.write_by_handle(Handle.CONTROL_POINT, h2s("8:1"))
[docs] def stopVibration(self):
"""Stop the vibration mode, if running.
.. note:: This method requires the device to be configured
with UserInfo. Otherwise, it will raise an ``Application error:
I/O``. See :meth:`mibanda.BandDevice.setUserInfo` for more info.
::
>>> device.stopVibration()
"""
self.requester.write_by_handle(Handle.CONTROL_POINT, h2s("13"))
[docs] def customVibration(self, times, on_time, off_time):
"""Vibrate ``times`` times. Each iteration will start vibrator
``on_time`` milliseconds (up to 500, will be truncated if
larger), and then stop it ``off_time`` milliseconds (no limit
here).
.. note:: This method requires the device to be configured
with UserInfo. Otherwise, it will raise an ``Application error:
I/O``. See :meth:`mibanda.BandDevice.setUserInfo` for more info.
::
>>> # faster vibration (10 times)
>>> device.customVibration(10, 25, 10)
>>> # water drop vibration (5 times)
>>> device.customVibration(5, 25, 1200)
>>> # longer vibrations, no 'off time' (5 times)
>>> device.customVibration(5, 500, 0)
"""
on_time = min(500, on_time)
for i in range(times):
self.startVibration()
time.sleep(on_time / 1000.0)
self.stopVibration()
time.sleep(off_time / 1000.0)
[docs] def setGoal(self, steps):
"""Set the number of steps to ``steps`` to what will be considered
your daily goal.
.. note:: This method requires the device to be configured
with UserInfo. Otherwise, it will raise an ``Application error:
I/O``. See :meth:`mibanda.BandDevice.setUserInfo` for more info.
::
>>> device.setGoal(8000)
"""
data = [0x05, 0x00, steps & 0xff, (steps >> 8) & 0xff]
self.requester.write_by_handle(Handle.CONTROL_POINT, h2s(data))
[docs] def setCurrentSteps(self, steps):
"""Set the current step counter value to ``steps``. It may be used to
reset the counter.
.. note:: This method requires the device to be configured
with UserInfo. Otherwise, it will raise an ``Application error:
I/O``. See :meth:`mibanda.BandDevice.setUserInfo` for more info.
::
>>> setCurrentSteps(0)
"""
data = [0x10, steps & 0xff, (steps >> 8) & 0xff]
self.requester.write_by_handle(Handle.CONTROL_POINT, h2s(data))
[docs] def locate(self):
"""Vibrate and flash LEDs to locate your miband."""
self.requester.write_by_handle(
Handle.CONTROL_POINT, str(bytearray([0x08, 0x00])))
[docs] def setAlarm1(self, when, smart=0, repeat=0):
"""Set the alarm 1 to 'when', see :meth:`mibanda.BandDevice.setAlarm`
for more info."""
self.setAlarm(1, 0, when, smart, repeat)
[docs] def setAlarm2(self, when, smart=0, repeat=0):
"""Set the alarm 2 to 'when', see :meth:`mibanda.BandDevice.setAlarm`
for more info."""
self.setAlarm(1, 1, when, smart, repeat)
[docs] def setAlarm3(self, when, smart=0, repeat=0):
"""Set the alarm 3 to 'when', see :meth:`mibanda.BandDevice.setAlarm`
for more info."""
self.setAlarm(1, 2, when, smart, repeat)
[docs] def clearAlarm1(self, when, smart=0, repeat=0):
"""Clear the alarm 1, see :meth:`mibanda.BandDevice.setAlarm`
for more info."""
self.setAlarm(1, 0, when, smart, repeat)
[docs] def clearAlarm2(self, when, smart=0, repeat=0):
"""Clear the alarm 2, see :meth:`mibanda.BandDevice.setAlarm`
for more info."""
self.setAlarm(1, 1, when, smart, repeat)
[docs] def clearAlarm3(self, when, smart=0, repeat=0):
"""Clear the alarm 3, see :meth:`mibanda.BandDevice.setAlarm`
for more info."""
self.setAlarm(1, 2, when, smart, repeat)
[docs] def setAlarm(self, enable, number, when, smart, repeat):
"""Enable or disable the alarm ``number`` to ``when`` (a
datetime.datetime object in the future). If you want a 'smart'
wake up, then set ``smart`` to 1. ``repeat`` could be
mibanda.Calendar.EVERYDAY, or a combination of days (i.e:
SATURDAY | SUNDAY).
.. note:: This method requires the device to be configured
with UserInfo. Otherwise, it will raise an ``Application error:
I/O``. See :meth:`mibanda.BandDevice.setUserInfo` for more info.
::
>>> when = datetime.now() + timedelta(hours=8)
>>> device.setAlarm(1, 0, when, 0, mibanda.Calendar.MONDAY)
"""
assert number in (0, 1, 2), "Invalid alarm id"
smart = 30 if smart != 0 else 0
repeat = min(Calendar.EVERYDAY, repeat)
byteseq = [
0x04, number, enable,
(when.year - 2000) & 0xff, when.month - 1, when.day,
when.hour, when.minute, when.second,
smart, repeat,
]
self.requester.write_by_handle(Handle.CONTROL_POINT, h2s(byteseq))
[docs] def enableRealTimeSteps(self):
"""Enable realtime notifications about steps detection."""
data = [0x03, 0x01]
self.requester.write_by_handle(Handle.CONTROL_POINT, h2s(data))
[docs] def disableRealTimeSteps(self):
"""Disable realtime notifications about steps detection."""
data = [0x03, 0x00]
self.requester.write_by_handle(Handle.CONTROL_POINT, h2s(data))
def _getCRC8(self, data):
crc = 0
for i in range(0, len(data)):
crc = crc ^ (data[i] & 0xff)
for j in range(8):
if crc & 0x01:
crc = (crc >> 1) ^ 0x8c
else:
crc >>= 1
return crc
[docs]class BatteryInfo(object):
"""Holds the battery information for the Mi Band."""
def __init__(self, data):
fields = map(ord, data)
#: Battery level (in percentage), ranges from 0 (discharged) to 100 (full).
self.level = fields[0]
#: Date of last charge.
self.last_charged = datetime(
fields[1] + 2000,
fields[2] + 1,
*fields[3:6])
#: Counter of number of charges.
self.charge_counter = fields[7] + (fields[8] << 8)
status_names = {1: 'low', 2: 'medium', 3: 'full', 4: 'not charging'}
#: Current battery status, one of ``low``, ``medium``, ``full``
#: or ``not charging``.
self.status = status_names.get(fields[9], "unknown")
[docs]class LEParams(object):
def __init__(self, data):
"""Holds the current Bluetooth LE connection params."""
fields = map(ord, data)
#: Minimum connection internal, in ms.
self.minimum_connection_interval = fields[0] + (fields[1] << 8)
#: Maximum connection internal, in ms.
self.maximum_connection_interval = fields[2] + (fields[3] << 8)
#: Connection latency.
self.latency = fields[4] + (fields[5] << 8)
#: Connection timeout, in hundredths of a second.
self.timeout = fields[6] + (fields[7] << 8)
#: Connection interval.
self.connection_interval = fields[8] + (fields[9] << 8)
#: Advertisement interval.
self.advertisement_interval = fields[10] + (fields[11] << 8)
[docs]class DeviceInfo(object):
def __init__(self, data):
"""Hols some assorted device information."""
fields = map(ord, data)
#: Current firmware version.
self.firmware_version = "{}.{}.{}.{}".format(*reversed(fields[-4:]))
[docs]class UUID(object):
"""Mi Band characteristic UUIDs constants."""
#: Holds the device related information: firmware version, hardware revision, etc.
DEVICE_INFO = "0000ff01-0000-1000-8000-00805f9b34fb"
#: Holds the Bluetooth LE device name.
DEVICE_NAME = "0000ff02-0000-1000-8000-00805f9b34fb"
#: Holds the user related information: uuid, gender, age, height and
#: weight. Used to make a permanent bond with the device.
USER_INFO = "0000ff04-0000-1000-8000-00805f9b34fb"
#: Special characteristic to do multiple tasks: LED/motor control,
#: alarms, set current steps, step goal, etc.
CONTROL_POINT = "0000ff05-0000-1000-8000-00805f9b34fb"
#: Used to read current step count.
STEPS = "0000ff06-0000-1000-8000-00805f9b34fb"
#: Configuration of Bluetooth LE conection parameters.
LE_PARAMS = "0000ff09-0000-1000-8000-00805f9b34fb"
#: Used to get/set the date/time information on the device. Could not
#: be read directly, first must be writen.
DATE_TIME = "0000ff0a-0000-1000-8000-00805f9b34fb"
#: Holds battery information: status, level, charge counter and last charge date.
BATTERY = "0000ff0c-0000-1000-8000-00805f9b34fb"
[docs]class Handle(object):
"""
.. warning:: These may change after a new firmware release, please use UUIDs
instead (when possible).
Mi Band characteristic handlers.
"""
#: Handle for UUID USER_INFO.
USER_INFO = 0x19
#: Handle for UUID CONTROL_POINT.
CONTROL_POINT = 0x1b
#: Handle for UUID DATE_TIME.
DATE_TIME = 0x27
#: Handle to do an automatic hardware test.
TEST = 0x2e
#: Handle for pairing device.
PAIR = 0x33
[docs]class Calendar(object):
"""Constants used on alarm repeat pattern."""
MONDAY = 0b00000001
TUESDAY = 0b00000010
WEDNESDAY = 0b00000100
THURSDAY = 0b00001000
FRIDAY = 0b00010000
SATURDAY = 0b00100000
SUNDAY = 0b01000000
EVERYDAY = 0b01111111
[docs]class Colors(object):
"""Common colors of Mi Band LEDs. You can create new ones: a
combination of three integers that range from 0 to 6 (both
included). The higher the number the brighter the LED, 0 is LED off."""
BLACK = (0, 0, 0)
BLUE = (0, 0, 6)
GREEN = (0, 6, 0)
AQUA = (0, 6, 6)
RED = (6, 0, 0)
FUCHSIA = (6, 0, 6)
YELLOW = (6, 6, 0)
GRAY = (3, 3, 3)
WHITE = (6, 6, 6)
ORANGE = (6, 3, 0)
[docs]def h2s(data):
"""Converts a string of hex numbers separated by a colon (:) to a
string with the actual byte sequence. This is useful when sending
commands to PyGattlib, because they can be expressed in the same way as
wireshark dumps them; in example: ``03:1b:05``. ::
>>> data = mibanda.h2s("68:65:6c:6c:6f")
>>> print data
hello
"""
if isinstance(data, str):
data = map(lambda x: int(x, 16), data.split(":"))
return str(bytearray(data))