From 6928e440bc4c0001b5f9a3621beb4e2995c99708 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 24 Dec 2017 13:15:37 +0100 Subject: [PATCH] Support for Flic button events --- platypush/__init__.py | 2 +- .../backend/assistant/google/__init__.py | 1 + platypush/backend/button/__init__.py | 0 platypush/backend/button/flic/__init__.py | 102 +++ .../backend/button/flic/fliclib/aioflic.py | 554 ++++++++++++++++ .../backend/button/flic/fliclib/fliclib.py | 607 ++++++++++++++++++ platypush/context/__init__.py | 10 +- platypush/event/hook.py | 14 +- platypush/event/processor/__init__.py | 11 + platypush/message/event/__init__.py | 30 +- platypush/message/event/assistant/__init__.py | 17 +- platypush/message/event/button/__init__.py | 0 .../message/event/button/flic/__init__.py | 36 ++ platypush/plugins/music/mpd/__init__.py | 18 +- 14 files changed, 1378 insertions(+), 24 deletions(-) create mode 100644 platypush/backend/button/__init__.py create mode 100644 platypush/backend/button/flic/__init__.py create mode 100644 platypush/backend/button/flic/fliclib/aioflic.py create mode 100644 platypush/backend/button/flic/fliclib/fliclib.py create mode 100644 platypush/message/event/button/__init__.py create mode 100644 platypush/message/event/button/flic/__init__.py diff --git a/platypush/__init__.py b/platypush/__init__.py index 7b0c3b89f..4402b5288 100644 --- a/platypush/__init__.py +++ b/platypush/__init__.py @@ -106,7 +106,7 @@ class Daemon(object): self.bus = Bus(on_message=self.on_message()) # Initialize the backends and link them to the bus - self.backends = register_backends(bus=self.bus) + self.backends = register_backends(bus=self.bus, global_scope=True) # Start the backend threads for backend in self.backends.values(): diff --git a/platypush/backend/assistant/google/__init__.py b/platypush/backend/assistant/google/__init__.py index d8b43f50a..e432fef33 100644 --- a/platypush/backend/assistant/google/__init__.py +++ b/platypush/backend/assistant/google/__init__.py @@ -33,6 +33,7 @@ class AssistantGoogleBackend(Backend): with open(self.credentials_file, 'r') as f: self.credentials = google.oauth2.credentials.Credentials(token=None, **json.load(f)) + logging.info('Initialized Google Assistant backend') def _process_event(self, event): logging.info('Received assistant event: {}'.format(event)) diff --git a/platypush/backend/button/__init__.py b/platypush/backend/button/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/platypush/backend/button/flic/__init__.py b/platypush/backend/button/flic/__init__.py new file mode 100644 index 000000000..ee3d20880 --- /dev/null +++ b/platypush/backend/button/flic/__init__.py @@ -0,0 +1,102 @@ +import importlib +import logging + +from enum import Enum +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): + _long_press_timeout = 0.3 + _btn_timeout = 0.5 + ShortPressEvent = "ShortPressEvent" + LongPressEvent = "LongPressEvent" + + def __init__(self, server, long_press_timeout=_long_press_timeout, + btn_timeout=_btn_timeout, **kwargs): + 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 = [] + + logging.info('Initialized Flic buttons backend on {}'.format(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(): + logging.info('Flic event triggered from {}: {}'.format( + 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): + def _f(bd_addr, channel, click_type, was_queued, time_diff): + 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 send_message(self, msg): + pass + + def run(self): + super().run() + + self.client.handle_events() + + +# vim:sw=4:ts=4:et: + diff --git a/platypush/backend/button/flic/fliclib/aioflic.py b/platypush/backend/button/flic/fliclib/aioflic.py new file mode 100644 index 000000000..31c9d3df5 --- /dev/null +++ b/platypush/backend/button/flic/fliclib/aioflic.py @@ -0,0 +1,554 @@ +"""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 + +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 + + 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 == "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.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 == "EvtGetButtonUUIDResponse": + self.on_get_button_uuid(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 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 + + diff --git a/platypush/backend/button/flic/fliclib/fliclib.py b/platypush/backend/button/flic/fliclib/fliclib.py new file mode 100644 index 000000000..6e48811ff --- /dev/null +++ b/platypush/backend/button/flic/fliclib/fliclib.py @@ -0,0 +1,607 @@ +"""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", "> 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.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() diff --git a/platypush/context/__init__.py b/platypush/context/__init__.py index 756f393ae..b4b385e7a 100644 --- a/platypush/context/__init__.py +++ b/platypush/context/__init__.py @@ -1,5 +1,6 @@ -import importlib import functools +import importlib +import logging from ..config import Config @@ -9,7 +10,7 @@ backends = {} # Map: plugin_name -> plugin_instance plugins = {} -def register_backends(bus=None, **kwargs): +def register_backends(bus=None, global_scope=False, **kwargs): """ Initialize the backend objects based on the configuration and returns a name -> backend_instance map. Params: @@ -19,7 +20,10 @@ def register_backends(bus=None, **kwargs): kwargs -- Any additional key-value parameters required to initialize the backends """ - global backends + if global_scope: + global backends + else: + backends = {} for (name, cfg) in Config.get_backends().items(): module = importlib.import_module('platypush.backend.' + name) diff --git a/platypush/event/hook.py b/platypush/event/hook.py index 486e64cc9..63c688c6b 100644 --- a/platypush/event/hook.py +++ b/platypush/event/hook.py @@ -120,16 +120,24 @@ class EventHook(object): return cls(name=name, condition=condition, actions=actions) + def matches_event(self, event): + """ Returns an EventMatchResult object containing the information + about the match between the event and this hook """ + + return event.matches_condition(self.condition) + + def run(self, event): """ Checks the condition of the hook against a particular event and runs the hook actions if the condition is met """ - result = event.matches_condition(self.condition) - if result[0]: # is match + result = self.matches_event(event) + + if result.is_match: logging.info('Running hook {} triggered by an event'.format(self.name)) for action in self.actions: - action.execute(**result[1]) + action.execute(**result.parsed_args) # vim:sw=4:ts=4:et: diff --git a/platypush/event/processor/__init__.py b/platypush/event/processor/__init__.py index 72e3d9176..ea096245d 100644 --- a/platypush/event/processor/__init__.py +++ b/platypush/event/processor/__init__.py @@ -24,7 +24,18 @@ class EventProcessor(object): def process_event(self, event): """ Processes an event and runs any matched hooks """ + matched_hooks = [] + max_score = 0 + for hook in self.hooks: + match = hook.matches_event(event) + if match.is_match: + if match.score > max_score: + matched_hooks = [hook] + elif match.score == max_score: + matched_hooks.append(hook) + + for hook in matched_hooks: hook.run(event) diff --git a/platypush/message/event/__init__.py b/platypush/message/event/__init__.py index 5e7889c8d..bca088d2f 100644 --- a/platypush/message/event/__init__.py +++ b/platypush/message/event/__init__.py @@ -45,25 +45,24 @@ class Event(Message): def matches_condition(self, condition): """ - If the event matches an event condition, it will return True and a - dictionary containing any parsed arguments, otherwise False and {} + If the event matches an event condition, it will return an EventMatchResult Params: -- condition -- The platypush.event.hook.EventCondition object """ - parsed_args = {} - if not isinstance(self, condition.type): return [False, parsed_args] + result = EventMatchResult(is_match=False) + if not isinstance(self, condition.type): return result for (attr, value) in condition.args.items(): - # TODO Be more sophisticated, not only simple match options! if not hasattr(self.args, attr): - return [False, parsed_args] + return result if isinstance(self.args[attr], str) and not value in self.args[attr]: - return [False, parsed_args] + return result elif self.args[attr] != value: - return [False, parsed_args] + return result - return [True, parsed_args] + result.is_match = True + return result @staticmethod @@ -92,6 +91,19 @@ class Event(Message): }) +class EventMatchResult(object): + """ When comparing an event against an event condition, you want to + return this object. It contains the match status (True or False), + any parsed arguments, and a match_score that identifies how "strong" + the match is - in case of multiple event matches, the ones with the + highest score will win """ + + def __init__(self, is_match, score=1, parsed_args = {}): + self.is_match = is_match + self.score = score + self.parsed_args = parsed_args + + # XXX Should be a stop Request, not an Event class StopEvent(Event): """ StopEvent message. When received on a Bus, it will terminate the diff --git a/platypush/message/event/assistant/__init__.py b/platypush/message/event/assistant/__init__.py index 4d00ff46f..81fd04917 100644 --- a/platypush/message/event/assistant/__init__.py +++ b/platypush/message/event/assistant/__init__.py @@ -1,7 +1,7 @@ import re from platypush.context import get_backend -from platypush.message.event import Event +from platypush.message.event import Event, EventMatchResult class AssistantEvent(Event): """ Base class for assistant events """ @@ -27,11 +27,12 @@ class SpeechRecognizedEvent(AssistantEvent): self.recognized_phrase = phrase.strip().lower() def matches_condition(self, condition): - if not isinstance(self, condition.type): return [False, {}] + result = EventMatchResult(is_match=False, score=0) + + if not isinstance(self, condition.type): return result recognized_tokens = re.split('\s+', self.recognized_phrase.strip().lower()) condition_tokens = re.split('\s+', condition.args['phrase'].strip().lower()) - parsed_args = {} while recognized_tokens and condition_tokens: rec_token = recognized_tokens[0] @@ -40,21 +41,23 @@ class SpeechRecognizedEvent(AssistantEvent): if rec_token == cond_token: recognized_tokens.pop(0) condition_tokens.pop(0) + result.score += 1 elif re.search(cond_token, rec_token): condition_tokens.pop(0) else: m = re.match('^\$([\w\d_])', cond_token) if m: - parsed_args[cond_token[1:]] = rec_token + result.parsed_args[cond_token[1:]] = rec_token recognized_tokens.pop(0) condition_tokens.pop(0) + result.score += 1 else: recognized_tokens.pop(0) - is_match = len(condition_tokens) == 0 - if is_match and self._assistant: self._assistant.stop_conversation() + result.is_match = len(condition_tokens) == 0 + if result.is_match and self._assistant: self._assistant.stop_conversation() - return [is_match, parsed_args] + return result # vim:sw=4:ts=4:et: diff --git a/platypush/message/event/button/__init__.py b/platypush/message/event/button/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/platypush/message/event/button/flic/__init__.py b/platypush/message/event/button/flic/__init__.py new file mode 100644 index 000000000..79fb0c52f --- /dev/null +++ b/platypush/message/event/button/flic/__init__.py @@ -0,0 +1,36 @@ +from platypush.message.event import Event, EventMatchResult + + +class FlicButtonEvent(Event): + """ Flic button event """ + + def __init__(self, btn_addr, sequence, *args, **kwargs): + super().__init__(btn_addr=btn_addr, sequence=sequence, *args, **kwargs) + + + def matches_condition(self, condition): + result = EventMatchResult(is_match=False) + + if not isinstance(self, condition.type) \ + or self.args['btn_addr'] != condition.args['btn_addr']: + return result + + cond_sequence = list(condition.args['sequence']) + event_sequence = list(self.args['sequence']) + + while cond_sequence and event_sequence: + cond_press = cond_sequence[0] + event_press = event_sequence[0] + + if cond_press == event_press: + cond_sequence.pop(0) + result.score += 1 + + event_sequence.pop(0) + + result.is_match = len(cond_sequence) == 0 + return result + + +# vim:sw=4:ts=4:et: + diff --git a/platypush/plugins/music/mpd/__init__.py b/platypush/plugins/music/mpd/__init__.py index 7b6d8200f..87429c0c6 100644 --- a/platypush/plugins/music/mpd/__init__.py +++ b/platypush/plugins/music/mpd/__init__.py @@ -26,7 +26,9 @@ class MusicMpdPlugin(MusicPlugin): return self._exec('play') def pause(self): - return self._exec('pause') + status = self.status().output['state'] + if status == 'play': return self._exec('pause') + else: return self._exec('play') def stop(self): return self._exec('stop') @@ -40,6 +42,20 @@ class MusicMpdPlugin(MusicPlugin): def setvol(self, vol): return self._exec('setvol', vol) + def volup(self, delta=10): + volume = int(self.status().output['volume']) + new_volume = volume+delta + if new_volume <= 100: + self.setvol(str(new_volume)) + return self.status() + + def voldown(self, delta=10): + volume = int(self.status().output['volume']) + new_volume = volume-delta + if new_volume >= 0: + self.setvol(str(new_volume)) + return self.status() + def add(self, content): return self._exec('add', content)