From 52fa1f093678bcc026ebac096ea857420b44c88d Mon Sep 17 00:00:00 2001 From: Benjamin Piouffle Date: Sat, 22 Jul 2017 21:05:50 +1100 Subject: [PATCH 1/2] Allow using multiple bulbs at the same time with magicblueshell * `connect` can now be called multiple time with all the bulbs you want to connect to. Commands are then dispatched on every bulb one at a time * `connect` now accepts bulb version as an optional argument, allowing to connect to multiple bulbs with different versions at the same time --- magicblue/magicbluelib.py | 6 +- magicblue/magicblueshell.py | 114 ++++++++++++++++++++---------------- 2 files changed, 65 insertions(+), 55 deletions(-) diff --git a/magicblue/magicbluelib.py b/magicblue/magicbluelib.py index 796ce66..7044e93 100644 --- a/magicblue/magicbluelib.py +++ b/magicblue/magicbluelib.py @@ -29,7 +29,7 @@ def connection_required(func): """Raise an exception before calling the actual function if the device is - not connection. + not connected. """ @functools.wraps(func) def wrapper(self, *args, **kwargs): @@ -50,9 +50,9 @@ def _figure_addr_type(mac_address=None, version=None, addr_type=None): if version == 9 or version == 10: return btle.ADDR_TYPE_PUBLIC - if version == 8: + if version == 7 or version == 8: return btle.ADDR_TYPE_RANDOM - + # try using mac_address if mac_address is not None: mac_address_num = int(mac_address.replace(':', ''), 16) diff --git a/magicblue/magicblueshell.py b/magicblue/magicblueshell.py index 79100ae..7391450 100755 --- a/magicblue/magicblueshell.py +++ b/magicblue/magicblueshell.py @@ -15,7 +15,7 @@ import sys from sys import platform as _platform -import webcolors +from webcolors import hex_to_rgb, name_to_rgb from bluepy.btle import Scanner, DefaultDelegate try: from magicblue.magicbluelib import MagicBlue, Effect @@ -30,13 +30,14 @@ class MagicBlueShell: class Cmd: def __init__(self, cmd_str, func, conn_required, help='', params=None, - aliases=None): + aliases=None, opt_params=None): self.cmd_str = cmd_str self.func = func self.conn_required = conn_required self.help = help self.params = params or [] self.aliases = aliases or [] + self.opt_params = opt_params or [] def __init__(self, bluetooth_adapter, bulb_version=7): # List available commands and their usage. 'con_required' define if @@ -51,7 +52,9 @@ def __init__(self, bluetooth_adapter, bulb_version=7): help='List available effects',), MagicBlueShell.Cmd('connect', self.cmd_connect, False, help='Connect to light bulb', - params=['mac_address or ID']), + params=['mac_address||ID'], + aliases=['c'], + opt_params=['bulb version (default 7)']), MagicBlueShell.Cmd('disconnect', self.cmd_disconnect, True, help='Disconnect from current light bulb'), MagicBlueShell.Cmd('set_color', self.cmd_set_color, True, @@ -75,7 +78,7 @@ def __init__(self, bluetooth_adapter, bulb_version=7): self.bluetooth_adapter = bluetooth_adapter self._bulb_version = bulb_version - self._magic_blue = None + self._bulbs = [] self._devices = [] self.last_scan = None @@ -98,12 +101,12 @@ def start_interactive_mode(self): def exec_cmd(self, str_cmd): cmd = self._get_command(str_cmd) + args = str_cmd.split()[1:] if cmd is not None: - if cmd.conn_required and not (self._magic_blue and - self._magic_blue.is_connected()): + if cmd.conn_required and not (len(self._bulbs) > 0): logger.error('You must be connected to run this command') - elif self._check_args(str_cmd, cmd): - cmd.func(str_cmd.split()[1:]) + elif self._check_args(cmd, args): + cmd.func(args) else: logger.error('"{}" is not a valid command.' 'Type "help" to see what you can do' @@ -112,12 +115,14 @@ def exec_cmd(self, str_cmd): def print_usage(self, str_cmd): cmd = self._get_command(str_cmd) if cmd is not None: - print('Usage: {} {}'.format(cmd.cmd_str, ' '.join(cmd.params))) + params = ' '.join(cmd.params) + opt_params = ' '.join('[{}]'.format(p) for p in cmd.opt_params) + print('Usage: {} {} {}'.format(cmd.cmd_str, params, opt_params)) else: logger.error('Unknown command {}'.format(str_cmd)) return False - def cmd_list_devices(self, *args): + def cmd_list_devices(self, args): scan_time = 300 try: self.last_scan = ScanDelegate() @@ -135,65 +140,70 @@ def cmd_list_devices(self, *args): logger.error('Problem with the Bluetooth adapter : {}'.format(e)) return False - def cmd_list_effects(self, *args): + def cmd_list_effects(self, args): for e in Effect.__members__.keys(): print(e) - def cmd_connect(self, *args): + def cmd_connect(self, args): + bulb_version = args[1] if len(args) > 1 else self._bulb_version # Use can enter either a mac address or the device ID from the list - if len(args[0][0]) < 4 and self.last_scan: + if len(args[0]) < 4 and self.last_scan: try: - dev_id = int(args[0][0]) - 1 + dev_id = int(args[0]) - 1 entry = self.last_scan.devices[dev_id] mac_address = entry.addr addr_type = entry.addrType except Exception: - logger.error('Bad ID / MAC address : {}'.format(args[0][0])) + logger.error('Bad ID / MAC address : {}'.format(args[0])) return False else: addr_type = None - mac_address = args[0][0] - self._magic_blue = MagicBlue(mac_address, - version=self._bulb_version, - addr_type=addr_type) - self._magic_blue.connect(self.bluetooth_adapter) + mac_address = args[0] + magic_blue = MagicBlue(mac_address, + version=bulb_version, + addr_type=addr_type) + magic_blue.connect(self.bluetooth_adapter) + self._bulbs.append(magic_blue) logger.info('Connected') def cmd_disconnect(self, *args): - self._magic_blue.disconnect() - self._magic_blue = None + for bulb in self._bulbs: + bulb.disconnect() + self._bulbs = [] - def cmd_turn(self, *args): - if args[0][0] == 'on': - self._magic_blue.turn_on() + def cmd_turn(self, args): + if args[0] == 'on': + [bulb.turn_on() for bulb in self._bulbs] else: - self._magic_blue.turn_off() - - def cmd_read(self, *args): - if args[0][0] == 'name': - name = self._magic_blue.get_device_name() - logger.info('Received name: {}'.format(name)) - elif args[0][0] == 'device_info': - device_info = self._magic_blue.get_device_info() - logger.info('Received device_info: {}'.format(device_info)) - elif args[0][0] == 'date_time': - datetime_ = self._magic_blue.get_date_time() - logger.info('Received datetime: {}'.format(datetime_)) - - def cmd_set_color(self, *args): - color = args[0][0] + [bulb.turn_off() for bulb in self._bulbs] + + def cmd_read(self, args): + for bulb in self._bulbs: + print('-------------------') + if args[0] == 'name': + name = bulb.get_device_name() + logger.info('Received name: {}'.format(name)) + elif args[0] == 'device_info': + device_info = bulb.get_device_info() + logger.info('Received device_info: {}'.format(device_info)) + elif args[0] == 'date_time': + datetime_ = bulb.get_date_time() + logger.info('Received datetime: {}'.format(datetime_)) + + def cmd_set_color(self, args): + color = args[0] try: if color.startswith('#'): - self._magic_blue.set_color(webcolors.hex_to_rgb(color)) + [b.set_color(hex_to_rgb(color)) for b in self._bulbs] else: - self._magic_blue.set_color(webcolors.name_to_rgb(color)) + [b.set_color(name_to_rgb(color)) for b in self._bulbs] except ValueError as e: logger.error('Invalid color value : {}'.format(str(e))) self.print_usage('set_color') def cmd_set_warm_light(self, *args): try: - self._magic_blue.set_warm_light(float(args[0][0])) + [bulb.set_warm_light(float(args[0][0])) for bulb in self._bulbs] except ValueError as e: logger.error('Invalid intensity value : {}'.format(str(e))) self.print_usage('set_color') @@ -208,7 +218,7 @@ def cmd_set_effect(self, *args): except ValueError: self.print_usage('set_effect') else: - self._magic_blue.set_effect(effect, speed) + [bulb.set_effect(effect, speed) for bulb in self._bulbs] def list_commands(self, *args): print(' ----------------------------') @@ -225,13 +235,13 @@ def list_commands(self, *args): def cmd_exit(self, *args): print('Bye !') - def _check_args(self, str_cmd, cmd): - expected_nb_args = len(cmd.params) - args = str_cmd.split()[1:] - if len(args) != expected_nb_args: - self.print_usage(str_cmd.split()[0]) - return False - return True + def _check_args(self, cmd, args): + min_expected_nb_args = len(cmd.params) + max_expected_nb_args = min_expected_nb_args + len(cmd.opt_params) + if min_expected_nb_args <= len(args) <= max_expected_nb_args: + return True + self.print_usage(cmd.cmd_str) + return False def _get_command(self, str_cmd): str_cmd = str_cmd.split()[0] @@ -277,7 +287,7 @@ def get_params(): default='7', dest='bulb_version', type=int, - help='Bulb version as displayed in the official app') + help='Bulb version (currently support 7, 8, 9 and 10)') return parser.parse_args() From f7c1743b3b8a810669fc447aba32b515de9c47af Mon Sep 17 00:00:00 2001 From: Benjamin Piouffle Date: Tue, 19 Sep 2017 19:12:47 +1100 Subject: [PATCH 2/2] V5.0.0 release --- README.md | 29 +++++++++++++++-------------- magicblue/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 1e89f5e..aca6c19 100644 --- a/README.md +++ b/README.md @@ -53,14 +53,21 @@ You must use python 3+ and have a proper Bluetooth 4.0 interface installed on yo > sudo pip3 install magicblue - ⚠️ If you get the error `No such file or directory: '/usr/local/lib/python3.4/dist-packages/bluepy/bluepy-helper'` or - `ERROR:magicblue.magicblueshell:Unexpected error with command "ls": [Errno 8] Exec format error` : - This is a [known bug](https://github.com/IanHarvey/bluepy/issues/158) in - bluepy that sometimes doesn't get compiled when installed from Pypi on Raspberry Pi. - You can fix it by compiling the helper yourself : - Go to the lib folder (usually `/usr/local/lib/python3.4/dist-packages/bluepy-1.1.0-py3.5.egg/bluepy/` - but could be different, especially if you're using a virtual env) and - run `sudo make` (`make` should be enought for a virtual env) + ⚠️ If you get an error like + `No such file or directory: '/usr/local/lib/python3.4/dist-packages/bluepy/bluepy-helper'` + or + `ERROR:magicblue.magicblueshell:Unexpected error with command "ls": Helper exited` + checkout details below. +
+ This is a known bug in bluepy that sometimes doesn't get compiled + when installed from Pypi. + You can fix it by compiling the helper yourself : + Go to the lib folder (usually `/usr/local/lib/python3.5/dist-packages/bluepy-1.1.2-py3.5.egg/bluepy/` + but could be different, especially if you're using a virtual env) and + run `sudo make` (`make` should be enought for a virtual env) + + More info: https://github.com/IanHarvey/bluepy/issues/158 +
* Raspberry Pi specifics @@ -185,9 +192,3 @@ optional arguments: So if you want to change the color of bulb with mac address "C7:17:1D:43:39:03", just run : > sudo magicblueshell -c 'set_color red' -m C7:17:1D:43:39:03 - - -## TODO (help welcome!) - -- Use the [wiki info](https://github.com/Betree/magicblue/wiki/How-to-use-manually-with-Gatttool#functions) as a reference to implement turn_on / turn_off in a cleaner way (this may means being able to get the state from the bulb directly) -- Create a proper documentation diff --git a/magicblue/__init__.py b/magicblue/__init__.py index 286939f..1a4789a 100644 --- a/magicblue/__init__.py +++ b/magicblue/__init__.py @@ -5,7 +5,7 @@ Unofficial Python API to control Magic Blue bulbs over Bluetooth """ -__version__ = "0.4.3" +__version__ = "0.5.0" try: from magicblue.magicbluelib import MagicBlue, Effect diff --git a/setup.py b/setup.py index 71b7a72..cfa4ad5 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ license='MIT', packages=['magicblue'], install_requires=[ - 'bluepy==1.1.0', + 'bluepy==1.1.2', 'webcolors' ], include_package_data=True,