232 lines
7.0 KiB
Python
232 lines
7.0 KiB
Python
#
|
|
#
|
|
#
|
|
|
|
from threading import (Thread, Event)
|
|
import time
|
|
from logging import getLogger as _Logger
|
|
|
|
from logitech.unifying_receiver import (api, base)
|
|
from logitech.unifying_receiver.listener import EventsListener
|
|
from logitech import devices
|
|
from logitech.devices import constants as C
|
|
|
|
|
|
_l = _Logger('watcher')
|
|
|
|
_UNIFYING_RECEIVER = 'Unifying Receiver'
|
|
_NO_RECEIVER = 'Receiver not found.'
|
|
_INITIALIZING = 'Initializing...'
|
|
_SCANNING = 'Scanning...'
|
|
_NO_DEVICES = 'No devices found.'
|
|
_OKAY = 'Status ok.'
|
|
|
|
|
|
class _DevStatus(api.AttachedDeviceInfo):
|
|
code = C.STATUS.UNKNOWN
|
|
text = _INITIALIZING
|
|
|
|
def __str__(self):
|
|
return 'DevStatus(%d,%s,%d)' % (self.number, self.name, self.code)
|
|
|
|
|
|
class Watcher(Thread):
|
|
"""Keeps a map of all attached devices and their statuses."""
|
|
def __init__(self, apptitle, notify=None):
|
|
super(Watcher, self).__init__(group=apptitle, name='Watcher')
|
|
self.daemon = True
|
|
self._active = False
|
|
|
|
self.listener = None
|
|
self.no_receiver = Event()
|
|
|
|
self.rstatus = _DevStatus(0, 0xFF, 'UR', _UNIFYING_RECEIVER, ())
|
|
self.rstatus.max_devices = api.C.MAX_ATTACHED_DEVICES
|
|
|
|
self.devices = {}
|
|
|
|
self.notify = notify
|
|
self.status_changed = Event()
|
|
|
|
def run(self):
|
|
self._active = True
|
|
|
|
while self._active:
|
|
if self.listener is None:
|
|
receiver = api.open()
|
|
if receiver:
|
|
self._device_status_changed(self.rstatus, C.STATUS.BOOTING, _INITIALIZING)
|
|
|
|
init = (base.request(receiver, 0xFF, b'\x81\x00') and
|
|
base.request(receiver, 0xFF, b'\x80\x00', b'\x00\x01') and
|
|
base.request(receiver, 0xFF, b'\x81\x02'))
|
|
if init:
|
|
_l.debug("receiver initialized ok")
|
|
else:
|
|
_l.debug("receiver initialization failed")
|
|
|
|
self._device_status_changed(self.rstatus, C.STATUS.BOOTING, _SCANNING)
|
|
|
|
self.listener = EventsListener(receiver, self._events_callback)
|
|
self.listener.start()
|
|
|
|
_l.debug("requesting devices status")
|
|
self.listener.request(base.request, 0xFF, b'\x80\x02', b'\x02')
|
|
|
|
# give it some time to get the devices
|
|
time.sleep(3)
|
|
elif not self.listener:
|
|
self.listener = None
|
|
self.devices.clear()
|
|
|
|
if self.listener:
|
|
if self.devices:
|
|
self._device_status_changed(self.rstatus, C.STATUS.CONNECTED, _OKAY)
|
|
else:
|
|
self._device_status_changed(self.rstatus, C.STATUS.CONNECTED, _NO_DEVICES)
|
|
|
|
self.no_receiver.wait()
|
|
self.no_receiver.clear()
|
|
else:
|
|
self._device_status_changed(self.rstatus, C.STATUS.UNAVAILABLE, _NO_RECEIVER)
|
|
time.sleep(3)
|
|
|
|
if self.listener:
|
|
self.listener.stop()
|
|
self.listener = None
|
|
|
|
def stop(self):
|
|
if self._active:
|
|
_l.debug("stopping %s", self)
|
|
self._active = False
|
|
self.no_receiver.set()
|
|
self.join()
|
|
|
|
def request_status(self, devstatus=None, **kwargs):
|
|
"""Trigger a status update on a device."""
|
|
if self.listener:
|
|
if devstatus is None or devstatus == self.rstatus:
|
|
for devstatus in self.devices.values():
|
|
self.request_status(devstatus)
|
|
else:
|
|
status = devices.request_status(devstatus, self.listener)
|
|
self._handle_status(devstatus, status)
|
|
|
|
def _handle_status(self, devstatus, status):
|
|
if status is not None:
|
|
if type(status) == int:
|
|
self._device_status_changed(devstatus, status)
|
|
else:
|
|
self._device_status_changed(devstatus, *status)
|
|
|
|
def _new_device(self, dev):
|
|
if not self._active:
|
|
return None
|
|
|
|
if type(dev) == int:
|
|
assert self.listener
|
|
dev = self.listener.request(api.get_device_info, dev)
|
|
|
|
if dev:
|
|
devstatus = _DevStatus(*dev)
|
|
self.devices[dev.number] = devstatus
|
|
self._device_status_changed(devstatus, C.STATUS.CONNECTED)
|
|
_l.debug("new devstatus %s", devstatus)
|
|
self._device_status_changed(self.rstatus, C.STATUS.CONNECTED, _OKAY)
|
|
return devstatus
|
|
|
|
def _device_status_changed(self, devstatus, status_code, status_data=None):
|
|
old_status_code = devstatus.code
|
|
status_text = devstatus.text
|
|
|
|
if status_data is None:
|
|
if status_code in C.STATUS_NAME:
|
|
status_text = C.STATUS_NAME[status_code]
|
|
elif isinstance(status_data, str):
|
|
status_text = status_data
|
|
elif isinstance(status_data, dict):
|
|
status_text = ''
|
|
for key, value in status_data.items():
|
|
if key == 'text':
|
|
status_text = value
|
|
else:
|
|
setattr(devstatus, key, value)
|
|
else:
|
|
_l.warn("don't know how to handle status %s", status_data)
|
|
return False
|
|
|
|
if status_code >= C.STATUS.CONNECTED and devstatus.type is None:
|
|
# ghost device that became active
|
|
if devstatus.code != C.STATUS.CONNECTED:
|
|
# initial update, while we're getting the devinfo
|
|
devstatus.code = C.STATUS.CONNECTED
|
|
devstatus.text = C.STATUS_NAME[C.STATUS.CONNECTED]
|
|
self.status_changed.set()
|
|
if self._new_device(devstatus.number) is None:
|
|
_l.warn("could not materialize device from %s", devstatus)
|
|
return False
|
|
|
|
if ((status_code == old_status_code and status_text == devstatus.text) or
|
|
(status_code == C.STATUS.CONNECTED and old_status_code > C.STATUS.CONNECTED)):
|
|
# this is just successful ping for a device with an already known status
|
|
return False
|
|
|
|
devstatus.code = status_code
|
|
devstatus.text = status_text
|
|
_l.debug("%s update %s => %s: %s", devstatus, old_status_code, status_code, status_text)
|
|
|
|
if self.notify and (status_code <= C.STATUS.CONNECTED or status_code != old_status_code):
|
|
self.notify(devstatus.code, devstatus.name, devstatus.text)
|
|
|
|
self.status_changed.set()
|
|
return True
|
|
|
|
def _events_callback(self, event):
|
|
if event.code == 0xFF and event.devnumber == 0xFF and event.data is None:
|
|
self.no_receiver.set()
|
|
return
|
|
|
|
if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
|
|
# 2 = 0010 ping
|
|
# 6 = 0110 off
|
|
# a = 1010 on
|
|
change = ord(event.data[2:3]) & 0xF0
|
|
status_code = C.STATUS.UNAVAILABLE if change == 0x60 else \
|
|
C.STATUS.CONNECTED if change == 0xA0 else \
|
|
C.STATUS.CONNECTED if change == 0x20 else \
|
|
None
|
|
if status_code is None:
|
|
_l.warn("don't know how to handle status %x: %s", change, event)
|
|
return
|
|
|
|
if event.devnumber in self.devices:
|
|
devstatus = self.devices[event.devnumber]
|
|
self._device_status_changed(devstatus, status_code)
|
|
return
|
|
|
|
if status_code == C.STATUS.CONNECTED:
|
|
self._new_device(event.devnumber)
|
|
return
|
|
|
|
# a device the UR knows about, but is not connected at this time
|
|
dev_id = self.listener.request(base.request, 0xFF, b'\x83\xB5', event.data[4:5])
|
|
name = str(dev_id[2:].rstrip(b'\x00')) if dev_id else '?'
|
|
name = devices.C.FULL_NAME[name]
|
|
ghost = _DevStatus(handle=self.listener.receiver, number=event.devnumber, type=None, name=name, features=[])
|
|
self.devices[event.devnumber] = ghost
|
|
self._device_status_changed(ghost, C.STATUS.UNAVAILABLE)
|
|
self._device_status_changed(self.rstatus, C.STATUS.CONNECTED, _OKAY)
|
|
return
|
|
|
|
if event.devnumber in self.devices:
|
|
devstatus = self.devices[event.devnumber]
|
|
if event.code == 0x11:
|
|
status = devices.process_event(devstatus, event.data, self.listener)
|
|
self._handle_status(devstatus, status)
|
|
return
|
|
if event.code == 0x10 and event.data[:1] == b'\x8F':
|
|
self._device_status_changed(devstatus, C.STATUS.UNAVAILABLE)
|
|
return
|
|
|
|
_l.warn("don't know how to handle event %s", event)
|