forked from platypush/platypush
parent
765ac6143e
commit
c7b0440562
17 changed files with 593 additions and 908 deletions
|
@ -6,7 +6,6 @@ Backends
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
:caption: Backends:
|
:caption: Backends:
|
||||||
|
|
||||||
platypush/backend/button.flic.rst
|
|
||||||
platypush/backend/chat.telegram.rst
|
platypush/backend/chat.telegram.rst
|
||||||
platypush/backend/http.rst
|
platypush/backend/http.rst
|
||||||
platypush/backend/midi.rst
|
platypush/backend/midi.rst
|
||||||
|
|
|
@ -11,7 +11,6 @@ Events
|
||||||
platypush/events/application.rst
|
platypush/events/application.rst
|
||||||
platypush/events/assistant.rst
|
platypush/events/assistant.rst
|
||||||
platypush/events/bluetooth.rst
|
platypush/events/bluetooth.rst
|
||||||
platypush/events/button.flic.rst
|
|
||||||
platypush/events/camera.rst
|
platypush/events/camera.rst
|
||||||
platypush/events/chat.slack.rst
|
platypush/events/chat.slack.rst
|
||||||
platypush/events/chat.telegram.rst
|
platypush/events/chat.telegram.rst
|
||||||
|
@ -21,6 +20,7 @@ Events
|
||||||
platypush/events/distance.rst
|
platypush/events/distance.rst
|
||||||
platypush/events/entities.rst
|
platypush/events/entities.rst
|
||||||
platypush/events/file.rst
|
platypush/events/file.rst
|
||||||
|
platypush/events/flic.rst
|
||||||
platypush/events/foursquare.rst
|
platypush/events/foursquare.rst
|
||||||
platypush/events/geo.rst
|
platypush/events/geo.rst
|
||||||
platypush/events/github.rst
|
platypush/events/github.rst
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
``button.flic``
|
|
||||||
=================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.button.flic
|
|
||||||
:members:
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
``button.flic``
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.button.flic
|
|
||||||
:members:
|
|
||||||
|
|
5
docs/source/platypush/events/flic.rst
Normal file
5
docs/source/platypush/events/flic.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``event.flic``
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.event.flic
|
||||||
|
:members:
|
5
docs/source/platypush/plugins/flic.rst
Normal file
5
docs/source/platypush/plugins/flic.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``flic``
|
||||||
|
========
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.flic
|
||||||
|
:members:
|
|
@ -35,6 +35,7 @@ Plugins
|
||||||
platypush/plugins/ffmpeg.rst
|
platypush/plugins/ffmpeg.rst
|
||||||
platypush/plugins/file.rst
|
platypush/plugins/file.rst
|
||||||
platypush/plugins/file.monitor.rst
|
platypush/plugins/file.monitor.rst
|
||||||
|
platypush/plugins/flic.rst
|
||||||
platypush/plugins/foursquare.rst
|
platypush/plugins/foursquare.rst
|
||||||
platypush/plugins/github.rst
|
platypush/plugins/github.rst
|
||||||
platypush/plugins/google.calendar.rst
|
platypush/plugins/google.calendar.rst
|
||||||
|
|
|
@ -1,131 +0,0 @@
|
||||||
from threading import Timer
|
|
||||||
from time import time
|
|
||||||
|
|
||||||
from platypush.backend import Backend
|
|
||||||
from platypush.message.event.button.flic import FlicButtonEvent
|
|
||||||
|
|
||||||
from .fliclib.fliclib import FlicClient, ButtonConnectionChannel, ClickType
|
|
||||||
|
|
||||||
|
|
||||||
class ButtonFlicBackend(Backend):
|
|
||||||
"""
|
|
||||||
Backend that listen for events from the Flic (https://flic.io/) bluetooth
|
|
||||||
smart buttons.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **fliclib** (https://github.com/50ButtonsEach/fliclib-linux-hci). For
|
|
||||||
the backend to work properly you need to have the ``flicd`` daemon
|
|
||||||
from the fliclib running, and you have to first pair the buttons with
|
|
||||||
your device using any of the scanners provided by the library.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
_long_press_timeout = 0.3
|
|
||||||
_btn_timeout = 0.5
|
|
||||||
ShortPressEvent = "ShortPressEvent"
|
|
||||||
LongPressEvent = "LongPressEvent"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
server='localhost',
|
|
||||||
long_press_timeout=_long_press_timeout,
|
|
||||||
btn_timeout=_btn_timeout,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
:param server: flicd server host (default: localhost)
|
|
||||||
:type server: str
|
|
||||||
|
|
||||||
:param long_press_timeout: How long you should press a button for a
|
|
||||||
press action to be considered "long press" (default: 0.3 secohds)
|
|
||||||
:type long_press_timeout: float
|
|
||||||
|
|
||||||
:param btn_timeout: How long since the last button release before
|
|
||||||
considering the user interaction completed (default: 0.5 seconds)
|
|
||||||
:type btn_timeout: float
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
self.server = server
|
|
||||||
self.client = FlicClient(server)
|
|
||||||
self.client.get_info(self._received_info())
|
|
||||||
self.client.on_new_verified_button = self._got_button()
|
|
||||||
|
|
||||||
self._long_press_timeout = long_press_timeout
|
|
||||||
self._btn_timeout = btn_timeout
|
|
||||||
self._btn_timer = None
|
|
||||||
self._btn_addr = None
|
|
||||||
self._down_pressed_time = None
|
|
||||||
self._cur_sequence = []
|
|
||||||
self.logger.info('Initialized Flic buttons backend on %s', self.server)
|
|
||||||
|
|
||||||
def _got_button(self):
|
|
||||||
def _f(bd_addr):
|
|
||||||
cc = ButtonConnectionChannel(bd_addr)
|
|
||||||
cc.on_button_up_or_down = (
|
|
||||||
lambda channel, click_type, was_queued, time_diff: self._on_event()(
|
|
||||||
bd_addr, channel, click_type, was_queued, time_diff
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.client.add_connection_channel(cc)
|
|
||||||
|
|
||||||
return _f
|
|
||||||
|
|
||||||
def _received_info(self):
|
|
||||||
def _f(items):
|
|
||||||
for bd_addr in items["bd_addr_of_verified_buttons"]:
|
|
||||||
self._got_button()(bd_addr)
|
|
||||||
|
|
||||||
return _f
|
|
||||||
|
|
||||||
def _on_btn_timeout(self):
|
|
||||||
def _f():
|
|
||||||
self.logger.info(
|
|
||||||
'Flic event triggered from %s: %s', self._btn_addr, self._cur_sequence
|
|
||||||
)
|
|
||||||
|
|
||||||
self.bus.post(
|
|
||||||
FlicButtonEvent(btn_addr=self._btn_addr, sequence=self._cur_sequence)
|
|
||||||
)
|
|
||||||
|
|
||||||
self._cur_sequence = []
|
|
||||||
|
|
||||||
return _f
|
|
||||||
|
|
||||||
def _on_event(self):
|
|
||||||
# _ = channel
|
|
||||||
# __ = time_diff
|
|
||||||
def _f(bd_addr, _, click_type, was_queued, __):
|
|
||||||
if was_queued:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._btn_timer:
|
|
||||||
self._btn_timer.cancel()
|
|
||||||
|
|
||||||
if click_type == ClickType.ButtonDown:
|
|
||||||
self._down_pressed_time = time()
|
|
||||||
return
|
|
||||||
|
|
||||||
btn_event = self.ShortPressEvent
|
|
||||||
if self._down_pressed_time:
|
|
||||||
if time() - self._down_pressed_time >= self._long_press_timeout:
|
|
||||||
btn_event = self.LongPressEvent
|
|
||||||
self._down_pressed_time = None
|
|
||||||
|
|
||||||
self._cur_sequence.append(btn_event)
|
|
||||||
|
|
||||||
self._btn_addr = bd_addr
|
|
||||||
self._btn_timer = Timer(self._btn_timeout, self._on_btn_timeout())
|
|
||||||
self._btn_timer.start()
|
|
||||||
|
|
||||||
return _f
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
super().run()
|
|
||||||
|
|
||||||
self.client.handle_events()
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,609 +0,0 @@
|
||||||
"""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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
class ButtonScanner:
|
|
||||||
"""ButtonScanner class.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
scanner = ButtonScanner()
|
|
||||||
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
|
|
||||||
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: 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 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.
|
|
||||||
|
|
||||||
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", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
|
|
||||||
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
|
|
||||||
("EvtConnectionStatusChanged", "<IBB", "conn_id connection_status disconnect_reason"),
|
|
||||||
("EvtConnectionChannelRemoved", "<IB", "conn_id removed_reason"),
|
|
||||||
("EvtButtonUpOrDown", "<IBBI", "conn_id click_type was_queued time_diff"),
|
|
||||||
("EvtButtonClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
|
|
||||||
("EvtButtonSingleOrDoubleClick", "<IBBI", "conn_id click_type was_queued time_diff"),
|
|
||||||
("EvtButtonSingleOrDoubleClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
|
|
||||||
("EvtNewVerifiedButton", "<6s", "bd_addr"),
|
|
||||||
("EvtGetInfoResponse", "<B6sBBhBBH", "bluetooth_controller_state my_bd_addr my_bd_addr_type max_pending_connections max_concurrently_connected_buttons current_pending_connections currently_no_space_for_new_connection nb_verified_buttons"),
|
|
||||||
("EvtNoSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
|
||||||
("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
|
||||||
("EvtBluetoothControllerStateChange", "<B", "state"),
|
|
||||||
("EvtPingResponse", "<I", "ping_id"),
|
|
||||||
("EvtGetButtonUUIDResponse", "<6s16s", "bd_addr uuid"),
|
|
||||||
("EvtScanWizardFoundPrivateButton", "<I", "scan_wizard_id"),
|
|
||||||
("EvtScanWizardFoundPublicButton", "<I6s17p", "scan_wizard_id bd_addr name"),
|
|
||||||
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
|
||||||
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
|
|
||||||
]
|
|
||||||
_EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _EVENTS))
|
|
||||||
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x is None else namedtuple(x[0], x[2]), _EVENTS))
|
|
||||||
|
|
||||||
_COMMANDS = [
|
|
||||||
("CmdGetInfo", "", ""),
|
|
||||||
("CmdCreateScanner", "<I", "scan_id"),
|
|
||||||
("CmdRemoveScanner", "<I", "scan_id"),
|
|
||||||
("CmdCreateConnectionChannel", "<I6sBh", "conn_id bd_addr latency_mode auto_disconnect_time"),
|
|
||||||
("CmdRemoveConnectionChannel", "<I", "conn_id"),
|
|
||||||
("CmdForceDisconnect", "<6s", "bd_addr"),
|
|
||||||
("CmdChangeModeParameters", "<IBh", "conn_id latency_mode auto_disconnect_time"),
|
|
||||||
("CmdPing", "<I", "ping_id"),
|
|
||||||
("CmdGetButtonUUID", "<6s", "bd_addr"),
|
|
||||||
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
|
||||||
("CmdCancelScanWizard", "<I", "scan_wizard_id")
|
|
||||||
]
|
|
||||||
|
|
||||||
_COMMAND_STRUCTS = list(map(lambda x: struct.Struct(x[1]), _COMMANDS))
|
|
||||||
_COMMAND_NAMED_TUPLES = list(map(lambda x: namedtuple(x[0], x[2]), _COMMANDS))
|
|
||||||
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
|
||||||
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _bdaddr_string_to_bytes(bdaddr_string):
|
|
||||||
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
|
||||||
|
|
||||||
def __init__(self, host, port = 5551):
|
|
||||||
self._sock = socket.create_connection((host, port), None)
|
|
||||||
self._lock = threading.RLock()
|
|
||||||
self._scanners = {}
|
|
||||||
self._scan_wizards = {}
|
|
||||||
self._connection_channels = {}
|
|
||||||
self._get_info_response_queue = queue.Queue()
|
|
||||||
self._get_button_uuid_queue = queue.Queue()
|
|
||||||
self._timers = queue.PriorityQueue()
|
|
||||||
self._handle_event_thread_ident = None
|
|
||||||
self._closed = False
|
|
||||||
|
|
||||||
self.on_new_verified_button = lambda bd_addr: None
|
|
||||||
self.on_no_space_for_new_connection = lambda max_concurrently_connected_buttons: None
|
|
||||||
self.on_got_space_for_new_connection = lambda max_concurrently_connected_buttons: None
|
|
||||||
self.on_bluetooth_controller_state_change = lambda state: None
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Closes the client. The handle_events() method will return."""
|
|
||||||
with self._lock:
|
|
||||||
if self._closed:
|
|
||||||
return
|
|
||||||
|
|
||||||
if threading.get_ident() != self._handle_event_thread_ident:
|
|
||||||
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
|
||||||
|
|
||||||
self._closed = True
|
|
||||||
|
|
||||||
def add_scanner(self, scanner):
|
|
||||||
"""Add a ButtonScanner object.
|
|
||||||
|
|
||||||
The scan will start directly once the scanner is added.
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
if scanner._scan_id in self._scanners:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._scanners[scanner._scan_id] = scanner
|
|
||||||
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
|
|
||||||
|
|
||||||
def remove_scanner(self, scanner):
|
|
||||||
"""Remove a ButtonScanner object.
|
|
||||||
|
|
||||||
You will no longer receive advertisement packets.
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
if scanner._scan_id not in self._scanners:
|
|
||||||
return
|
|
||||||
|
|
||||||
del self._scanners[scanner._scan_id]
|
|
||||||
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
|
|
||||||
|
|
||||||
def add_scan_wizard(self, scan_wizard):
|
|
||||||
"""Add a ScanWizard object.
|
|
||||||
|
|
||||||
The scan wizard will start directly once the scan wizard is added.
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
|
||||||
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
|
||||||
|
|
||||||
def cancel_scan_wizard(self, scan_wizard):
|
|
||||||
"""Cancel a ScanWizard.
|
|
||||||
|
|
||||||
Note: The effect of this command will take place at the time the on_completed event arrives on the scan wizard object.
|
|
||||||
If cancelled due to this command, "result" in the on_completed event will be "WizardCancelledByUser".
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
if scan_wizard._scan_wizard_id not in self._scan_wizards:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
|
||||||
|
|
||||||
def add_connection_channel(self, channel):
|
|
||||||
"""Adds a connection channel to a specific Flic button.
|
|
||||||
|
|
||||||
This will start listening for a specific Flic button's connection and button events.
|
|
||||||
Make sure the Flic is either in public mode (by holding it down for 7 seconds) or already verified before calling this method.
|
|
||||||
|
|
||||||
The on_create_connection_channel_response callback property will be called on the
|
|
||||||
connection channel after this command has been received by the server.
|
|
||||||
|
|
||||||
You may have as many connection channels as you wish for a specific Flic Button.
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
if channel._conn_id in self._connection_channels:
|
|
||||||
return
|
|
||||||
|
|
||||||
channel._client = self
|
|
||||||
|
|
||||||
self._connection_channels[channel._conn_id] = channel
|
|
||||||
self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr, "latency_mode": channel._latency_mode, "auto_disconnect_time": channel._auto_disconnect_time})
|
|
||||||
|
|
||||||
def remove_connection_channel(self, channel):
|
|
||||||
"""Remove a connection channel.
|
|
||||||
|
|
||||||
This will stop listening for new events for a specific connection channel that has previously been added.
|
|
||||||
Note: The effect of this command will take place at the time the on_removed event arrives on the connection channel object.
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
if channel._conn_id not in self._connection_channels:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
|
|
||||||
|
|
||||||
def force_disconnect(self, bd_addr):
|
|
||||||
"""Force disconnection or cancel pending connection of a specific Flic button.
|
|
||||||
|
|
||||||
This removes all connection channels for all clients connected to the server for this specific Flic button.
|
|
||||||
"""
|
|
||||||
self._send_command("CmdForceDisconnect", {"bd_addr": bd_addr})
|
|
||||||
|
|
||||||
def get_info(self, callback):
|
|
||||||
"""Get info about the current state of the server.
|
|
||||||
|
|
||||||
The server will send back its information directly and the callback will be called once the response arrives.
|
|
||||||
The callback takes only one parameter: info. This info parameter is a dictionary with the following objects:
|
|
||||||
bluetooth_controller_state, my_bd_addr, my_bd_addr_type, max_pending_connections, max_concurrently_connected_buttons,
|
|
||||||
current_pending_connections, currently_no_space_for_new_connection, bd_addr_of_verified_buttons (a list of bd addresses).
|
|
||||||
"""
|
|
||||||
self._get_info_response_queue.put(callback)
|
|
||||||
self._send_command("CmdGetInfo", {})
|
|
||||||
|
|
||||||
def get_button_uuid(self, bd_addr, callback):
|
|
||||||
"""Get button uuid for a verified button.
|
|
||||||
|
|
||||||
The server will send back its information directly and the callback will be called once the response arrives.
|
|
||||||
Responses will arrive in the same order as requested.
|
|
||||||
|
|
||||||
The callback takes two parameters: bd_addr, uuid (hex string of 32 characters).
|
|
||||||
|
|
||||||
Note: if the button isn't verified, the uuid sent to the callback will rather be None.
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
self._get_button_uuid_queue.put(callback)
|
|
||||||
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
|
|
||||||
|
|
||||||
def set_timer(self, timeout_millis, callback):
|
|
||||||
"""Set a timer
|
|
||||||
|
|
||||||
This timer callback will run after the specified timeout_millis on the thread that handles the events.
|
|
||||||
"""
|
|
||||||
point_in_time = time.monotonic() + timeout_millis / 1000.0
|
|
||||||
self._timers.put((point_in_time, callback))
|
|
||||||
|
|
||||||
if threading.get_ident() != self._handle_event_thread_ident:
|
|
||||||
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
|
||||||
|
|
||||||
def run_on_handle_events_thread(self, callback):
|
|
||||||
"""Run a function on the thread that handles the events."""
|
|
||||||
if threading.get_ident() == self._handle_event_thread_ident:
|
|
||||||
callback()
|
|
||||||
else:
|
|
||||||
self.set_timer(0, callback)
|
|
||||||
|
|
||||||
def _send_command(self, name, items):
|
|
||||||
for key, value in items.items():
|
|
||||||
if isinstance(value, Enum):
|
|
||||||
items[key] = value.value
|
|
||||||
|
|
||||||
if "bd_addr" in items:
|
|
||||||
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes(items["bd_addr"])
|
|
||||||
|
|
||||||
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
|
|
||||||
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
|
|
||||||
bytes = bytearray(3)
|
|
||||||
bytes[0] = (len(data_bytes) + 1) & 0xff
|
|
||||||
bytes[1] = (len(data_bytes) + 1) >> 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] is 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 == "EvtGetButtonUUIDResponse":
|
|
||||||
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
|
||||||
if items["uuid"] == "00000000000000000000000000000000":
|
|
||||||
items["uuid"] = 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._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 == "EvtGetButtonUUIDResponse":
|
|
||||||
self._get_button_uuid_queue.get()(items["bd_addr"], items["uuid"])
|
|
||||||
|
|
||||||
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 _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()
|
|
|
@ -1,9 +0,0 @@
|
||||||
manifest:
|
|
||||||
events:
|
|
||||||
platypush.message.event.button.flic.FlicButtonEvent: when a button is pressed.The
|
|
||||||
event will also contain the press sequence(e.g. ``["ShortPressEvent", "LongPressEvent",
|
|
||||||
"ShortPressEvent"]``)
|
|
||||||
install:
|
|
||||||
pip: []
|
|
||||||
package: platypush.backend.button.flic
|
|
||||||
type: backend
|
|
|
@ -1,34 +1,33 @@
|
||||||
|
from typing import Sequence
|
||||||
from platypush.message.event import Event, EventMatchResult
|
from platypush.message.event import Event, EventMatchResult
|
||||||
|
|
||||||
|
|
||||||
class FlicButtonEvent(Event):
|
class FlicButtonEvent(Event):
|
||||||
"""
|
"""
|
||||||
Event triggered when a sequence of user short/long presses is detected on a
|
Event triggered when a sequence of user short/long presses is detected on a
|
||||||
Flic button (https://flic.io).
|
`Flic button <https://flic.io>`_.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, btn_addr, sequence, *args, **kwargs):
|
def __init__(self, btn_addr: str, sequence: Sequence[str], *_, **kwargs):
|
||||||
"""
|
"""
|
||||||
:param btn_addr: Physical address of the button that originated the event
|
:param btn_addr: Physical address of the button that originated the
|
||||||
:type btn_addr: str
|
event.
|
||||||
|
:param sequence: Detected sequence, as a list of Flic button event
|
||||||
:param sequence: Detected sequence, as a list of Flic button event types (either "ShortPressEvent" or
|
types (either ``ShortPressEvent`` or ``LongPressEvent``).
|
||||||
"LongPressEvent")
|
|
||||||
:type sequence: list[str]
|
|
||||||
"""
|
"""
|
||||||
|
super().__init__(btn_addr=btn_addr, sequence=sequence, **kwargs)
|
||||||
super().__init__(btn_addr=btn_addr, sequence=sequence, *args, **kwargs)
|
|
||||||
|
|
||||||
def matches_condition(self, condition):
|
def matches_condition(self, condition):
|
||||||
"""
|
"""
|
||||||
:param condition: Condition to be checked against, as a sequence of button presses ("ShortPressEvent" and
|
:param condition: Condition to be checked against, as a sequence of
|
||||||
"LongPressEvent")
|
button presses (``ShortPressEvent`` and ``LongPressEvent``).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = EventMatchResult(is_match=False)
|
result = EventMatchResult(is_match=False)
|
||||||
|
|
||||||
if not isinstance(self, condition.type) \
|
if (
|
||||||
or self.args['btn_addr'] != condition.args['btn_addr']:
|
not isinstance(self, condition.type)
|
||||||
|
or self.args['btn_addr'] != condition.args['btn_addr']
|
||||||
|
):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
cond_sequence = list(condition.args['sequence'])
|
cond_sequence = list(condition.args['sequence'])
|
||||||
|
@ -47,4 +46,5 @@ class FlicButtonEvent(Event):
|
||||||
result.is_match = len(cond_sequence) == 0
|
result.is_match = len(cond_sequence) == 0
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
155
platypush/plugins/flic/__init__.py
Normal file
155
platypush/plugins/flic/__init__.py
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
from multiprocessing import Process
|
||||||
|
from threading import RLock, Timer
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from platypush.context import get_bus
|
||||||
|
from platypush.message.event.flic import FlicButtonEvent
|
||||||
|
from platypush.plugins import RunnablePlugin
|
||||||
|
|
||||||
|
from .fliclib import FlicClient, ButtonConnectionChannel, ClickType
|
||||||
|
|
||||||
|
|
||||||
|
class FlicPlugin(RunnablePlugin):
|
||||||
|
"""
|
||||||
|
This integration listens for events from the `Flic <https://flic.io/>`_
|
||||||
|
smart buttons over Bluetooth.
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
|
||||||
|
* **fliclib** (https://github.com/50ButtonsEach/fliclib-linux-hci).
|
||||||
|
|
||||||
|
Clone the repository and follow the instructions in the README. This plugin
|
||||||
|
requires:
|
||||||
|
|
||||||
|
* The ``flicd`` daemon to be running - either on the same machine or on
|
||||||
|
a machine that can be reached through the network.
|
||||||
|
|
||||||
|
* The buttons to be paired with the device running the ``flicd`` daemon.
|
||||||
|
The repository provides several scanners, but the easiest way is
|
||||||
|
probably through the ``new_scan_wizard.py`` script.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# Clone the repository
|
||||||
|
$ git clone https://github.com/50ButtonsEach/fliclib-linux-hci.git
|
||||||
|
# Run the flid daemon
|
||||||
|
$ [sudo] fliclib-linux-hci/bin/$(uname -m)/flicd -f /path/to/flicd.db &
|
||||||
|
# Run the new_scan_wizard.py script to pair the buttons
|
||||||
|
$ cd fliclib-linux-hci/clientlib/python
|
||||||
|
$ python3 new_scan_wizard.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_long_press_timeout = 0.3
|
||||||
|
_btn_timeout = 0.5
|
||||||
|
ShortPressEvent = "ShortPressEvent"
|
||||||
|
LongPressEvent = "LongPressEvent"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
server: str = 'localhost',
|
||||||
|
long_press_timeout: float = _long_press_timeout,
|
||||||
|
btn_timeout: float = _btn_timeout,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param server: flicd server host (default: localhost)
|
||||||
|
:param long_press_timeout: How long you should press a button for a
|
||||||
|
press action to be considered "long press" (default: 0.3 secohds)
|
||||||
|
:param btn_timeout: How long since the last button release before
|
||||||
|
considering the user interaction completed (default: 0.5 seconds)
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self.server = server
|
||||||
|
self._client = None
|
||||||
|
self._client_lock = RLock()
|
||||||
|
|
||||||
|
self._long_press_timeout = long_press_timeout
|
||||||
|
self._btn_timeout = btn_timeout
|
||||||
|
self._btn_timer = None
|
||||||
|
self._btn_addr = None
|
||||||
|
self._down_pressed_time = None
|
||||||
|
self._cur_sequence = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
with self._client_lock:
|
||||||
|
if not self._client:
|
||||||
|
self._client = FlicClient(self.server)
|
||||||
|
self._client.get_info(self._received_info)
|
||||||
|
self._client.on_new_verified_button = self._got_button
|
||||||
|
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
def _got_button(self, bd_addr):
|
||||||
|
cc = ButtonConnectionChannel(bd_addr)
|
||||||
|
cc.on_button_up_or_down = (
|
||||||
|
lambda channel, click_type, was_queued, time_diff: self._on_event(
|
||||||
|
bd_addr, channel, click_type, was_queued, time_diff
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.client.add_connection_channel(cc)
|
||||||
|
|
||||||
|
def _received_info(self, items):
|
||||||
|
for bd_addr in items["bd_addr_of_verified_buttons"]:
|
||||||
|
self._got_button(bd_addr)
|
||||||
|
|
||||||
|
def _on_btn_timeout(self):
|
||||||
|
if self._btn_addr:
|
||||||
|
get_bus().post(
|
||||||
|
FlicButtonEvent(
|
||||||
|
btn_addr=self._btn_addr,
|
||||||
|
sequence=self._cur_sequence,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._cur_sequence = []
|
||||||
|
|
||||||
|
# _ = channel
|
||||||
|
# __ = time_diff
|
||||||
|
def _on_event(self, bd_addr, _, click_type, was_queued, __):
|
||||||
|
if was_queued and self._btn_addr:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._btn_timer:
|
||||||
|
self._btn_timer.cancel()
|
||||||
|
|
||||||
|
if click_type == ClickType.ButtonDown:
|
||||||
|
self._down_pressed_time = time()
|
||||||
|
return
|
||||||
|
|
||||||
|
btn_event = self.ShortPressEvent
|
||||||
|
if self._down_pressed_time:
|
||||||
|
if time() - self._down_pressed_time >= self._long_press_timeout:
|
||||||
|
btn_event = self.LongPressEvent
|
||||||
|
self._down_pressed_time = None
|
||||||
|
|
||||||
|
self._cur_sequence.append(btn_event)
|
||||||
|
self._btn_addr = bd_addr
|
||||||
|
self._btn_timer = Timer(self._btn_timeout, self._on_btn_timeout)
|
||||||
|
self._btn_timer.start()
|
||||||
|
|
||||||
|
def _processor(self):
|
||||||
|
try:
|
||||||
|
self.client.handle_events()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def main(self):
|
||||||
|
while not self.should_stop():
|
||||||
|
proc = Process(target=self._processor, name='FlicProcessor')
|
||||||
|
proc.start()
|
||||||
|
self.wait_stop()
|
||||||
|
|
||||||
|
if proc.is_alive():
|
||||||
|
proc.terminate()
|
||||||
|
proc.join(2)
|
||||||
|
|
||||||
|
if proc.is_alive():
|
||||||
|
self.logger.warning('Flic processor still alive after termination')
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -10,11 +10,15 @@ Booleans use the Boolean type.
|
||||||
Enums use the defined python enums below.
|
Enums use the defined python enums below.
|
||||||
Bd addr are represented as standard python strings, e.g. "aa:bb:cc:dd:ee:ff".
|
Bd addr are represented as standard python strings, e.g. "aa:bb:cc:dd:ee:ff".
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
import select
|
||||||
import struct
|
import struct
|
||||||
import itertools
|
import itertools
|
||||||
|
import queue
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,6 +52,11 @@ class RemovedReason(Enum):
|
||||||
|
|
||||||
CouldntLoadDevice = 7
|
CouldntLoadDevice = 7
|
||||||
|
|
||||||
|
DeletedByThisClient = 8
|
||||||
|
DeletedByOtherClient = 9
|
||||||
|
ButtonBelongsToOtherPartner = 10
|
||||||
|
DeletedFromButton = 11
|
||||||
|
|
||||||
|
|
||||||
class ClickType(Enum):
|
class ClickType(Enum):
|
||||||
ButtonDown = 0
|
ButtonDown = 0
|
||||||
|
@ -83,6 +92,8 @@ class ScanWizardResult(Enum):
|
||||||
WizardBluetoothUnavailable = 4
|
WizardBluetoothUnavailable = 4
|
||||||
WizardInternetBackendError = 5
|
WizardInternetBackendError = 5
|
||||||
WizardInvalidData = 6
|
WizardInvalidData = 6
|
||||||
|
WizardButtonBelongsToOtherPartner = 7
|
||||||
|
WizardButtonAlreadyConnectedToOtherDevice = 8
|
||||||
|
|
||||||
|
|
||||||
class ButtonScanner:
|
class ButtonScanner:
|
||||||
|
@ -90,7 +101,7 @@ class ButtonScanner:
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
scanner = ButtonScanner()
|
scanner = ButtonScanner()
|
||||||
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
|
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)
|
client.add_scanner(scanner)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -98,7 +109,9 @@ class ButtonScanner:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._scan_id = next(ButtonScanner._cnt)
|
self._scan_id = next(ButtonScanner._cnt)
|
||||||
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
|
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:
|
class ScanWizard:
|
||||||
|
@ -125,6 +138,29 @@ class ScanWizard:
|
||||||
self.on_completed = lambda scan_wizard, result, 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:
|
class ButtonConnectionChannel:
|
||||||
"""ButtonConnectionChannel class.
|
"""ButtonConnectionChannel class.
|
||||||
|
|
||||||
|
@ -145,20 +181,34 @@ class ButtonConnectionChannel:
|
||||||
|
|
||||||
_cnt = itertools.count()
|
_cnt = itertools.count()
|
||||||
|
|
||||||
def __init__(self, bd_addr, latency_mode=LatencyMode.NormalLatency, auto_disconnect_time=511):
|
def __init__(
|
||||||
|
self, bd_addr, latency_mode=LatencyMode.NormalLatency, auto_disconnect_time=511
|
||||||
|
):
|
||||||
self._conn_id = next(ButtonConnectionChannel._cnt)
|
self._conn_id = next(ButtonConnectionChannel._cnt)
|
||||||
self._bd_addr = bd_addr
|
self._bd_addr = bd_addr
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
|
self.on_create_connection_channel_response = (
|
||||||
|
lambda channel, error, connection_status: None
|
||||||
|
)
|
||||||
self.on_removed = lambda channel, removed_reason: None
|
self.on_removed = lambda channel, removed_reason: None
|
||||||
self.on_connection_status_changed = lambda channel, connection_status, disconnect_reason: None
|
self.on_connection_status_changed = (
|
||||||
self.on_button_up_or_down = lambda channel, click_type, was_queued, time_diff: None
|
lambda channel, connection_status, disconnect_reason: 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_up_or_down = (
|
||||||
self.on_button_single_or_double_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
|
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
|
@property
|
||||||
def bd_addr(self):
|
def bd_addr(self):
|
||||||
|
@ -174,11 +224,17 @@ class ButtonConnectionChannel:
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
return
|
return
|
||||||
|
|
||||||
|
with self._client._lock:
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
if not self._client._closed:
|
if not self._client._closed:
|
||||||
self._client._send_command("CmdChangeModeParameters",
|
self._client._send_command(
|
||||||
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
|
"CmdChangeModeParameters",
|
||||||
"auto_disconnect_time": self._auto_disconnect_time})
|
{
|
||||||
|
"conn_id": self._conn_id,
|
||||||
|
"latency_mode": self._latency_mode,
|
||||||
|
"auto_disconnect_time": self._auto_disconnect_time,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_disconnect_time(self):
|
def auto_disconnect_time(self):
|
||||||
|
@ -190,14 +246,20 @@ class ButtonConnectionChannel:
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
return
|
return
|
||||||
|
|
||||||
|
with self._client._lock:
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
if not self._client._closed:
|
if not self._client._closed:
|
||||||
self._client._send_command("CmdChangeModeParameters",
|
self._client._send_command(
|
||||||
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
|
"CmdChangeModeParameters",
|
||||||
"auto_disconnect_time": self._auto_disconnect_time})
|
{
|
||||||
|
"conn_id": self._conn_id,
|
||||||
|
"latency_mode": self._latency_mode,
|
||||||
|
"auto_disconnect_time": self._auto_disconnect_time,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FlicClient(asyncio.Protocol):
|
class FlicClient:
|
||||||
"""FlicClient class.
|
"""FlicClient class.
|
||||||
|
|
||||||
When this class is constructed, a socket connection is established.
|
When this class is constructed, a socket connection is established.
|
||||||
|
@ -211,6 +273,7 @@ class FlicClient(asyncio.Protocol):
|
||||||
|
|
||||||
The ButtonScanner is used to set up a handler for advertisement packets.
|
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 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):
|
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_new_verified_button: bd_addr
|
||||||
|
@ -220,83 +283,132 @@ class FlicClient(asyncio.Protocol):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_EVENTS = [
|
_EVENTS = [
|
||||||
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
|
(
|
||||||
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
|
"EvtAdvertisementPacket",
|
||||||
("EvtConnectionStatusChanged", "<IBB", "conn_id connection_status disconnect_reason"),
|
"<I6s17pb????",
|
||||||
|
"scan_id bd_addr name rssi is_private already_verified already_connected_to_this_device already_connected_to_other_device",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"EvtCreateConnectionChannelResponse",
|
||||||
|
"<IBB",
|
||||||
|
"conn_id error connection_status",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"EvtConnectionStatusChanged",
|
||||||
|
"<IBB",
|
||||||
|
"conn_id connection_status disconnect_reason",
|
||||||
|
),
|
||||||
("EvtConnectionChannelRemoved", "<IB", "conn_id removed_reason"),
|
("EvtConnectionChannelRemoved", "<IB", "conn_id removed_reason"),
|
||||||
("EvtButtonUpOrDown", "<IBBI", "conn_id click_type was_queued time_diff"),
|
("EvtButtonUpOrDown", "<IBBI", "conn_id click_type was_queued time_diff"),
|
||||||
("EvtButtonClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
|
("EvtButtonClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
|
||||||
("EvtButtonSingleOrDoubleClick", "<IBBI", "conn_id click_type was_queued time_diff"),
|
(
|
||||||
("EvtButtonSingleOrDoubleClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
|
"EvtButtonSingleOrDoubleClick",
|
||||||
|
"<IBBI",
|
||||||
|
"conn_id click_type was_queued time_diff",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"EvtButtonSingleOrDoubleClickOrHold",
|
||||||
|
"<IBBI",
|
||||||
|
"conn_id click_type was_queued time_diff",
|
||||||
|
),
|
||||||
("EvtNewVerifiedButton", "<6s", "bd_addr"),
|
("EvtNewVerifiedButton", "<6s", "bd_addr"),
|
||||||
("EvtGetInfoResponse", "<B6sBBhBBH",
|
(
|
||||||
"bluetooth_controller_state my_bd_addr my_bd_addr_type max_pending_connections max_concurrently_connected_buttons current_pending_connections currently_no_space_for_new_connection nb_verified_buttons"),
|
"EvtGetInfoResponse",
|
||||||
|
"<B6sBBhBBH",
|
||||||
|
"bluetooth_controller_state my_bd_addr my_bd_addr_type max_pending_connections max_concurrently_connected_buttons current_pending_connections currently_no_space_for_new_connection nb_verified_buttons",
|
||||||
|
),
|
||||||
("EvtNoSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
("EvtNoSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
||||||
("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
||||||
("EvtBluetoothControllerStateChange", "<B", "state"),
|
("EvtBluetoothControllerStateChange", "<B", "state"),
|
||||||
("EvtPingResponse", "<I", "ping_id"),
|
("EvtPingResponse", "<I", "ping_id"),
|
||||||
("EvtGetButtonUUIDResponse", "<6s16s", "bd_addr uuid"),
|
(
|
||||||
|
"EvtGetButtonInfoResponse",
|
||||||
|
"<6s16s17p17pBI",
|
||||||
|
"bd_addr uuid color serial_number flic_version firmware_version",
|
||||||
|
),
|
||||||
("EvtScanWizardFoundPrivateButton", "<I", "scan_wizard_id"),
|
("EvtScanWizardFoundPrivateButton", "<I", "scan_wizard_id"),
|
||||||
("EvtScanWizardFoundPublicButton", "<I6s17p", "scan_wizard_id bd_addr name"),
|
("EvtScanWizardFoundPublicButton", "<I6s17p", "scan_wizard_id bd_addr name"),
|
||||||
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
||||||
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
|
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result"),
|
||||||
|
("EvtButtonDeleted", "<6s?", "bd_addr deleted_by_this_client"),
|
||||||
|
("EvtBatteryStatus", "<Ibq", "listener_id battery_percentage timestamp"),
|
||||||
]
|
]
|
||||||
_EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _EVENTS))
|
_EVENT_STRUCTS = list(
|
||||||
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x is None else namedtuple(x[0], x[2]), _EVENTS))
|
map(lambda x: None if x == None else struct.Struct(x[1]), _EVENTS)
|
||||||
|
)
|
||||||
|
_EVENT_NAMED_TUPLES = list(
|
||||||
|
map(lambda x: None if x == None else namedtuple(x[0], x[2]), _EVENTS)
|
||||||
|
)
|
||||||
|
|
||||||
_COMMANDS = [
|
_COMMANDS = [
|
||||||
("CmdGetInfo", "", ""),
|
("CmdGetInfo", "", ""),
|
||||||
("CmdCreateScanner", "<I", "scan_id"),
|
("CmdCreateScanner", "<I", "scan_id"),
|
||||||
("CmdRemoveScanner", "<I", "scan_id"),
|
("CmdRemoveScanner", "<I", "scan_id"),
|
||||||
("CmdCreateConnectionChannel", "<I6sBh", "conn_id bd_addr latency_mode auto_disconnect_time"),
|
(
|
||||||
|
"CmdCreateConnectionChannel",
|
||||||
|
"<I6sBh",
|
||||||
|
"conn_id bd_addr latency_mode auto_disconnect_time",
|
||||||
|
),
|
||||||
("CmdRemoveConnectionChannel", "<I", "conn_id"),
|
("CmdRemoveConnectionChannel", "<I", "conn_id"),
|
||||||
("CmdForceDisconnect", "<6s", "bd_addr"),
|
("CmdForceDisconnect", "<6s", "bd_addr"),
|
||||||
("CmdChangeModeParameters", "<IBh", "conn_id latency_mode auto_disconnect_time"),
|
(
|
||||||
|
"CmdChangeModeParameters",
|
||||||
|
"<IBh",
|
||||||
|
"conn_id latency_mode auto_disconnect_time",
|
||||||
|
),
|
||||||
("CmdPing", "<I", "ping_id"),
|
("CmdPing", "<I", "ping_id"),
|
||||||
("CmdGetButtonUUID", "<6s", "bd_addr"),
|
("CmdGetButtonInfo", "<6s", "bd_addr"),
|
||||||
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
||||||
("CmdCancelScanWizard", "<I", "scan_wizard_id")
|
("CmdCancelScanWizard", "<I", "scan_wizard_id"),
|
||||||
|
("CmdDeleteButton", "<6s", "bd_addr"),
|
||||||
|
("CmdCreateBatteryStatusListener", "<I6s", "listener_id bd_addr"),
|
||||||
|
("CmdRemoveBatteryStatusListener", "<I", "listener_id"),
|
||||||
]
|
]
|
||||||
|
|
||||||
_COMMAND_STRUCTS = list(map(lambda x: struct.Struct(x[1]), _COMMANDS))
|
_COMMAND_STRUCTS = list(map(lambda x: struct.Struct(x[1]), _COMMANDS))
|
||||||
_COMMAND_NAMED_TUPLES = list(map(lambda x: namedtuple(x[0], x[2]), _COMMANDS))
|
_COMMAND_NAMED_TUPLES = list(map(lambda x: namedtuple(x[0], x[2]), _COMMANDS))
|
||||||
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
|
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
||||||
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _bdaddr_string_to_bytes(bdaddr_string):
|
def _bdaddr_string_to_bytes(bdaddr_string):
|
||||||
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
||||||
|
|
||||||
def __init__(self, loop, parent=None):
|
def __init__(self, host, port=5551):
|
||||||
self.loop = loop
|
self._sock = socket.create_connection((host, port), None)
|
||||||
self.buffer = b""
|
self._lock = threading.RLock()
|
||||||
self.transport = None
|
|
||||||
self.parent = parent
|
|
||||||
self._scanners = {}
|
self._scanners = {}
|
||||||
self._scan_wizards = {}
|
self._scan_wizards = {}
|
||||||
self._connection_channels = {}
|
self._connection_channels = {}
|
||||||
|
self._battery_status_listeners = {}
|
||||||
|
self._get_info_response_queue = queue.Queue()
|
||||||
|
self._get_button_info_queue = queue.Queue()
|
||||||
|
self._timers = queue.PriorityQueue()
|
||||||
|
self._handle_event_thread_ident = None
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|
||||||
self.on_new_verified_button = lambda bd_addr: None
|
self.on_new_verified_button = lambda bd_addr: None
|
||||||
self.on_no_space_for_new_connection = lambda max_concurrently_connected_buttons: None
|
self.on_no_space_for_new_connection = (
|
||||||
self.on_got_space_for_new_connection = lambda max_concurrently_connected_buttons: None
|
lambda max_concurrently_connected_buttons: None
|
||||||
|
)
|
||||||
|
self.on_got_space_for_new_connection = (
|
||||||
|
lambda max_concurrently_connected_buttons: None
|
||||||
|
)
|
||||||
self.on_bluetooth_controller_state_change = lambda state: None
|
self.on_bluetooth_controller_state_change = lambda state: None
|
||||||
self.on_get_info = lambda items: None
|
self.on_button_deleted = lambda bd_addr, deleted_by_this_client: None
|
||||||
self.on_get_button_uuid = lambda addr, uuid: None
|
|
||||||
|
|
||||||
def connection_made(self, transport):
|
|
||||||
self.transport = transport
|
|
||||||
if self.parent:
|
|
||||||
self.parent.register_protocol(self)
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Closes the client. The handle_events() method will return."""
|
"""Closes the client. The handle_events() method will return."""
|
||||||
|
with self._lock:
|
||||||
if self._closed:
|
if self._closed:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if threading.get_ident() != self._handle_event_thread_ident:
|
||||||
|
self._send_command(
|
||||||
|
"CmdPing", {"ping_id": 0}
|
||||||
|
) # To unblock socket select
|
||||||
|
|
||||||
self._closed = True
|
self._closed = True
|
||||||
|
|
||||||
def add_scanner(self, scanner):
|
def add_scanner(self, scanner):
|
||||||
|
@ -304,6 +416,7 @@ class FlicClient(asyncio.Protocol):
|
||||||
|
|
||||||
The scan will start directly once the scanner is added.
|
The scan will start directly once the scanner is added.
|
||||||
"""
|
"""
|
||||||
|
with self._lock:
|
||||||
if scanner._scan_id in self._scanners:
|
if scanner._scan_id in self._scanners:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -315,6 +428,7 @@ class FlicClient(asyncio.Protocol):
|
||||||
|
|
||||||
You will no longer receive advertisement packets.
|
You will no longer receive advertisement packets.
|
||||||
"""
|
"""
|
||||||
|
with self._lock:
|
||||||
if scanner._scan_id not in self._scanners:
|
if scanner._scan_id not in self._scanners:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -326,11 +440,14 @@ class FlicClient(asyncio.Protocol):
|
||||||
|
|
||||||
The scan wizard will start directly once the scan wizard is added.
|
The scan wizard will start directly once the scan wizard is added.
|
||||||
"""
|
"""
|
||||||
|
with self._lock:
|
||||||
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
||||||
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
self._send_command(
|
||||||
|
"CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id}
|
||||||
|
)
|
||||||
|
|
||||||
def cancel_scan_wizard(self, scan_wizard):
|
def cancel_scan_wizard(self, scan_wizard):
|
||||||
"""Cancel a ScanWizard.
|
"""Cancel a ScanWizard.
|
||||||
|
@ -338,10 +455,13 @@ class FlicClient(asyncio.Protocol):
|
||||||
Note: The effect of this command will take place at the time the on_completed event arrives on the scan wizard object.
|
Note: The effect of this command will take place at the time the on_completed event arrives on the scan wizard object.
|
||||||
If cancelled due to this command, "result" in the on_completed event will be "WizardCancelledByUser".
|
If cancelled due to this command, "result" in the on_completed event will be "WizardCancelledByUser".
|
||||||
"""
|
"""
|
||||||
|
with self._lock:
|
||||||
if scan_wizard._scan_wizard_id not in self._scan_wizards:
|
if scan_wizard._scan_wizard_id not in self._scan_wizards:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
self._send_command(
|
||||||
|
"CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id}
|
||||||
|
)
|
||||||
|
|
||||||
def add_connection_channel(self, channel):
|
def add_connection_channel(self, channel):
|
||||||
"""Adds a connection channel to a specific Flic button.
|
"""Adds a connection channel to a specific Flic button.
|
||||||
|
@ -354,15 +474,22 @@ class FlicClient(asyncio.Protocol):
|
||||||
|
|
||||||
You may have as many connection channels as you wish for a specific Flic Button.
|
You may have as many connection channels as you wish for a specific Flic Button.
|
||||||
"""
|
"""
|
||||||
|
with self._lock:
|
||||||
if channel._conn_id in self._connection_channels:
|
if channel._conn_id in self._connection_channels:
|
||||||
return
|
return
|
||||||
|
|
||||||
channel._client = self
|
channel._client = self
|
||||||
|
|
||||||
self._connection_channels[channel._conn_id] = channel
|
self._connection_channels[channel._conn_id] = channel
|
||||||
self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr,
|
self._send_command(
|
||||||
|
"CmdCreateConnectionChannel",
|
||||||
|
{
|
||||||
|
"conn_id": channel._conn_id,
|
||||||
|
"bd_addr": channel.bd_addr,
|
||||||
"latency_mode": channel._latency_mode,
|
"latency_mode": channel._latency_mode,
|
||||||
"auto_disconnect_time": channel._auto_disconnect_time})
|
"auto_disconnect_time": channel._auto_disconnect_time,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def remove_connection_channel(self, channel):
|
def remove_connection_channel(self, channel):
|
||||||
"""Remove a connection channel.
|
"""Remove a connection channel.
|
||||||
|
@ -370,10 +497,36 @@ class FlicClient(asyncio.Protocol):
|
||||||
This will stop listening for new events for a specific connection channel that has previously been added.
|
This will stop listening for new events for a specific connection channel that has previously been added.
|
||||||
Note: The effect of this command will take place at the time the on_removed event arrives on the connection channel object.
|
Note: The effect of this command will take place at the time the on_removed event arrives on the connection channel object.
|
||||||
"""
|
"""
|
||||||
|
with self._lock:
|
||||||
if channel._conn_id not in self._connection_channels:
|
if channel._conn_id not in self._connection_channels:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
|
self._send_command(
|
||||||
|
"CmdRemoveConnectionChannel", {"conn_id": channel._conn_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_battery_status_listener(self, listener):
|
||||||
|
"""Adds a battery status listener for a specific Flic button."""
|
||||||
|
with self._lock:
|
||||||
|
if listener._listener_id in self._battery_status_listeners:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._battery_status_listeners[listener._listener_id] = listener
|
||||||
|
self._send_command(
|
||||||
|
"CmdCreateBatteryStatusListener",
|
||||||
|
{"listener_id": listener._listener_id, "bd_addr": listener._bd_addr},
|
||||||
|
)
|
||||||
|
|
||||||
|
def remove_battery_status_listener(self, listener):
|
||||||
|
"""Remove a battery status listener."""
|
||||||
|
with self._lock:
|
||||||
|
if listener._listener_id not in self._battery_status_listeners:
|
||||||
|
return
|
||||||
|
|
||||||
|
del self._battery_status_listeners[listener._listener_id]
|
||||||
|
self._send_command(
|
||||||
|
"CmdRemoveBatteryStatusListener", {"listener_id": listener._listener_id}
|
||||||
|
)
|
||||||
|
|
||||||
def force_disconnect(self, bd_addr):
|
def force_disconnect(self, bd_addr):
|
||||||
"""Force disconnection or cancel pending connection of a specific Flic button.
|
"""Force disconnection or cancel pending connection of a specific Flic button.
|
||||||
|
@ -382,7 +535,7 @@ class FlicClient(asyncio.Protocol):
|
||||||
"""
|
"""
|
||||||
self._send_command("CmdForceDisconnect", {"bd_addr": bd_addr})
|
self._send_command("CmdForceDisconnect", {"bd_addr": bd_addr})
|
||||||
|
|
||||||
def get_info(self):
|
def get_info(self, callback):
|
||||||
"""Get info about the current state of the server.
|
"""Get info about the current state of the server.
|
||||||
|
|
||||||
The server will send back its information directly and the callback will be called once the response arrives.
|
The server will send back its information directly and the callback will be called once the response arrives.
|
||||||
|
@ -390,19 +543,37 @@ class FlicClient(asyncio.Protocol):
|
||||||
bluetooth_controller_state, my_bd_addr, my_bd_addr_type, max_pending_connections, max_concurrently_connected_buttons,
|
bluetooth_controller_state, my_bd_addr, my_bd_addr_type, max_pending_connections, max_concurrently_connected_buttons,
|
||||||
current_pending_connections, currently_no_space_for_new_connection, bd_addr_of_verified_buttons (a list of bd addresses).
|
current_pending_connections, currently_no_space_for_new_connection, bd_addr_of_verified_buttons (a list of bd addresses).
|
||||||
"""
|
"""
|
||||||
|
self._get_info_response_queue.put(callback)
|
||||||
self._send_command("CmdGetInfo", {})
|
self._send_command("CmdGetInfo", {})
|
||||||
|
|
||||||
def get_button_uuid(self, bd_addr):
|
def delete_button(self, bd_addr):
|
||||||
"""Get button uuid for a verified button.
|
"""Delete a verified button."""
|
||||||
|
self._send_command("CmdDeleteButton", {"bd_addr": bd_addr})
|
||||||
|
|
||||||
|
def get_button_info(self, bd_addr, callback):
|
||||||
|
"""Get button info for a verified button.
|
||||||
|
|
||||||
The server will send back its information directly and the callback will be called once the response arrives.
|
The server will send back its information directly and the callback will be called once the response arrives.
|
||||||
Responses will arrive in the same order as requested.
|
Responses will arrive in the same order as requested.
|
||||||
|
|
||||||
The callback takes two parameters: bd_addr, uuid (hex string of 32 characters).
|
The callback takes four parameters: bd_addr, uuid (hex string of 32 characters), color (string and None if unknown), serial_number, flic_version, firmware_version.
|
||||||
|
|
||||||
Note: if the button isn't verified, the uuid sent to the callback will rather be None.
|
Note: if the button isn't verified, the uuid sent to the callback will rather be None.
|
||||||
"""
|
"""
|
||||||
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
|
with self._lock:
|
||||||
|
self._get_button_info_queue.put(callback)
|
||||||
|
self._send_command("CmdGetButtonInfo", {"bd_addr": bd_addr})
|
||||||
|
|
||||||
|
def set_timer(self, timeout_millis, callback):
|
||||||
|
"""Set a timer
|
||||||
|
|
||||||
|
This timer callback will run after the specified timeout_millis on the thread that handles the events.
|
||||||
|
"""
|
||||||
|
point_in_time = time.monotonic() + timeout_millis / 1000.0
|
||||||
|
self._timers.put((point_in_time, callback))
|
||||||
|
|
||||||
|
if threading.get_ident() != self._handle_event_thread_ident:
|
||||||
|
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
||||||
|
|
||||||
def run_on_handle_events_thread(self, callback):
|
def run_on_handle_events_thread(self, callback):
|
||||||
"""Run a function on the thread that handles the events."""
|
"""Run a function on the thread that handles the events."""
|
||||||
|
@ -412,38 +583,43 @@ class FlicClient(asyncio.Protocol):
|
||||||
self.set_timer(0, callback)
|
self.set_timer(0, callback)
|
||||||
|
|
||||||
def _send_command(self, name, items):
|
def _send_command(self, name, items):
|
||||||
|
|
||||||
for key, value in items.items():
|
for key, value in items.items():
|
||||||
if isinstance(value, Enum):
|
if isinstance(value, Enum):
|
||||||
items[key] = value.value
|
items[key] = value.value
|
||||||
|
|
||||||
if "bd_addr" in items:
|
if "bd_addr" in items:
|
||||||
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes()
|
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes(items["bd_addr"])
|
||||||
|
|
||||||
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
|
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
|
||||||
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
|
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(
|
||||||
|
*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items)
|
||||||
|
)
|
||||||
bytes = bytearray(3)
|
bytes = bytearray(3)
|
||||||
bytes[0] = (len(data_bytes) + 1) & 0xff
|
bytes[0] = (len(data_bytes) + 1) & 0xFF
|
||||||
bytes[1] = (len(data_bytes) + 1) >> 8
|
bytes[1] = (len(data_bytes) + 1) >> 8
|
||||||
bytes[2] = opcode
|
bytes[2] = opcode
|
||||||
bytes += data_bytes
|
bytes += data_bytes
|
||||||
self.transport.write(bytes)
|
with self._lock:
|
||||||
|
if not self._closed:
|
||||||
|
self._sock.sendall(bytes)
|
||||||
|
|
||||||
def _dispatch_event(self, data):
|
def _dispatch_event(self, data):
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return
|
return
|
||||||
opcode = data[0]
|
opcode = data[0]
|
||||||
|
|
||||||
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] is None:
|
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] == None:
|
||||||
return
|
return
|
||||||
|
|
||||||
event_name = FlicClient._EVENTS[opcode][0]
|
event_name = FlicClient._EVENTS[opcode][0]
|
||||||
data_tuple = FlicClient._EVENT_STRUCTS[opcode].unpack(data[1: 1 + FlicClient._EVENT_STRUCTS[opcode].size])
|
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()
|
items = FlicClient._EVENT_NAMED_TUPLES[opcode]._make(data_tuple)._asdict()
|
||||||
|
|
||||||
# Process some kind of items whose data type is not supported by struct
|
# Process some kind of items whose data type is not supported by struct
|
||||||
if "bd_addr" in items:
|
if "bd_addr" in items:
|
||||||
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string()
|
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"])
|
||||||
|
|
||||||
if "name" in items:
|
if "name" in items:
|
||||||
items["name"] = items["name"].decode("utf-8")
|
items["name"] = items["name"].decode("utf-8")
|
||||||
|
@ -459,28 +635,44 @@ class FlicClient(asyncio.Protocol):
|
||||||
if event_name == "EvtConnectionChannelRemoved":
|
if event_name == "EvtConnectionChannelRemoved":
|
||||||
items["removed_reason"] = RemovedReason(items["removed_reason"])
|
items["removed_reason"] = RemovedReason(items["removed_reason"])
|
||||||
|
|
||||||
if event_name.startswith("EvtButton"):
|
if (
|
||||||
|
event_name == "EvtButtonUpOrDown"
|
||||||
|
or event_name == "EvtButtonClickOrHold"
|
||||||
|
or event_name == "EvtButtonSingleOrDoubleClick"
|
||||||
|
or event_name == "EvtButtonSingleOrDoubleClickOrHold"
|
||||||
|
):
|
||||||
items["click_type"] = ClickType(items["click_type"])
|
items["click_type"] = ClickType(items["click_type"])
|
||||||
|
|
||||||
if event_name == "EvtGetInfoResponse":
|
if event_name == "EvtGetInfoResponse":
|
||||||
items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"])
|
items["bluetooth_controller_state"] = BluetoothControllerState(
|
||||||
items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string()
|
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["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
|
||||||
items["bd_addr_of_verified_buttons"] = []
|
items["bd_addr_of_verified_buttons"] = []
|
||||||
|
|
||||||
pos = FlicClient._EVENT_STRUCTS[opcode].size
|
pos = FlicClient._EVENT_STRUCTS[opcode].size
|
||||||
for i in range(items["nb_verified_buttons"]):
|
for _ in range(items["nb_verified_buttons"]):
|
||||||
items["bd_addr_of_verified_buttons"].append(
|
items["bd_addr_of_verified_buttons"].append(
|
||||||
FlicClient._bdaddr_bytes_to_string())
|
FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6])
|
||||||
|
)
|
||||||
pos += 6
|
pos += 6
|
||||||
|
|
||||||
if event_name == "EvtBluetoothControllerStateChange":
|
if event_name == "EvtBluetoothControllerStateChange":
|
||||||
items["state"] = BluetoothControllerState(items["state"])
|
items["state"] = BluetoothControllerState(items["state"])
|
||||||
|
|
||||||
if event_name == "EvtGetButtonUUIDResponse":
|
if event_name == "EvtGetButtonInfoResponse":
|
||||||
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
||||||
if items["uuid"] == "00000000000000000000000000000000":
|
if items["uuid"] == "00000000000000000000000000000000":
|
||||||
items["uuid"] = None
|
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":
|
if event_name == "EvtScanWizardCompleted":
|
||||||
items["result"] = ScanWizardResult(items["result"])
|
items["result"] = ScanWizardResult(items["result"])
|
||||||
|
@ -489,18 +681,30 @@ class FlicClient(asyncio.Protocol):
|
||||||
if event_name == "EvtAdvertisementPacket":
|
if event_name == "EvtAdvertisementPacket":
|
||||||
scanner = self._scanners.get(items["scan_id"])
|
scanner = self._scanners.get(items["scan_id"])
|
||||||
if scanner is not None:
|
if scanner is not None:
|
||||||
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"],
|
scanner.on_advertisement_packet(
|
||||||
items["is_private"], items["already_verified"])
|
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":
|
if event_name == "EvtCreateConnectionChannelResponse":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
if items["error"] != CreateConnectionChannelError.NoError:
|
if items["error"] != CreateConnectionChannelError.NoError:
|
||||||
del self._connection_channels[items["conn_id"]]
|
del self._connection_channels[items["conn_id"]]
|
||||||
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
|
channel.on_create_connection_channel_response(
|
||||||
|
channel, items["error"], items["connection_status"]
|
||||||
|
)
|
||||||
|
|
||||||
if event_name == "EvtConnectionStatusChanged":
|
if event_name == "EvtConnectionStatusChanged":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
|
channel.on_connection_status_changed(
|
||||||
|
channel, items["connection_status"], items["disconnect_reason"]
|
||||||
|
)
|
||||||
|
|
||||||
if event_name == "EvtConnectionChannelRemoved":
|
if event_name == "EvtConnectionChannelRemoved":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
|
@ -509,36 +713,53 @@ class FlicClient(asyncio.Protocol):
|
||||||
|
|
||||||
if event_name == "EvtButtonUpOrDown":
|
if event_name == "EvtButtonUpOrDown":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_button_up_or_down(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
channel.on_button_up_or_down(
|
||||||
|
channel, items["click_type"], items["was_queued"], items["time_diff"]
|
||||||
|
)
|
||||||
if event_name == "EvtButtonClickOrHold":
|
if event_name == "EvtButtonClickOrHold":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_button_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
channel.on_button_click_or_hold(
|
||||||
|
channel, items["click_type"], items["was_queued"], items["time_diff"]
|
||||||
|
)
|
||||||
if event_name == "EvtButtonSingleOrDoubleClick":
|
if event_name == "EvtButtonSingleOrDoubleClick":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"],
|
channel.on_button_single_or_double_click(
|
||||||
items["time_diff"])
|
channel, items["click_type"], items["was_queued"], items["time_diff"]
|
||||||
|
)
|
||||||
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
|
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"],
|
channel.on_button_single_or_double_click_or_hold(
|
||||||
items["time_diff"])
|
channel, items["click_type"], items["was_queued"], items["time_diff"]
|
||||||
|
)
|
||||||
|
|
||||||
if event_name == "EvtNewVerifiedButton":
|
if event_name == "EvtNewVerifiedButton":
|
||||||
self.on_new_verified_button(items["bd_addr"])
|
self.on_new_verified_button(items["bd_addr"])
|
||||||
|
|
||||||
if event_name == "EvtGetInfoResponse":
|
if event_name == "EvtGetInfoResponse":
|
||||||
self.on_get_info(items)
|
self._get_info_response_queue.get()(items)
|
||||||
|
|
||||||
if event_name == "EvtNoSpaceForNewConnection":
|
if event_name == "EvtNoSpaceForNewConnection":
|
||||||
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
self.on_no_space_for_new_connection(
|
||||||
|
items["max_concurrently_connected_buttons"]
|
||||||
|
)
|
||||||
|
|
||||||
if event_name == "EvtGotSpaceForNewConnection":
|
if event_name == "EvtGotSpaceForNewConnection":
|
||||||
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
self.on_got_space_for_new_connection(
|
||||||
|
items["max_concurrently_connected_buttons"]
|
||||||
|
)
|
||||||
|
|
||||||
if event_name == "EvtBluetoothControllerStateChange":
|
if event_name == "EvtBluetoothControllerStateChange":
|
||||||
self.on_bluetooth_controller_state_change(items["state"])
|
self.on_bluetooth_controller_state_change(items["state"])
|
||||||
|
|
||||||
if event_name == "EvtGetButtonUUIDResponse":
|
if event_name == "EvtGetButtonInfoResponse":
|
||||||
self.on_get_button_uuid(items["bd_addr"], items["uuid"])
|
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":
|
if event_name == "EvtScanWizardFoundPrivateButton":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
|
@ -548,27 +769,76 @@ class FlicClient(asyncio.Protocol):
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard._bd_addr = items["bd_addr"]
|
scan_wizard._bd_addr = items["bd_addr"]
|
||||||
scan_wizard._name = items["name"]
|
scan_wizard._name = items["name"]
|
||||||
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
scan_wizard.on_found_public_button(
|
||||||
|
scan_wizard, scan_wizard._bd_addr, scan_wizard._name
|
||||||
|
)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardButtonConnected":
|
if event_name == "EvtScanWizardButtonConnected":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
scan_wizard.on_button_connected(
|
||||||
|
scan_wizard, scan_wizard._bd_addr, scan_wizard._name
|
||||||
|
)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardCompleted":
|
if event_name == "EvtScanWizardCompleted":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
del 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)
|
scan_wizard.on_completed(
|
||||||
|
scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name
|
||||||
|
)
|
||||||
|
|
||||||
def data_received(self, data):
|
if event_name == "EvtButtonDeleted":
|
||||||
cdata = self.buffer + data
|
self.on_button_deleted(items["bd_addr"], items["deleted_by_this_client"])
|
||||||
self.buffer = b""
|
|
||||||
while len(cdata):
|
if event_name == "EvtBatteryStatus":
|
||||||
packet_len = cdata[0] | (cdata[1] << 8)
|
listener = self._battery_status_listeners.get(items["listener_id"])
|
||||||
packet_len += 2
|
if listener is not None:
|
||||||
if len(cdata) >= packet_len:
|
listener.on_battery_status(
|
||||||
self._dispatch_event(cdata[2:packet_len])
|
listener, items["battery_percentage"], items["timestamp"]
|
||||||
cdata = cdata[packet_len:]
|
)
|
||||||
else:
|
|
||||||
if len(cdata):
|
def _handle_one_event(self):
|
||||||
self.buffer = cdata # unlikely to happen but.....
|
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
|
break
|
||||||
|
self._sock.close()
|
7
platypush/plugins/flic/manifest.yaml
Normal file
7
platypush/plugins/flic/manifest.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
manifest:
|
||||||
|
events:
|
||||||
|
- platypush.message.event.flic.FlicButtonEvent
|
||||||
|
install:
|
||||||
|
pip: []
|
||||||
|
package: platypush.plugins.flic
|
||||||
|
type: plugin
|
|
@ -17,6 +17,10 @@ description_file = README.md
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
|
exclude =
|
||||||
|
# Legacy library copied from the upstream repo
|
||||||
|
fliclib.py
|
||||||
|
|
||||||
extend-ignore =
|
extend-ignore =
|
||||||
E203
|
E203
|
||||||
W503
|
W503
|
||||||
|
|
Loading…
Reference in a new issue