379 lines
10 KiB
Python
379 lines
10 KiB
Python
#
|
|
#
|
|
#
|
|
|
|
from logging import getLogger as _Logger
|
|
from struct import pack as _pack
|
|
|
|
from logitech.unifying_receiver import base as _base
|
|
from logitech.unifying_receiver import api as _api
|
|
from logitech.unifying_receiver.listener import EventsListener as _EventsListener
|
|
from logitech.unifying_receiver.common import FallbackDict as _FallbackDict
|
|
from logitech import devices as _devices
|
|
from logitech.devices.constants import (STATUS, STATUS_NAME, PROPS, NAMES)
|
|
|
|
#
|
|
#
|
|
#
|
|
|
|
class _FeaturesArray(object):
|
|
__slots__ = ('device', 'features', 'supported')
|
|
|
|
def __init__(self, device):
|
|
self.device = device
|
|
self.features = None
|
|
self.supported = True
|
|
|
|
def _check(self):
|
|
if self.supported:
|
|
if self.features is not None:
|
|
return True
|
|
|
|
if self.device.status >= STATUS.CONNECTED:
|
|
handle = self.device.handle
|
|
try:
|
|
index = _api.get_feature_index(handle, self.device.number, _api.FEATURE.FEATURE_SET)
|
|
except _api._FeatureNotSupported:
|
|
self.supported = False
|
|
else:
|
|
count = _base.request(handle, self.device.number, _pack('!BB', index, 0x00))
|
|
if count is None:
|
|
self.supported = False
|
|
else:
|
|
count = ord(count[:1])
|
|
self.features = [None] * (1 + count)
|
|
self.features[0] = _api.FEATURE.ROOT
|
|
self.features[index] = _api.FEATURE.FEATURE_SET
|
|
return True
|
|
|
|
return False
|
|
|
|
__bool__ = __nonzero__ = _check
|
|
|
|
def __getitem__(self, index):
|
|
if not self._check():
|
|
return None
|
|
|
|
if index < 0 or index >= len(self.features):
|
|
raise IndexError
|
|
if self.features[index] is None:
|
|
fs_index = self.features.index(_api.FEATURE.FEATURE_SET)
|
|
feature = _base.request(self.device.handle, self.device.number, _pack('!BB', fs_index, 0x10), _pack('!B', index))
|
|
if feature is not None:
|
|
self.features[index] = feature[:2]
|
|
|
|
return self.features[index]
|
|
|
|
def __contains__(self, value):
|
|
if self._check():
|
|
if value in self.features:
|
|
return True
|
|
|
|
for index in range(0, len(self.features)):
|
|
f = self.features[index] or self.__getitem__(index)
|
|
assert f is not None
|
|
if f == value:
|
|
return True
|
|
if f > value:
|
|
break
|
|
|
|
return False
|
|
|
|
def index(self, value):
|
|
if self._check():
|
|
if self.features is not None and value in self.features:
|
|
return self.features.index(value)
|
|
raise ValueError("%s not in list" % repr(value))
|
|
|
|
def __iter__(self):
|
|
if self._check():
|
|
yield _api.FEATURE.ROOT
|
|
index = 1
|
|
last_index = len(self.features)
|
|
while index < last_index:
|
|
yield self.__getitem__(index)
|
|
index += 1
|
|
|
|
def __len__(self):
|
|
return len(self.features) if self._check() else 0
|
|
|
|
#
|
|
#
|
|
#
|
|
|
|
class DeviceInfo(_api.PairedDevice):
|
|
"""A device attached to the receiver.
|
|
"""
|
|
def __init__(self, listener, number, pair_code, status=STATUS.UNKNOWN):
|
|
super(DeviceInfo, self).__init__(listener.handle, number)
|
|
|
|
self.LOG = _Logger("Device[%d]" % number)
|
|
self._listener = listener
|
|
self._pair_code = pair_code
|
|
self._serial = None
|
|
self._codename = None
|
|
|
|
self._status = status
|
|
self.props = {}
|
|
|
|
self.features = _FeaturesArray(self)
|
|
|
|
# read them now, otherwise it it temporarily hang the UI
|
|
if status >= STATUS.CONNECTED:
|
|
n, k, s, f = self.name, self.kind, self.serial, self.firmware
|
|
|
|
@property
|
|
def receiver(self):
|
|
return self._listener.receiver
|
|
|
|
@property
|
|
def status(self):
|
|
return self._status
|
|
|
|
@status.setter
|
|
def status(self, new_status):
|
|
if new_status != self._status and not (new_status == STATUS.CONNECTED and self._status > new_status):
|
|
self.LOG.debug("status %d => %d", self._status, new_status)
|
|
urgent = new_status < STATUS.CONNECTED or self._status < STATUS.CONNECTED
|
|
self._status = new_status
|
|
self._listener.status_changed_callback(self, urgent)
|
|
|
|
if new_status < STATUS.CONNECTED:
|
|
self.props.clear()
|
|
|
|
@property
|
|
def status_text(self):
|
|
if self._status < STATUS.CONNECTED:
|
|
return STATUS_NAME[self._status]
|
|
|
|
t = []
|
|
if self.props.get(PROPS.BATTERY_LEVEL):
|
|
t.append('Battery: %d%%' % self.props[PROPS.BATTERY_LEVEL])
|
|
if self.props.get(PROPS.BATTERY_STATUS):
|
|
t.append(self.props[PROPS.BATTERY_STATUS])
|
|
if self.props.get(PROPS.LIGHT_LEVEL):
|
|
t.append('Light: %d lux' % self.props[PROPS.LIGHT_LEVEL])
|
|
return ', '.join(t) if t else STATUS_NAME[STATUS.CONNECTED]
|
|
|
|
@property
|
|
def name(self):
|
|
if self._name is None:
|
|
if self._status >= STATUS.CONNECTED:
|
|
self._name = _api.get_device_name(self.handle, self.number, self.features)
|
|
return self._name or self.codename
|
|
|
|
@property
|
|
def kind(self):
|
|
if self._kind is None:
|
|
if self._status < STATUS.CONNECTED:
|
|
codename = self.codename
|
|
if codename in NAMES:
|
|
self._kind = NAMES[codename][-1]
|
|
else:
|
|
self._kind = _api.get_device_kind(self.handle, self.number, self.features)
|
|
return self._kind or '?'
|
|
|
|
@property
|
|
def serial(self):
|
|
if self._serial is None:
|
|
# dodgy
|
|
b = bytearray(self._pair_code)
|
|
b[0] -= 0x10
|
|
serial = _base.request(self.handle, 0xFF, b'\x83\xB5', bytes(b))
|
|
if serial:
|
|
self._serial = _base._hex(serial[1:5])
|
|
return self._serial or '?'
|
|
|
|
@property
|
|
def codename(self):
|
|
if self._codename is None:
|
|
codename = _base.request(self.handle, 0xFF, b'\x83\xB5', self._pair_code)
|
|
if codename:
|
|
self._codename = codename[2:].rstrip(b'\x00').decode('ascii')
|
|
return self._codename or '?'
|
|
|
|
@property
|
|
def firmware(self):
|
|
if self._firmware is None:
|
|
if self._status >= STATUS.CONNECTED:
|
|
self._firmware = _api.get_device_firmware(self.handle, self.number, self.features)
|
|
return self._firmware or ()
|
|
|
|
def process_event(self, code, data):
|
|
if code == 0x10 and data[:1] == b'\x8F':
|
|
self.status = STATUS.UNAVAILABLE
|
|
return True
|
|
|
|
if code == 0x11:
|
|
status = _devices.process_event(self, data)
|
|
if status:
|
|
if type(status) == int:
|
|
self.status = status
|
|
return True
|
|
|
|
if type(status) == tuple:
|
|
p = dict(self.props)
|
|
self.props.update(status[1])
|
|
if self.status == status[0]:
|
|
if p != self.props:
|
|
self._listener.status_changed_callback(self)
|
|
else:
|
|
self.status = status[0]
|
|
return True
|
|
|
|
self.LOG.warn("don't know how to handle processed event status %s", status)
|
|
|
|
return False
|
|
|
|
def __str__(self):
|
|
return 'DeviceInfo(%d,%s,%d)' % (self.number, self._name or '?', self._status)
|
|
|
|
#
|
|
#
|
|
#
|
|
|
|
_RECEIVER_STATUS_NAME = _FallbackDict(
|
|
lambda x:
|
|
'1 device found' if x == STATUS.CONNECTED + 1 else
|
|
'%d devices found' if x > STATUS.CONNECTED else
|
|
'?',
|
|
{
|
|
STATUS.UNKNOWN: 'Initializing...',
|
|
STATUS.UNAVAILABLE: 'Receiver not found.',
|
|
STATUS.BOOTING: 'Scanning...',
|
|
STATUS.CONNECTED: 'No devices found.',
|
|
}
|
|
)
|
|
|
|
class ReceiverListener(_EventsListener):
|
|
"""Keeps the status of a Unifying Receiver.
|
|
"""
|
|
|
|
def __init__(self, receiver, status_changed_callback):
|
|
super(ReceiverListener, self).__init__(receiver.handle, self._events_handler)
|
|
self.receiver = receiver
|
|
|
|
self.LOG = _Logger("ReceiverListener(%s)" % receiver.path)
|
|
|
|
self.events_filter = None
|
|
self.events_handler = None
|
|
|
|
self.status_changed_callback = status_changed_callback or (lambda reason, urgent=False: None)
|
|
|
|
receiver.kind = receiver.name
|
|
receiver.devices = {}
|
|
receiver.status = STATUS.BOOTING
|
|
receiver.status_text = _RECEIVER_STATUS_NAME[STATUS.BOOTING]
|
|
|
|
if _base.request(receiver.handle, 0xFF, b'\x80\x00', b'\x00\x01'):
|
|
self.LOG.info("initialized")
|
|
else:
|
|
self.LOG.warn("initialization failed")
|
|
|
|
if _base.request(receiver.handle, 0xFF, b'\x80\x02', b'\x02'):
|
|
self.LOG.info("triggered device events")
|
|
else:
|
|
self.LOG.warn("failed to trigger device events")
|
|
|
|
def change_status(self, new_status):
|
|
if new_status != self.receiver.status:
|
|
self.LOG.debug("status %d => %d", self.receiver.status, new_status)
|
|
self.receiver.status = new_status
|
|
self.receiver.status_text = _RECEIVER_STATUS_NAME[new_status]
|
|
self.status_changed_callback(self.receiver, True)
|
|
|
|
def _device_status_from(self, event):
|
|
state_code = ord(event.data[2:3]) & 0xF0
|
|
state = STATUS.UNAVAILABLE if state_code == 0x60 else \
|
|
STATUS.CONNECTED if state_code == 0xA0 else \
|
|
STATUS.CONNECTED if state_code == 0x20 else \
|
|
STATUS.UNKNOWN
|
|
if state == STATUS.UNKNOWN:
|
|
self.LOG.warn("don't know how to handle state code 0x%02X: %s", state_code, event)
|
|
return state
|
|
|
|
def _events_handler(self, event):
|
|
if self.events_filter and self.events_filter(event):
|
|
return
|
|
|
|
if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
|
|
|
|
if event.devnumber in self.receiver.devices:
|
|
status = self._device_status_from(event)
|
|
if status > STATUS.UNKNOWN:
|
|
self.receiver.devices[event.devnumber].status = status
|
|
else:
|
|
dev = self.make_device(event)
|
|
if dev is None:
|
|
self.LOG.warn("failed to make new device from %s", event)
|
|
else:
|
|
self.receiver.devices[event.devnumber] = dev
|
|
self.change_status(STATUS.CONNECTED + len(self.receiver.devices))
|
|
return
|
|
|
|
if event.devnumber == 0xFF:
|
|
if event.code == 0xFF and event.data is None:
|
|
# receiver disconnected
|
|
self.LOG.warn("disconnected")
|
|
self.receiver.devices = {}
|
|
self.change_status(STATUS.UNAVAILABLE)
|
|
return
|
|
elif event.devnumber in self.receiver.devices:
|
|
dev = self.receiver.devices[event.devnumber]
|
|
if dev.process_event(event.code, event.data):
|
|
return
|
|
|
|
if self.events_handler and self.events_handler(event):
|
|
return
|
|
|
|
self.LOG.warn("don't know how to handle event %s", event)
|
|
|
|
def make_device(self, event):
|
|
if event.devnumber < 1 or event.devnumber > self.receiver.max_devices:
|
|
self.LOG.warn("got event for invalid device number %d: %s", event.devnumber, event)
|
|
return None
|
|
|
|
status = self._device_status_from(event)
|
|
|
|
dev = DeviceInfo(self, event.devnumber, event.data[4:5], status)
|
|
self.LOG.info("new device %s", dev)
|
|
self.status_changed_callback(dev, True)
|
|
return dev
|
|
|
|
def unpair_device(self, device):
|
|
try:
|
|
del self.receiver[device.number]
|
|
except IndexError:
|
|
self.LOG.error("failed to unpair device %s", device)
|
|
return False
|
|
|
|
del self.receiver.devices[device.number]
|
|
self.LOG.info("unpaired device %s", device)
|
|
self.change_status(STATUS.CONNECTED + len(self.receiver.devices))
|
|
device.status = STATUS.UNPAIRED
|
|
return True
|
|
|
|
def __str__(self):
|
|
return '<ReceiverListener(%s,%d)>' % (self.receiver.path, self.receiver.status)
|
|
|
|
@classmethod
|
|
def open(self, status_changed_callback=None):
|
|
receiver = _api.Receiver.open()
|
|
if receiver:
|
|
rl = ReceiverListener(receiver, status_changed_callback)
|
|
rl.start()
|
|
return rl
|
|
|
|
#
|
|
#
|
|
#
|
|
|
|
class _DUMMY_RECEIVER(object):
|
|
name = _api.Receiver.name
|
|
max_devices = _api.Receiver.max_devices
|
|
status = STATUS.UNAVAILABLE
|
|
status_text = _RECEIVER_STATUS_NAME[STATUS.UNAVAILABLE]
|
|
devices = {}
|
|
__bool__ = __nonzero__ = lambda self: False
|
|
DUMMY = _DUMMY_RECEIVER()
|