From 72c55c03f2e408de48a58c296d45f3bfacbb8074 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Fri, 3 Mar 2023 02:10:11 +0100 Subject: [PATCH] [WIP] Refactoring/extending models and parsers for Bluetooth entities. --- .../plugins/bluetooth/_legacy/__init__.py | 11 + .../bluetooth/_legacy/_model/__init__.py | 8 + .../_legacy/_model/_classes/__init__.py | 9 + .../_legacy/_model/_classes/_base.py | 59 ++++ .../_model/_classes/_device/__init__.py | 8 + .../_legacy/_model/_classes/_device/_major.py | 20 ++ .../_legacy/_model/_classes/_device/_minor.py | 255 ++++++++++++++++++ .../_legacy/_model/_classes/_service.py | 19 ++ .../bluetooth/_legacy/_model/_protocol.py | 42 +++ .../_legacy/_model/_service/__init__.py | 4 + .../_legacy/_model/_service/_base.py | 127 +++++++++ .../_legacy/_model/_service/_directory.py | 163 +++++++++++ .../_legacy/_model/_service/_directory.pyi | 31 +++ .../_legacy/_model/_service/_types.py | 8 + 14 files changed, 764 insertions(+) create mode 100644 platypush/plugins/bluetooth/_legacy/__init__.py create mode 100644 platypush/plugins/bluetooth/_legacy/_model/__init__.py create mode 100644 platypush/plugins/bluetooth/_legacy/_model/_classes/__init__.py create mode 100644 platypush/plugins/bluetooth/_legacy/_model/_classes/_base.py create mode 100644 platypush/plugins/bluetooth/_legacy/_model/_classes/_device/__init__.py create mode 100644 platypush/plugins/bluetooth/_legacy/_model/_classes/_device/_major.py create mode 100644 platypush/plugins/bluetooth/_legacy/_model/_classes/_device/_minor.py create mode 100644 platypush/plugins/bluetooth/_legacy/_model/_classes/_service.py create mode 100644 platypush/plugins/bluetooth/_legacy/_model/_protocol.py create mode 100644 platypush/plugins/bluetooth/_legacy/_model/_service/__init__.py create mode 100644 platypush/plugins/bluetooth/_legacy/_model/_service/_base.py create mode 100644 platypush/plugins/bluetooth/_legacy/_model/_service/_directory.py create mode 100644 platypush/plugins/bluetooth/_legacy/_model/_service/_directory.pyi create mode 100644 platypush/plugins/bluetooth/_legacy/_model/_service/_types.py diff --git a/platypush/plugins/bluetooth/_legacy/__init__.py b/platypush/plugins/bluetooth/_legacy/__init__.py new file mode 100644 index 00000000..a100fedc --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/__init__.py @@ -0,0 +1,11 @@ +from ._model import BluetoothDevice +from ._scanner import DeviceScanner + + +__all__ = [ + "BluetoothDevice", + "DeviceScanner", +] + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/bluetooth/_legacy/_model/__init__.py b/platypush/plugins/bluetooth/_legacy/_model/__init__.py new file mode 100644 index 00000000..f226124b --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/__init__.py @@ -0,0 +1,8 @@ +from ._device import BluetoothDevice +from ._service import BluetoothService + + +__all__ = [ + "BluetoothDevice", + "BluetoothService", +] diff --git a/platypush/plugins/bluetooth/_legacy/_model/_classes/__init__.py b/platypush/plugins/bluetooth/_legacy/_model/_classes/__init__.py new file mode 100644 index 00000000..4694d6c4 --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/_classes/__init__.py @@ -0,0 +1,9 @@ +from ._device import MajorDeviceClass, MinorDeviceClass +from ._service import MajorServiceClass + + +__all__ = [ + "MajorDeviceClass", + "MinorDeviceClass", + "MajorServiceClass", +] diff --git a/platypush/plugins/bluetooth/_legacy/_model/_classes/_base.py b/platypush/plugins/bluetooth/_legacy/_model/_classes/_base.py new file mode 100644 index 00000000..3646e033 --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/_classes/_base.py @@ -0,0 +1,59 @@ +from enum import Enum +from dataclasses import dataclass +from typing import Optional + + +class BaseBluetoothClass(Enum): + """ + Base enum to model Bluetooth device/service classes. + """ + + def __repr__(self) -> str: + """ + :return: The enum class formatted as ````. + """ + return f'<{self.__class__.__name__}.{self.name}: {str(self)}>' + + def __str__(self) -> str: + """ + :return Only the readable string value of the class. + """ + return self.value.name + + +@dataclass +class ClassProperty: + """ + Models a Bluetooth class property. + + Given a Bluetooth class as a 24-bit unsigned integer, this class models the + filter that should be applied to the class to tell if the device exposes the + property. + """ + + name: str + """ The name of the property. """ + bitmask: int + """ Bitmask used to select the property bits from the class. """ + bit_shift: int = 0 + """ Number of bits to shift the class value after applying the bitmask. """ + match_value: int = 1 + """ + This should be the result of the bitwise filter for the property to match. + """ + parent: Optional[BaseBluetoothClass] = None + """ + Parent class property, if this is a minor property. If this is the case, + then the advertised class value should also match the parent property. + """ + + def matches(self, cls: int) -> bool: + """ + Match function. + + :param cls: The Bluetooth composite class value. + :return: True if the class matches the property. + """ + if self.parent and not self.parent.value.matches(cls): + return False + return ((cls & self.bitmask) >> self.bit_shift) == self.match_value diff --git a/platypush/plugins/bluetooth/_legacy/_model/_classes/_device/__init__.py b/platypush/plugins/bluetooth/_legacy/_model/_classes/_device/__init__.py new file mode 100644 index 00000000..3e568b84 --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/_classes/_device/__init__.py @@ -0,0 +1,8 @@ +from ._major import MajorDeviceClass +from ._minor import MinorDeviceClass + + +__all__ = [ + "MajorDeviceClass", + "MinorDeviceClass", +] diff --git a/platypush/plugins/bluetooth/_legacy/_model/_classes/_device/_major.py b/platypush/plugins/bluetooth/_legacy/_model/_classes/_device/_major.py new file mode 100644 index 00000000..f426ebff --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/_classes/_device/_major.py @@ -0,0 +1,20 @@ +from .._base import BaseBluetoothClass, ClassProperty + + +class MajorDeviceClass(BaseBluetoothClass): + """ + Models Bluetooth major device classes - see + https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Assigned%20Numbers.pdf, + Section 2.8.2 + """ + + UNKNOWN = ClassProperty('Unknown', 0x1F00, 8, 0b0000) + COMPUTER = ClassProperty('Computer', 0x1F00, 8, 0b0001) + PHONE = ClassProperty('Phone', 0x1F00, 8, 0b0010) + AP = ClassProperty('LAN / Network Access Point', 0x1F00, 8, 0b0011) + MULTIMEDIA = ClassProperty('Audio / Video', 0x1F00, 8, 0b0100) + PERIPHERAL = ClassProperty('Peripheral', 0x1F00, 8, 0b0101) + IMAGING = ClassProperty('Imaging', 0x1F00, 8, 0b0110) + WEARABLE = ClassProperty('Wearable', 0x1F00, 8, 0b0111) + TOY = ClassProperty('Toy', 0x1F00, 8, 0b1000) + HEALTH = ClassProperty('Health', 0x1F00, 8, 0b1001) diff --git a/platypush/plugins/bluetooth/_legacy/_model/_classes/_device/_minor.py b/platypush/plugins/bluetooth/_legacy/_model/_classes/_device/_minor.py new file mode 100644 index 00000000..b27535c3 --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/_classes/_device/_minor.py @@ -0,0 +1,255 @@ +from .._base import BaseBluetoothClass, ClassProperty +from ._major import MajorDeviceClass + + +class MinorDeviceClass(BaseBluetoothClass): + """ + Models Bluetooth minor device classes - see + https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Assigned%20Numbers.pdf, + Sections 2.8.2.1 - 2.8.2.9 + """ + + # Computer classes + COMPUTER_UNKNOWN = ClassProperty( + 'Unknown', 0xFC, 2, 0b000, MajorDeviceClass.COMPUTER + ) + COMPUTER_DESKTOP = ClassProperty( + 'Desktop Workstation', 0xFC, 2, 0b001, MajorDeviceClass.COMPUTER + ) + COMPUTER_SERVER = ClassProperty('Server', 0xFC, 2, 0b010, MajorDeviceClass.COMPUTER) + COMPUTER_LAPTOP = ClassProperty('Laptop', 0xFC, 2, 0b011, MajorDeviceClass.COMPUTER) + COMPUTER_HANDHELD_PDA = ClassProperty( + 'Handheld PDA', 0xFC, 2, 0b100, MajorDeviceClass.COMPUTER + ) + COMPUTER_PALM_PDA = ClassProperty( + 'Palm-sized PDA', 0xFC, 2, 0b101, MajorDeviceClass.COMPUTER + ) + COMPUTER_WEARABLE = ClassProperty( + 'Wearable Computer', 0xFC, 2, 0b110, MajorDeviceClass.COMPUTER + ) + + # Phone classes + PHONE_UNKNOWN = ClassProperty('Unknown', 0xFC, 2, 0b000, MajorDeviceClass.PHONE) + PHONE_CELLULAR = ClassProperty('Cellular', 0xFC, 2, 0b001, MajorDeviceClass.PHONE) + PHONE_CORDLESS = ClassProperty('Cordless', 0xFC, 2, 0b010, MajorDeviceClass.PHONE) + PHONE_SMARTPHONE = ClassProperty( + 'Smartphone', 0xFC, 2, 0b011, MajorDeviceClass.PHONE + ) + PHONE_WIRED_MODEM = ClassProperty( + 'Wired Modem', 0xFC, 2, 0b100, MajorDeviceClass.PHONE + ) + PHONE_ISDN_ACCESS = ClassProperty( + 'ISDN Access Point', 0xFC, 2, 0b101, MajorDeviceClass.PHONE + ) + + # LAN / Access Point classes + AP_USAGE_0 = ClassProperty('Fully Available', 0xE0, 5, 0b000, MajorDeviceClass.AP) + AP_USAGE_1_17 = ClassProperty( + '1 - 17% Utilized', 0xE0, 5, 0b001, MajorDeviceClass.AP + ) + AP_USAGE_17_33 = ClassProperty( + '17 - 33% Utilized', 0xE0, 5, 0b010, MajorDeviceClass.AP + ) + AP_USAGE_33_50 = ClassProperty( + '33 - 50% Utilized', 0xE0, 5, 0b011, MajorDeviceClass.AP + ) + AP_USAGE_50_67 = ClassProperty( + '50 - 67% Utilized', 0xE0, 5, 0b100, MajorDeviceClass.AP + ) + AP_USAGE_67_83 = ClassProperty( + '67 - 83% Utilized', 0xE0, 5, 0b101, MajorDeviceClass.AP + ) + AP_USAGE_83_99 = ClassProperty( + '83 - 99% Utilized', 0xE0, 5, 0b110, MajorDeviceClass.AP + ) + AP_USAGE_100 = ClassProperty( + 'No Service Available', 0xE0, 5, 0b111, MajorDeviceClass.AP + ) + + # Multimedia classes + MULTIMEDIA_HEADSET = ClassProperty( + 'Headset', 0xFC, 2, 0b000001, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_HANDS_FREE = ClassProperty( + 'Hands-free Device', 0xFC, 2, 0b000010, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_MICROPHONE = ClassProperty( + 'Microphone', 0xFC, 2, 0b000100, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_LOUDSPEAKER = ClassProperty( + 'Loudspeaker', 0xFC, 2, 0b000101, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_HEADPHONES = ClassProperty( + 'Headphones', 0xFC, 2, 0b000110, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_PORTABLE_AUDIO = ClassProperty( + 'Portable Audio', 0xFC, 2, 0b000111, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_CAR_AUDIO = ClassProperty( + 'Car Audio', 0xFC, 2, 0b001000, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_SET_TOP_BOX = ClassProperty( + 'Set-top Box', 0xFC, 2, 0b001001, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_HIFI_AUDIO = ClassProperty( + 'HiFi Audio Device', 0xFC, 2, 0b001010, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_VCR = ClassProperty( + 'VCR', 0xFC, 2, 0b001011, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_VIDEO_CAMERA = ClassProperty( + 'Video Camera', 0xFC, 2, 0b001100, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_CAMCODER = ClassProperty( + 'Camcoder', 0xFC, 2, 0b001101, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_VIDEO_MONITOR = ClassProperty( + 'Video Monitor', 0xFC, 2, 0b001110, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_VIDEO_DISPLAY_AND_LOUDSPEAKER = ClassProperty( + 'Video Display and Loudspeaker', 0xFC, 2, 0b001111, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_VIDEO_CONFERENCING = ClassProperty( + 'Video Conferencing', 0xFC, 2, 0b010000, MajorDeviceClass.MULTIMEDIA + ) + MULTIMEDIA_GAMING_TOY = ClassProperty( + 'Gaming / Toy', 0xFC, 2, 0b010010, MajorDeviceClass.MULTIMEDIA + ) + + # Peripheral classes + PERIPHERAL_UNKNOWN = ClassProperty( + 'Unknown', 0xFC, 2, 0b000000, MajorDeviceClass.PERIPHERAL + ) + PERIPHERAL_KEYBOARD = ClassProperty( + 'Keyboard', 0xC0, 6, 0b01, MajorDeviceClass.PERIPHERAL + ) + PERIPHERAL_POINTER = ClassProperty( + 'Pointing Device', 0xC0, 6, 0b10, MajorDeviceClass.PERIPHERAL + ) + PERIPHERAL_KEYBOARD_POINTER = ClassProperty( + 'Combo Keyboard/Pointing Device', 0xC0, 6, 0b11, MajorDeviceClass.PERIPHERAL + ) + PERIPHERAL_JOYSTICK = ClassProperty( + 'Joystick', 0x3C, 2, 0b0001, MajorDeviceClass.PERIPHERAL + ) + PERIPHERAL_GAMEPAD = ClassProperty( + 'Gamepad', 0x3C, 2, 0b0010, MajorDeviceClass.PERIPHERAL + ) + PERIPHERAL_REMOTE_CONTROL = ClassProperty( + 'Remote Control', 0x3C, 2, 0b0011, MajorDeviceClass.PERIPHERAL + ) + PERIPHERAL_SENSOR = ClassProperty( + 'Sensing Device', 0x3C, 2, 0b0100, MajorDeviceClass.PERIPHERAL + ) + PERIPHERAL_DIGIT_TABLET = ClassProperty( + 'Digitizer Tablet', 0x3C, 2, 0b0101, MajorDeviceClass.PERIPHERAL + ) + PERIPHERAL_CARD_READER = ClassProperty( + 'Card Reader', 0x3C, 2, 0b0110, MajorDeviceClass.PERIPHERAL + ) + PERIPHERAL_DIGITAL_PEN = ClassProperty( + 'Card Reader', 0x3C, 2, 0b0111, MajorDeviceClass.PERIPHERAL + ) + PERIPHERAL_SCANNER = ClassProperty( + 'Handheld Scanner', 0x3C, 2, 0b1000, MajorDeviceClass.PERIPHERAL + ) + PERIPHERAL_GESTURES = ClassProperty( + 'Handheld Gesture Input Device', 0x3C, 2, 0b1001, MajorDeviceClass.PERIPHERAL + ) + + # Imaging classes + IMAGING_UNKNOWN = ClassProperty( + 'Unknown', 0xFC, 2, 0b000000, MajorDeviceClass.IMAGING + ) + IMAGING_DISPLAY = ClassProperty( + 'Display', 0xF0, 4, 0b0001, MajorDeviceClass.IMAGING + ) + IMAGING_CAMERA = ClassProperty('Camera', 0xF0, 4, 0b0010, MajorDeviceClass.IMAGING) + IMAGING_SCANNER = ClassProperty( + 'Scanner', 0xF0, 4, 0b0100, MajorDeviceClass.IMAGING + ) + IMAGING_PRINTER = ClassProperty( + 'Printer', 0xF0, 4, 0b1000, MajorDeviceClass.IMAGING + ) + + # Wearable classes + WEARABLE_UNKNOWN = ClassProperty( + 'Unknown', 0xFC, 2, 0b000000, MajorDeviceClass.WEARABLE + ) + WEARABLE_WRISTWATCH = ClassProperty( + 'Wristwatch', 0xFC, 2, 0b000001, MajorDeviceClass.WEARABLE + ) + WEARABLE_PAGER = ClassProperty( + 'Pager', 0xFC, 2, 0b000010, MajorDeviceClass.WEARABLE + ) + WEARABLE_JACKET = ClassProperty( + 'Jacket', 0xFC, 2, 0b000011, MajorDeviceClass.WEARABLE + ) + WEARABLE_HELMET = ClassProperty( + 'Helmet', 0xFC, 2, 0b000100, MajorDeviceClass.WEARABLE + ) + WEARABLE_GLASSES = ClassProperty( + 'Glasses', 0xFC, 2, 0b000101, MajorDeviceClass.WEARABLE + ) + + # Toy classes + TOY_UNKNOWN = ClassProperty('Unknown', 0xFC, 2, 0b000000, MajorDeviceClass.TOY) + TOY_ROBOT = ClassProperty('Robot', 0xFC, 2, 0b000001, MajorDeviceClass.TOY) + TOY_VEHICLE = ClassProperty('Vehicle', 0xFC, 2, 0b000010, MajorDeviceClass.TOY) + TOY_DOLL = ClassProperty( + 'Doll / Action Figure', 0xFC, 2, 0b000011, MajorDeviceClass.TOY + ) + TOY_CONTROLLER = ClassProperty( + 'Controller', 0xFC, 2, 0b000100, MajorDeviceClass.TOY + ) + TOY_GAME = ClassProperty('Game', 0xFC, 2, 0b000101, MajorDeviceClass.TOY) + + # Health classes + HEALTH_UNKNOWN = ClassProperty( + 'Unknown', 0xFC, 2, 0b000000, MajorDeviceClass.HEALTH + ) + HEALTH_BLOOD_PRESSURE = ClassProperty( + 'Blood Pressure Monitor', 0xFC, 2, 0b000001, MajorDeviceClass.HEALTH + ) + HEALTH_THERMOMETER = ClassProperty( + 'Thermometer', 0xFC, 2, 0b000010, MajorDeviceClass.HEALTH + ) + HEALTH_SCALE = ClassProperty( + 'Weighing Scale', 0xFC, 2, 0b000011, MajorDeviceClass.HEALTH + ) + HEALTH_GLUCOSE = ClassProperty( + 'Glucose Meter', 0xFC, 2, 0b000100, MajorDeviceClass.HEALTH + ) + HEALTH_OXIMETER = ClassProperty( + 'Pulse Oximeter', 0xFC, 2, 0b000101, MajorDeviceClass.HEALTH + ) + HEALTH_PULSE = ClassProperty( + 'Heart Rate/Pulse Monitor', 0xFC, 2, 0b000110, MajorDeviceClass.HEALTH + ) + HEALTH_DISPLAY = ClassProperty( + 'Health Data Display', 0xFC, 2, 0b000111, MajorDeviceClass.HEALTH + ) + HEALTH_STEPS = ClassProperty( + 'Step Counter', 0xFC, 2, 0b001000, MajorDeviceClass.HEALTH + ) + HEALTH_COMPOSITION = ClassProperty( + 'Body Composition Analyzer', 0xFC, 2, 0b001001, MajorDeviceClass.HEALTH + ) + HEALTH_PEAK_FLOW = ClassProperty( + 'Peak Flow Monitor', 0xFC, 2, 0b001010, MajorDeviceClass.HEALTH + ) + HEALTH_MEDICATION = ClassProperty( + 'Medication Monitor', 0xFC, 2, 0b001011, MajorDeviceClass.HEALTH + ) + HEALTH_KNEE_PROSTHESIS = ClassProperty( + 'Knee Prosthesis', 0xFC, 2, 0b001100, MajorDeviceClass.HEALTH + ) + HEALTH_ANKLE_PROSTHESIS = ClassProperty( + 'Ankle Prosthesis', 0xFC, 2, 0b001101, MajorDeviceClass.HEALTH + ) + HEALTH_GENERIC = ClassProperty( + 'Generic Health Manager', 0xFC, 2, 0b001110, MajorDeviceClass.HEALTH + ) + HEALTH_MOBILITY = ClassProperty( + 'Personal Mobility Device', 0xFC, 2, 0b001111, MajorDeviceClass.HEALTH + ) diff --git a/platypush/plugins/bluetooth/_legacy/_model/_classes/_service.py b/platypush/plugins/bluetooth/_legacy/_model/_classes/_service.py new file mode 100644 index 00000000..d89a7d91 --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/_classes/_service.py @@ -0,0 +1,19 @@ +from ._base import BaseBluetoothClass, ClassProperty + + +class MajorServiceClass(BaseBluetoothClass): + """ + Models Bluetooth major service classes - see + https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Assigned%20Numbers.pdf, + Section 2.8.1 + """ + + LE_AUDIO = ClassProperty('Low-energy Audio', 1 << 14, 14) + POSITIONING = ClassProperty('Positioning', 1 << 16, 16) + NETWORKING = ClassProperty('Networking', 1 << 17, 17) + RENDERING = ClassProperty('Rendering', 1 << 18, 18) + CAPTURING = ClassProperty('Capturing', 1 << 19, 19) + OBJECT_TRANSFER = ClassProperty('Object Transfer', 1 << 20, 20) + AUDIO = ClassProperty('Audio', 1 << 21, 21) + TELEPHONY = ClassProperty('Telephony', 1 << 22, 22) + INFORMATION = ClassProperty('Information', 1 << 23, 23) diff --git a/platypush/plugins/bluetooth/_legacy/_model/_protocol.py b/platypush/plugins/bluetooth/_legacy/_model/_protocol.py new file mode 100644 index 00000000..db69bf46 --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/_protocol.py @@ -0,0 +1,42 @@ +from enum import Enum + + +class Protocol(Enum): + """ + Models a Bluetooth protocol. + """ + + RFCOMM = 'RFCOMM' + L2CAP = 'L2CAP' + TCP = 'TCP' + UDP = 'UDP' + SDP = 'SDP' + BNEP = 'BNEP' + TCS_BIN = 'TCS-BIN' + TCS_AT = 'TCS-AT' + OBEX = 'OBEX' + IP = 'IP' + FTP = 'FTP' + HTTP = 'HTTP' + WSP = 'WSP' + UPNP = 'UPNP' + HIDP = 'HIDP' + AVCTP = 'AVCTP' + AVDTP = 'AVDTP' + CMTP = 'CMTP' + UDI_C_PLANE = 'UDI_C-Plane' + HardCopyControlChannel = 'HardCopyControlChannel' + HardCopyDataChannel = 'HardCopyDataChannel' + HardCopyNotification = 'HardCopyNotification' + + def __str__(self) -> str: + """ + Only returns the value of the enum. + """ + return self.value + + def __repr__(self) -> str: + """ + Only returns the value of the enum. + """ + return str(self) diff --git a/platypush/plugins/bluetooth/_legacy/_model/_service/__init__.py b/platypush/plugins/bluetooth/_legacy/_model/_service/__init__.py new file mode 100644 index 00000000..66d5d6e2 --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/_service/__init__.py @@ -0,0 +1,4 @@ +from ._base import BluetoothService + + +__all__ = ['BluetoothService'] diff --git a/platypush/plugins/bluetooth/_legacy/_model/_service/_base.py b/platypush/plugins/bluetooth/_legacy/_model/_service/_base.py new file mode 100644 index 00000000..fa779635 --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/_service/_base.py @@ -0,0 +1,127 @@ +from dataclasses import dataclass +from typing import Any, Dict, Iterable, Optional, Set, Tuple +from uuid import UUID + +from .._protocol import Protocol +from ._directory import ServiceClass +from ._types import RawServiceClass + +VersionedServices = Dict[ServiceClass, Optional[int]] +""" Service -> Version mapping. """ + + +@dataclass +class BluetoothService: + """ + Models a discovered Bluetooth service. + """ + + address: str + """ The address of the service that exposes the service. """ + port: int + """ The Bluetooth port associated to the service. """ + protocol: Protocol + """ The service protocol. """ + name: Optional[str] + """ The name of the service. """ + description: Optional[str] + """ The description of the service. """ + service_id: Optional[str] + """ The ID of the service. """ + service_classes: VersionedServices + """ + The compatible classes exposed by the service - see + https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Assigned%20Numbers.pdf, + Section 5. + """ + unknown_service_classes: Iterable[RawServiceClass] + """ Service classes that are not supported. """ + + @classmethod + def build(cls, service: Dict[str, Any]) -> 'BluetoothService': + """ + Builds a :class:`BluetoothService` from a service dictionary returned by + pybluez. + """ + return cls( + address=service['host'], + port=service['port'], + protocol=Protocol(service['protocol']), + name=service['name'], + description=service['description'], + service_id=service['service-id'], + service_classes=cls._parse_services( + service['service-classes'], service['profiles'] + ), + unknown_service_classes=cls._parse_unknown_services( + service['service-classes'], + ), + ) + + @classmethod + def _parse_services( + cls, service_classes: Iterable[str], profiles: Iterable[Tuple[str, int]] + ) -> VersionedServices: + """ + Parses the services. + + :param service_classes: The service classes returned by pybluez. + :param profiles: The profiles returned by pybluez as a list of + ``[(service, version)]`` tuples. + :return: A list of parsed service classes. + """ + # Parse the service classes + parsed_services: Dict[RawServiceClass, ServiceClass] = {} + for srv in service_classes: + srv_class = cls._parse_service_class(srv) + parsed_services[srv_class.value] = srv_class + + # Parse the service classes versions + versioned_classes: VersionedServices = {} + for srv, version in profiles: + value = cls._parse_service_class(srv).value + parsed_srv = parsed_services.get(value) + if parsed_srv: + versioned_classes[parsed_srv] = version + + return { + srv: versioned_classes.get(srv) + for srv in parsed_services.values() + if srv != ServiceClass.UNKNOWN + } + + @classmethod + def _parse_unknown_services( + cls, service_classes: Iterable[str] + ) -> Set[RawServiceClass]: + return { + cls._uuid(srv) + for srv in service_classes + if cls._parse_service_class(srv) == ServiceClass.UNKNOWN + } + + @classmethod + def _parse_service_class(cls, srv: str) -> ServiceClass: + """ + :param srv: The service class returned by pybluez as a string (either + hex-encoded or UUID). + :return: The parsed :class:`ServiceClass` object or ``ServiceClass.UNKNOWN``. + """ + srv_class: ServiceClass = ServiceClass.UNKNOWN + try: + srv_class = ServiceClass.get(cls._uuid(srv)) + except (TypeError, ValueError): + pass + + return srv_class + + @staticmethod + def _uuid(s: str) -> RawServiceClass: + """ + :param s: The service class returned by pybluez as a string. + :return: The UUID of the service class as a 16-bit or 128-bit identifier. + """ + try: + return UUID(s) + except ValueError: + return int(s, 16) diff --git a/platypush/plugins/bluetooth/_legacy/_model/_service/_directory.py b/platypush/plugins/bluetooth/_legacy/_model/_service/_directory.py new file mode 100644 index 00000000..cb32c988 --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/_service/_directory.py @@ -0,0 +1,163 @@ +import re +from enum import Enum +from typing import Dict + +import bluetooth_numbers + +from ._types import RawServiceClass + + +def _service_name_to_enum_name(service_name: str) -> str: + """ + Convert a service name to an enum-key compatible string. + """ + ret = service_name.title() + ret = re.sub(r"\(.+?\)", "", ret) + ret = re.sub(r"\s+", "_", ret) + ret = re.sub(r"[^a-zA-Z0-9_]", "", ret) + ret = re.sub(r"_+", "_", ret) + return ret.upper() + + +_service_classes: Dict[RawServiceClass, str] = { + 0x0: "Unknown", + 0x1000: "Service Discovery Server Service Class ID", + 0x1001: "Browse Group Descriptor Service Class ID", + 0x1101: "Serial Port", + 0x1102: "LAN Access Using PPP", + 0x1103: "Dialup Networking", + 0x1104: "IR MC Sync", + 0x1105: "OBEX Object Push", + 0x1106: "OBEX File Transfer", + 0x1107: "IR MC Sync Command", + 0x1108: "Headset", + 0x1109: "Cordless Telephony", + 0x110A: "Audio Source", + 0x110B: "Audio Sink", + 0x110C: "A/V Remote Control Target", + 0x110D: "Advanced Audio Distribution", + 0x110E: "A/V Remote Control", + 0x110F: "A/V Remote Control Controller", + 0x1110: "Intercom", + 0x1111: "Fax", + 0x1112: "Headset Audio Gateway", + 0x1113: "WAP", + 0x1114: "WAP Client", + 0x1115: "PANU", + 0x1116: "NAP", + 0x1117: "GN", + 0x1118: "Direct Printing", + 0x1119: "Reference Printing", + 0x111A: "Basic Imaging Profile", + 0x111B: "Imaging Responder", + 0x111C: "Imaging Automatic Archive", + 0x111D: "Imaging Referenced Objects", + 0x111E: "Handsfree", + 0x111F: "Handsfree Audio Gateway", + 0x1120: "Direct Printing Reference Objects Service", + 0x1121: "Reflected UI", + 0x1122: "Basic Printing", + 0x1123: "Printing Status", + 0x1124: "Human Interface Device Service", + 0x1125: "Hard Copy Cable Replacement", + 0x1126: "HCR Print", + 0x1127: "HCR Scan", + 0x1128: "Common ISDN Access", + 0x112D: "SIM Access", + 0x112E: "Phone Book Access PCE", + 0x112F: "Phone Book Access PSE", + 0x1130: "Phone Book Access", + 0x1131: "Headset NS", + 0x1132: "Message Access Server", + 0x1133: "Message Notification Server", + 0x1134: "Message Notification Profile", + 0x1135: "GNSS", + 0x1136: "GNSS Server", + 0x1137: "3D Display", + 0x1138: "3D Glasses", + 0x1139: "3D Synchronization", + 0x113A: "MPS Profile", + 0x113B: "MPS SC", + 0x113C: "CTN Access Service", + 0x113D: "CTN Notification Service", + 0x113E: "CTN Profile", + 0x1200: "PnP Information", + 0x1201: "Generic Networking", + 0x1202: "Generic File Transfer", + 0x1203: "Generic Audio", + 0x1204: "Generic Telephony", + 0x1205: "UPNP Service", + 0x1206: "UPNP IP Service", + 0x1300: "ESDP UPNP IP PAN", + 0x1301: "ESDP UPNP IP LAP", + 0x1302: "ESDP UPNP L2CAP", + 0x1303: "Video Source", + 0x1304: "Video Sink", + 0x1305: "Video Distribution", + 0x1400: "HDP", + 0x1401: "HDP Source", + 0x1402: "HDP Sink", +} +""" +Directory of known Bluetooth service UUIDs. + +See +https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Assigned%20Numbers.pdf, +Section 3.3. +""" + +# Update the base services with the GATT service UUIDs defined in ``bluetooth_numbers``. See +# https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Assigned%20Numbers.pdf, +# Section 3.4 +_service_classes.update(bluetooth_numbers.service) + +_service_classes_by_name: Dict[str, RawServiceClass] = { + name: cls for cls, name in _service_classes.items() +} + + +class _ServiceClassMeta: + """ + Metaclass for :class:`ServiceClass`. + """ + + value: RawServiceClass + """ The raw service class value. """ + + @classmethod + def get(cls, value: RawServiceClass) -> "ServiceClass": + """ + :param value: The raw service class UUID. + :return: The parsed :class:`ServiceClass` instance, or + ``ServiceClass.UNKNOWN``. + """ + try: + return ServiceClass(value) + except ValueError: + return ServiceClass.UNKNOWN # type: ignore + + @classmethod + def by_name(cls, name: str) -> "ServiceClass": + """ + :param name: The name of the service class. + :return: The :class:`ServiceClass` instance, or + ``ServiceClass.UNKNOWN``. + """ + return ( + ServiceClass(_service_classes_by_name.get(name)) + or ServiceClass.UNKNOWN # type: ignore + ) + + def __str__(self) -> str: + return _service_classes.get(self.value, ServiceClass(0).value) + + def __repr__(self) -> str: + return f"<{self.value}: {str(self)}>" + + +ServiceClass = Enum( # type: ignore + "ServiceClass", + {_service_name_to_enum_name(name): cls for cls, name in _service_classes.items()}, + type=_ServiceClassMeta, +) +""" Enumeration of known Bluetooth services. """ diff --git a/platypush/plugins/bluetooth/_legacy/_model/_service/_directory.pyi b/platypush/plugins/bluetooth/_legacy/_model/_service/_directory.pyi new file mode 100644 index 00000000..37aea1bb --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/_service/_directory.pyi @@ -0,0 +1,31 @@ +# mypy stub for ServiceClass + +from enum import Enum + +from ._types import RawServiceClass + +class ServiceClass(Enum): + """ + Enumeration of supported Bluetooth service classes. + """ + + value: RawServiceClass + """ The raw service class value. """ + + UNKNOWN = ... + """ A class for unknown services. """ + + @classmethod + def get(cls, value: RawServiceClass) -> "ServiceClass": + """ + :param value: The raw service class UUID. + :return: The parsed :class:`ServiceClass` instance, or + ``ServiceClass.UNKNOWN``. + """ + @classmethod + def by_name(cls, name: str) -> "ServiceClass": + """ + :param name: The name of the service class. + :return: The :class:`ServiceClass` instance, or + ``ServiceClass.UNKNOWN``. + """ diff --git a/platypush/plugins/bluetooth/_legacy/_model/_service/_types.py b/platypush/plugins/bluetooth/_legacy/_model/_service/_types.py new file mode 100644 index 00000000..455e8560 --- /dev/null +++ b/platypush/plugins/bluetooth/_legacy/_model/_service/_types.py @@ -0,0 +1,8 @@ +from typing import Union +from uuid import UUID + +RawServiceClass = Union[UUID, int] +""" +Raw type for service classes received by pybluez. +Can be either a 16-bit integer or a UUID. +"""