pax_global_header00006660000000000000000000000064146567416650014536gustar00rootroot0000000000000052 comment=5a3a6345a93ee29bb3741861de9f1c74e9802835 pyflic-2.0.4/000077500000000000000000000000001465674166500130275ustar00rootroot00000000000000pyflic-2.0.4/.gitignore000066400000000000000000000020251465674166500150160ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject pyflic-2.0.4/LICENSE000066400000000000000000000020541465674166500140350ustar00rootroot00000000000000MIT License Copyright (c) 2024 Sören Oldag Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.pyflic-2.0.4/README.md000066400000000000000000000004171465674166500143100ustar00rootroot00000000000000# pyflic [![PyPI version](https://badge.fury.io/py/pyflic.svg)](https://badge.fury.io/py/pyflic) This repository only mirrors the flic python client library from https://github.com/50ButtonsEach/fliclib-linux-hci and just exists until there is an official PyPI package. pyflic-2.0.4/pyflic/000077500000000000000000000000001465674166500143155ustar00rootroot00000000000000pyflic-2.0.4/pyflic/__init__.py000066400000000000000000000651121465674166500164330ustar00rootroot00000000000000"""Flic client library for python Requires python 3.3 or higher. For detailed documentation, see the protocol documentation. Notes on the data type used in this python implementation compared to the protocol documentation: All kind of integers are represented as python integers. Booleans use the Boolean type. Enums use the defined python enums below. Bd addr are represented as standard python strings, e.g. "aa:bb:cc:dd:ee:ff". """ from enum import Enum from collections import namedtuple import time import socket import select import struct import itertools import queue import threading class CreateConnectionChannelError(Enum): NoError = 0 MaxPendingConnectionsReached = 1 class ConnectionStatus(Enum): Disconnected = 0 Connected = 1 Ready = 2 class DisconnectReason(Enum): Unspecified = 0 ConnectionEstablishmentFailed = 1 TimedOut = 2 BondingKeysMismatch = 3 class RemovedReason(Enum): RemovedByThisClient = 0 ForceDisconnectedByThisClient = 1 ForceDisconnectedByOtherClient = 2 ButtonIsPrivate = 3 VerifyTimeout = 4 InternetBackendError = 5 InvalidData = 6 CouldntLoadDevice = 7 DeletedByThisClient = 8 DeletedByOtherClient = 9 ButtonBelongsToOtherPartner = 10 DeletedFromButton = 11 class ClickType(Enum): ButtonDown = 0 ButtonUp = 1 ButtonClick = 2 ButtonSingleClick = 3 ButtonDoubleClick = 4 ButtonHold = 5 class BdAddrType(Enum): PublicBdAddrType = 0 RandomBdAddrType = 1 class LatencyMode(Enum): NormalLatency = 0 LowLatency = 1 HighLatency = 2 class BluetoothControllerState(Enum): Detached = 0 Resetting = 1 Attached = 2 class ScanWizardResult(Enum): WizardSuccess = 0 WizardCancelledByUser = 1 WizardFailedTimeout = 2 WizardButtonIsPrivate = 3 WizardBluetoothUnavailable = 4 WizardInternetBackendError = 5 WizardInvalidData = 6 WizardButtonBelongsToOtherPartner = 7 WizardButtonAlreadyConnectedToOtherDevice = 8 class ButtonScanner: """ButtonScanner class. Usage: scanner = ButtonScanner() scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified, already_connected_to_this_device, already_connected_to_other_device: ... client.add_scanner(scanner) """ _cnt = itertools.count() def __init__(self): self._scan_id = next(ButtonScanner._cnt) self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified, already_connected_to_this_device, already_connected_to_other_device: None class ScanWizard: """ScanWizard class Usage: wizard = ScanWizard() wizard.on_found_private_button = lambda scan_wizard: ... wizard.on_found_public_button = lambda scan_wizard, bd_addr, name: ... wizard.on_button_connected = lambda scan_wizard, bd_addr, name: ... wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ... client.add_scan_wizard(wizard) """ _cnt = itertools.count() def __init__(self): self._scan_wizard_id = next(ScanWizard._cnt) self._bd_addr = None self._name = None self.on_found_private_button = lambda scan_wizard: None self.on_found_public_button = lambda scan_wizard, bd_addr, name: None self.on_button_connected = lambda scan_wizard, bd_addr, name: None self.on_completed = lambda scan_wizard, result, bd_addr, name: None class BatteryStatusListener: """BatteryStatusListener class Usage: listener = BatteryStatusListener(bd_addr) listener.on_battery_status = lambda battery_status_listener, bd_addr, battery_percentage, timestamp: ... client.add_battery_status_listener(listener) """ _cnt = itertools.count() def __init__(self, bd_addr): self._listener_id = next(BatteryStatusListener._cnt) self._bd_addr = bd_addr self.on_battery_status = lambda battery_status_listener, battery_percentage, timestamp: None @property def bd_addr(self): return self._bd_addr class ButtonConnectionChannel: """ButtonConnectionChannel class. This class represents a connection channel to a Flic button. Add this button connection channel to a FlicClient by executing client.add_connection_channel(connection_channel). You may only have this connection channel added to one FlicClient at a time. Before you add the connection channel to the client, you should set up your callback functions by assigning the corresponding properties to this object with a function. Each callback function has a channel parameter as the first one, referencing this object. Available properties and the function parameters are: on_create_connection_channel_response: channel, error, connection_status on_removed: channel, removed_reason on_connection_status_changed: channel, connection_status, disconnect_reason on_button_up_or_down / on_button_click_or_hold / on_button_single_or_double_click / on_button_single_or_double_click_or_hold: channel, click_type, was_queued, time_diff """ _cnt = itertools.count() def __init__(self, bd_addr, latency_mode = LatencyMode.NormalLatency, auto_disconnect_time = 511): self._conn_id = next(ButtonConnectionChannel._cnt) self._bd_addr = bd_addr self._latency_mode = latency_mode self._auto_disconnect_time = auto_disconnect_time self._client = None self.on_create_connection_channel_response = lambda channel, error, connection_status: None self.on_removed = lambda channel, removed_reason: None self.on_connection_status_changed = lambda channel, connection_status, disconnect_reason: None self.on_button_up_or_down = lambda channel, click_type, was_queued, time_diff: None self.on_button_click_or_hold = lambda channel, click_type, was_queued, time_diff: None self.on_button_single_or_double_click = lambda channel, click_type, was_queued, time_diff: None self.on_button_single_or_double_click_or_hold = lambda channel, click_type, was_queued, time_diff: None @property def bd_addr(self): return self._bd_addr @property def latency_mode(self): return self._latency_mode @latency_mode.setter def latency_mode(self, latency_mode): if self._client is None: self._latency_mode = latency_mode return with self._client._lock: self._latency_mode = latency_mode if not self._client._closed: self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time}) @property def auto_disconnect_time(self): return self._auto_disconnect_time @auto_disconnect_time.setter def auto_disconnect_time(self, auto_disconnect_time): if self._client is None: self._auto_disconnect_time = auto_disconnect_time return with self._client._lock: self._auto_disconnect_time = auto_disconnect_time if not self._client._closed: self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time}) class FlicClient: """FlicClient class. When this class is constructed, a socket connection is established. You may then send commands to the server and set timers. Once you are ready with the initialization you must call the handle_events() method which is a main loop that never exits, unless the socket is closed. For a more detailed description of all commands, events and enums, check the protocol specification. All commands are wrapped in more high level functions and events are reported using callback functions. All methods called on this class will take effect only if you eventually call the handle_events() method. The ButtonScanner is used to set up a handler for advertisement packets. The ButtonConnectionChannel is used to interact with connections to flic buttons and receive their events. The BatteryStatusListener is used to get battery level. Other events are handled by the following callback functions that can be assigned to this object (and a list of the callback function parameters): on_new_verified_button: bd_addr on_no_space_for_new_connection: max_concurrently_connected_buttons on_got_space_for_new_connection: max_concurrently_connected_buttons on_bluetooth_controller_state_change: state """ _EVENTS = [ ("EvtAdvertisementPacket", "> 8 bytes[2] = opcode bytes += data_bytes with self._lock: if not self._closed: self._sock.sendall(bytes) def _dispatch_event(self, data): if len(data) == 0: return opcode = data[0] if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] == None: return event_name = FlicClient._EVENTS[opcode][0] data_tuple = FlicClient._EVENT_STRUCTS[opcode].unpack(data[1 : 1 + FlicClient._EVENT_STRUCTS[opcode].size]) items = FlicClient._EVENT_NAMED_TUPLES[opcode]._make(data_tuple)._asdict() # Process some kind of items whose data type is not supported by struct if "bd_addr" in items: items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"]) if "name" in items: items["name"] = items["name"].decode("utf-8") if event_name == "EvtCreateConnectionChannelResponse": items["error"] = CreateConnectionChannelError(items["error"]) items["connection_status"] = ConnectionStatus(items["connection_status"]) if event_name == "EvtConnectionStatusChanged": items["connection_status"] = ConnectionStatus(items["connection_status"]) items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"]) if event_name == "EvtConnectionChannelRemoved": items["removed_reason"] = RemovedReason(items["removed_reason"]) if event_name == "EvtButtonUpOrDown" or event_name == "EvtButtonClickOrHold" or event_name == "EvtButtonSingleOrDoubleClick" or event_name == "EvtButtonSingleOrDoubleClickOrHold": items["click_type"] = ClickType(items["click_type"]) if event_name == "EvtGetInfoResponse": items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"]) items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["my_bd_addr"]) items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"]) items["bd_addr_of_verified_buttons"] = [] pos = FlicClient._EVENT_STRUCTS[opcode].size for i in range(items["nb_verified_buttons"]): items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6])) pos += 6 if event_name == "EvtBluetoothControllerStateChange": items["state"] = BluetoothControllerState(items["state"]) if event_name == "EvtGetButtonInfoResponse": items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"])) if items["uuid"] == "00000000000000000000000000000000": items["uuid"] = None items["color"] = items["color"].decode("utf-8") if items["color"] == "": items["color"] = None items["serial_number"] = items["serial_number"].decode("utf-8") if items["serial_number"] == "": items["serial_number"] = None if event_name == "EvtScanWizardCompleted": items["result"] = ScanWizardResult(items["result"]) # Process event if event_name == "EvtAdvertisementPacket": scanner = self._scanners.get(items["scan_id"]) if scanner is not None: scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"], items["is_private"], items["already_verified"], items["already_connected_to_this_device"], items["already_connected_to_other_device"]) if event_name == "EvtCreateConnectionChannelResponse": channel = self._connection_channels[items["conn_id"]] if items["error"] != CreateConnectionChannelError.NoError: del self._connection_channels[items["conn_id"]] channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"]) if event_name == "EvtConnectionStatusChanged": channel = self._connection_channels[items["conn_id"]] channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"]) if event_name == "EvtConnectionChannelRemoved": channel = self._connection_channels[items["conn_id"]] del self._connection_channels[items["conn_id"]] channel.on_removed(channel, items["removed_reason"]) if event_name == "EvtButtonUpOrDown": channel = self._connection_channels[items["conn_id"]] channel.on_button_up_or_down(channel, items["click_type"], items["was_queued"], items["time_diff"]) if event_name == "EvtButtonClickOrHold": channel = self._connection_channels[items["conn_id"]] channel.on_button_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"]) if event_name == "EvtButtonSingleOrDoubleClick": channel = self._connection_channels[items["conn_id"]] channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"], items["time_diff"]) if event_name == "EvtButtonSingleOrDoubleClickOrHold": channel = self._connection_channels[items["conn_id"]] channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"]) if event_name == "EvtNewVerifiedButton": self.on_new_verified_button(items["bd_addr"]) if event_name == "EvtGetInfoResponse": self._get_info_response_queue.get()(items) if event_name == "EvtNoSpaceForNewConnection": self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"]) if event_name == "EvtGotSpaceForNewConnection": self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"]) if event_name == "EvtBluetoothControllerStateChange": self.on_bluetooth_controller_state_change(items["state"]) if event_name == "EvtGetButtonInfoResponse": self._get_button_info_queue.get()(items["bd_addr"], items["uuid"], items["color"], items["serial_number"], items["flic_version"], items["firmware_version"]) if event_name == "EvtScanWizardFoundPrivateButton": scan_wizard = self._scan_wizards[items["scan_wizard_id"]] scan_wizard.on_found_private_button(scan_wizard) if event_name == "EvtScanWizardFoundPublicButton": scan_wizard = self._scan_wizards[items["scan_wizard_id"]] scan_wizard._bd_addr = items["bd_addr"] scan_wizard._name = items["name"] scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name) if event_name == "EvtScanWizardButtonConnected": scan_wizard = self._scan_wizards[items["scan_wizard_id"]] scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name) if event_name == "EvtScanWizardCompleted": scan_wizard = self._scan_wizards[items["scan_wizard_id"]] del self._scan_wizards[items["scan_wizard_id"]] scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name) if event_name == "EvtButtonDeleted": self.on_button_deleted(items["bd_addr"], items["deleted_by_this_client"]) if event_name == "EvtBatteryStatus": listener = self._battery_status_listeners.get(items["listener_id"]) if listener is not None: listener.on_battery_status(listener, items["battery_percentage"], items["timestamp"]) def _handle_one_event(self): if len(self._timers.queue) > 0: current_timer = self._timers.queue[0] timeout = max(current_timer[0] - time.monotonic(), 0) if timeout == 0: self._timers.get()[1]() return True if len(select.select([self._sock], [], [], timeout)[0]) == 0: return True len_arr = bytearray(2) view = memoryview(len_arr) toread = 2 while toread > 0: nbytes = self._sock.recv_into(view, toread) if nbytes == 0: return False view = view[nbytes:] toread -= nbytes packet_len = len_arr[0] | (len_arr[1] << 8) data = bytearray(packet_len) view = memoryview(data) toread = packet_len while toread > 0: nbytes = self._sock.recv_into(view, toread) if nbytes == 0: return False view = view[nbytes:] toread -= nbytes self._dispatch_event(data) return True def handle_events(self): """Start the main loop for this client. This method will not return until the socket has been closed. Once it has returned, any use of this FlicClient is illegal. """ self._handle_event_thread_ident = threading.get_ident() while not self._closed: if not self._handle_one_event(): break self._sock.close() pyflic-2.0.4/pyflic/asyncio.py000066400000000000000000000662451465674166500163510ustar00rootroot00000000000000"""Flic client library for python Requires python 3.3 or higher. For detailed documentation, see the protocol documentation. Notes on the data type used in this python implementation compared to the protocol documentation: All kind of integers are represented as python integers. Booleans use the Boolean type. Enums use the defined python enums below. Bd addr are represented as standard python strings, e.g. "aa:bb:cc:dd:ee:ff". """ import asyncio from enum import Enum from collections import namedtuple import time import struct import itertools class CreateConnectionChannelError(Enum): NoError = 0 MaxPendingConnectionsReached = 1 class ConnectionStatus(Enum): Disconnected = 0 Connected = 1 Ready = 2 class DisconnectReason(Enum): Unspecified = 0 ConnectionEstablishmentFailed = 1 TimedOut = 2 BondingKeysMismatch = 3 class RemovedReason(Enum): RemovedByThisClient = 0 ForceDisconnectedByThisClient = 1 ForceDisconnectedByOtherClient = 2 ButtonIsPrivate = 3 VerifyTimeout = 4 InternetBackendError = 5 InvalidData = 6 CouldntLoadDevice = 7 DeletedByThisClient = 8 DeletedByOtherClient = 9 ButtonBelongsToOtherPartner = 10 DeletedFromButton = 11 class ClickType(Enum): ButtonDown = 0 ButtonUp = 1 ButtonClick = 2 ButtonSingleClick = 3 ButtonDoubleClick = 4 ButtonHold = 5 class BdAddrType(Enum): PublicBdAddrType = 0 RandomBdAddrType = 1 class LatencyMode(Enum): NormalLatency = 0 LowLatency = 1 HighLatency = 2 class BluetoothControllerState(Enum): Detached = 0 Resetting = 1 Attached = 2 class ScanWizardResult(Enum): WizardSuccess = 0 WizardCancelledByUser = 1 WizardFailedTimeout = 2 WizardButtonIsPrivate = 3 WizardBluetoothUnavailable = 4 WizardInternetBackendError = 5 WizardInvalidData = 6 WizardButtonBelongsToOtherPartner = 7 WizardButtonAlreadyConnectedToOtherDevice = 8 class ButtonScanner: """ButtonScanner class. Usage: scanner = ButtonScanner() scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified, already_connected_to_this_device, already_connected_to_other_device: ... client.add_scanner(scanner) """ _cnt = itertools.count() def __init__(self): self._scan_id = next(ButtonScanner._cnt) self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified, already_connected_to_this_device, already_connected_to_other_device: None class ScanWizard: """ScanWizard class Usage: wizard = ScanWizard() wizard.on_found_private_button = lambda scan_wizard: ... wizard.on_found_public_button = lambda scan_wizard, bd_addr, name: ... wizard.on_button_connected = lambda scan_wizard, bd_addr, name: ... wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ... client.add_scan_wizard(wizard) """ _cnt = itertools.count() def __init__(self): self._scan_wizard_id = next(ScanWizard._cnt) self._bd_addr = None self._name = None self.on_found_private_button = lambda scan_wizard: None self.on_found_public_button = lambda scan_wizard, bd_addr, name: None self.on_button_connected = lambda scan_wizard, bd_addr, name: None self.on_completed = lambda scan_wizard, result, bd_addr, name: None class BatteryStatusListener: """BatteryStatusListener class Usage: listener = BatteryStatusListener(bd_addr) listener.on_battery_status = lambda battery_status_listener, bd_addr, battery_percentage, timestamp: ... client.add_battery_status_listener(listener) """ _cnt = itertools.count() def __init__(self, bd_addr): self._listener_id = next(BatteryStatusListener._cnt) self._bd_addr = bd_addr self.on_battery_status = lambda battery_status_listener, battery_percentage, timestamp: None @property def bd_addr(self): return self._bd_addr class ButtonConnectionChannel: """ButtonConnectionChannel class. This class represents a connection channel to a Flic button. Add this button connection channel to a FlicClient by executing client.add_connection_channel(connection_channel). You may only have this connection channel added to one FlicClient at a time. Before you add the connection channel to the client, you should set up your callback functions by assigning the corresponding properties to this object with a function. Each callback function has a channel parameter as the first one, referencing this object. Available properties and the function parameters are: on_create_connection_channel_response: channel, error, connection_status on_removed: channel, removed_reason on_connection_status_changed: channel, connection_status, disconnect_reason on_button_up_or_down / on_button_click_or_hold / on_button_single_or_double_click / on_button_single_or_double_click_or_hold: channel, click_type, was_queued, time_diff """ _cnt = itertools.count() def __init__(self, bd_addr, latency_mode = LatencyMode.NormalLatency, auto_disconnect_time = 511): self._conn_id = next(ButtonConnectionChannel._cnt) self._bd_addr = bd_addr self._latency_mode = latency_mode self._auto_disconnect_time = auto_disconnect_time self._client = None self.on_create_connection_channel_response = lambda channel, error, connection_status: None self.on_removed = lambda channel, removed_reason: None self.on_connection_status_changed = lambda channel, connection_status, disconnect_reason: None self.on_button_up_or_down = lambda channel, click_type, was_queued, time_diff: None self.on_button_click_or_hold = lambda channel, click_type, was_queued, time_diff: None self.on_button_single_or_double_click = lambda channel, click_type, was_queued, time_diff: None self.on_button_single_or_double_click_or_hold = lambda channel, click_type, was_queued, time_diff: None @property def bd_addr(self): return self._bd_addr @property def latency_mode(self): return self._latency_mode @latency_mode.setter def latency_mode(self, latency_mode): if self._client is None: self._latency_mode = latency_mode return self._latency_mode = latency_mode if not self._client._closed: self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time}) @property def auto_disconnect_time(self): return self._auto_disconnect_time @auto_disconnect_time.setter def auto_disconnect_time(self, auto_disconnect_time): if self._client is None: self._auto_disconnect_time = auto_disconnect_time return self._auto_disconnect_time = auto_disconnect_time if not self._client._closed: self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time}) class FlicClient(asyncio.Protocol): """FlicClient class. When this class is constructed, a socket connection is established. You may then send commands to the server and set timers. Once you are ready with the initialization you must call the handle_events() method which is a main loop that never exits, unless the socket is closed. For a more detailed description of all commands, events and enums, check the protocol specification. All commands are wrapped in more high level functions and events are reported using callback functions. All methods called on this class will take effect only if you eventually call the handle_events() method. The ButtonScanner is used to set up a handler for advertisement packets. The ButtonConnectionChannel is used to interact with connections to flic buttons and receive their events. Other events are handled by the following callback functions that can be assigned to this object (and a list of the callback function parameters): on_new_verified_button: bd_addr on_no_space_for_new_connection: max_concurrently_connected_buttons on_got_space_for_new_connection: max_concurrently_connected_buttons on_bluetooth_controller_state_change: state """ _EVENTS = [ ("EvtAdvertisementPacket", "> 8 bytes[2] = opcode bytes += data_bytes self.transport.write(bytes) def _dispatch_event(self, data): if len(data) == 0: return opcode = data[0] if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] == None: return event_name = FlicClient._EVENTS[opcode][0] data_tuple = FlicClient._EVENT_STRUCTS[opcode].unpack(data[1 : 1 + FlicClient._EVENT_STRUCTS[opcode].size]) items = FlicClient._EVENT_NAMED_TUPLES[opcode]._make(data_tuple)._asdict() # Process some kind of items whose data type is not supported by struct if "bd_addr" in items: items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"]) if "name" in items: items["name"] = items["name"].decode("utf-8") if event_name == "EvtCreateConnectionChannelResponse": items["error"] = CreateConnectionChannelError(items["error"]) items["connection_status"] = ConnectionStatus(items["connection_status"]) if event_name == "EvtConnectionStatusChanged": items["connection_status"] = ConnectionStatus(items["connection_status"]) items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"]) if event_name == "EvtConnectionChannelRemoved": items["removed_reason"] = RemovedReason(items["removed_reason"]) if event_name.startswith("EvtButton"): items["click_type"] = ClickType(items["click_type"]) if event_name == "EvtGetInfoResponse": items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"]) items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["my_bd_addr"]) items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"]) items["bd_addr_of_verified_buttons"] = [] pos = FlicClient._EVENT_STRUCTS[opcode].size for i in range(items["nb_verified_buttons"]): items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6])) pos += 6 if event_name == "EvtBluetoothControllerStateChange": items["state"] = BluetoothControllerState(items["state"]) if event_name == "EvtGetButtonInfoResponse": items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"])) if items["uuid"] == "00000000000000000000000000000000": items["uuid"] = None items["color"] = items["color"].decode("utf-8") if items["color"] == "": items["color"] = None items["serial_number"] = items["serial_number"].decode("utf-8") if items["serial_number"] == "": items["serial_number"] = None if event_name == "EvtScanWizardCompleted": items["result"] = ScanWizardResult(items["result"]) # Process event if event_name == "EvtAdvertisementPacket": scanner = self._scanners.get(items["scan_id"]) if scanner is not None: scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"], items["is_private"], items["already_verified"]) if event_name == "EvtCreateConnectionChannelResponse": channel = self._connection_channels[items["conn_id"]] if items["error"] != CreateConnectionChannelError.NoError: del self._connection_channels[items["conn_id"]] channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"]) if event_name == "EvtConnectionStatusChanged": channel = self._connection_channels[items["conn_id"]] channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"]) if event_name == "EvtConnectionChannelRemoved": channel = self._connection_channels[items["conn_id"]] del self._connection_channels[items["conn_id"]] channel.on_removed(channel, items["removed_reason"]) if event_name == "EvtButtonUpOrDown": channel = self._connection_channels[items["conn_id"]] channel.on_button_up_or_down(channel, items["click_type"], items["was_queued"], items["time_diff"]) if event_name == "EvtButtonClickOrHold": channel = self._connection_channels[items["conn_id"]] channel.on_button_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"]) if event_name == "EvtButtonSingleOrDoubleClick": channel = self._connection_channels[items["conn_id"]] channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"], items["time_diff"]) if event_name == "EvtButtonSingleOrDoubleClickOrHold": channel = self._connection_channels[items["conn_id"]] channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"]) if event_name == "EvtNewVerifiedButton": self.on_new_verified_button(items["bd_addr"]) if event_name == "EvtGetInfoResponse": self.on_get_info(items) if event_name == "EvtNoSpaceForNewConnection": self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"]) if event_name == "EvtGotSpaceForNewConnection": self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"]) if event_name == "EvtBluetoothControllerStateChange": self.on_bluetooth_controller_state_change(items["state"]) if event_name == "EvtGetButtonInfoResponse": self._get_button_info_queue.get()(items["bd_addr"], items["uuid"], items["color"], items["serial_number"], items["flic_version"], items["firmware_version"]) if event_name == "EvtScanWizardFoundPrivateButton": scan_wizard = self._scan_wizards[items["scan_wizard_id"]] scan_wizard.on_found_private_button(scan_wizard) if event_name == "EvtScanWizardFoundPublicButton": scan_wizard = self._scan_wizards[items["scan_wizard_id"]] scan_wizard._bd_addr = items["bd_addr"] scan_wizard._name = items["name"] scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name) if event_name == "EvtScanWizardButtonConnected": scan_wizard = self._scan_wizards[items["scan_wizard_id"]] scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name) if event_name == "EvtScanWizardCompleted": scan_wizard = self._scan_wizards[items["scan_wizard_id"]] del self._scan_wizards[items["scan_wizard_id"]] scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name) def data_received(self,data): cdata=self.buffer+data self.buffer=b"" while len(cdata): packet_len = cdata[0] | (cdata[1] << 8) packet_len += 2 if len(cdata)>= packet_len: self._dispatch_event(cdata[2:packet_len]) cdata=cdata[packet_len:] else: if len(cdata): self.buffer=cdata #unlikely to happen but..... break pyflic-2.0.4/setup.cfg000066400000000000000000000000471465674166500146510ustar00rootroot00000000000000[metadata] description-file = README.mdpyflic-2.0.4/setup.py000066400000000000000000000007661465674166500145520ustar00rootroot00000000000000from setuptools import setup, find_packages with open('README.md', 'r') as f: long_description = f.read() setup(name='pyflic', version='2.0.4', description='Python library to connect to and interact with Flic buttons.', long_description=long_description, long_description_content_type='text/markdown', author='soldag', author_email='soeren_oldag@freenet.de', url='https://github.com/soldag/pyflic', license='MIT', packages=find_packages())