Solaar/app/watcher.py

236 lines
6.5 KiB
Python

#
#
#
from threading import Thread
import time
from logging import getLogger as _Logger
from logitech.unifying_receiver import api
from logitech.unifying_receiver.listener import EventsListener
from logitech import devices
from logitech.devices import constants as C
import actions
_l = _Logger('watcher')
_STATUS_TIMEOUT = 61 # seconds
_THREAD_SLEEP = 3 # seconds
_SLEEP_QUANT = 0.33 # seconds
_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):
timestamp = time.time()
code = C.STATUS.UNKNOWN
text = _INITIALIZING
refresh = None
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, status_changed_callback, notify_callback=None):
super(Watcher, self).__init__(group='Solaar', name='Watcher')
self.daemon = True
self.active = False
self.notify = notify_callback
self.status_text = None
self.status_changed_callback = status_changed_callback
self.listener = None
self.rstatus = _DevStatus(0, 0xFF, None, _UNIFYING_RECEIVER, None, None)
self.rstatus.max_devices = api.C.MAX_ATTACHED_DEVICES
self.rstatus.refresh = (actions.full_scan, self)
self.rstatus.pair = None # (actions.pair, self)
self.devices = {}
def run(self):
self.active = True
while self.active:
if self.listener is None:
self._update_status_text()
receiver = api.open()
if receiver:
self._device_status_changed(self.rstatus, (C.STATUS.BOOTING, _SCANNING))
self._update_status_text()
for devinfo in api.list_devices(receiver):
self._new_device(devinfo)
if self.devices:
self._update_status_text()
self.listener = EventsListener(receiver, self._events_callback)
self.listener.start()
# need to wait for the thread to come alive
time.sleep(_SLEEP_QUANT / 2)
elif not self.listener:
self.listener = None
self.devices.clear()
if self.listener:
if self.devices:
update_icon = self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _OKAY))
update_icon |= self._check_old_statuses()
else:
update_icon = self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _NO_DEVICES))
else:
update_icon = self._device_status_changed(self.rstatus, (C.STATUS.UNAVAILABLE, _NO_RECEIVER))
if update_icon:
self._update_status_text()
for i in range(0, int(_THREAD_SLEEP / _SLEEP_QUANT)):
if self.active:
time.sleep(_SLEEP_QUANT)
else:
break
if self.listener:
self.listener.stop()
api.close(self.listener.receiver)
self.listener = None
def stop(self):
self.active = False
self.join()
def _request_status(self, devstatus):
if self.listener and devstatus:
status = devices.request_status(devstatus, self.listener)
self._device_status_changed(devstatus, status)
def _check_old_statuses(self):
updated = False
for devstatus in self.devices.values():
if devstatus != self.rstatus:
if time.time() - devstatus.timestamp > _STATUS_TIMEOUT:
status = devices.ping(devstatus, self.listener)
updated |= self._device_status_changed(devstatus, status)
return updated
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)
devstatus.refresh = self._request_status
self.devices[dev.number] = devstatus
_l.debug("new devstatus %s", devstatus)
self._device_status_changed(devstatus, C.STATUS.CONNECTED)
self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _OKAY))
return devstatus
def _device_status_changed(self, devstatus, status):
if status is None:
return False
old_status_code = devstatus.code
devstatus.timestamp = time.time()
if type(status) == int:
status_code = status
if status_code in C.STATUS_NAME:
status_text = C.STATUS_NAME[status_code]
else:
status_code = status[0]
if isinstance(status[1], str):
status_text = status[1]
elif isinstance(status[1], dict):
status_text = ''
for key, value in status[1].items():
if key == 'text':
status_text = value
else:
setattr(devstatus, key, value)
else:
_l.warn("don't know how to handle status %s", status)
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:
self.notify(devstatus.code, devstatus.name, devstatus.text)
return True
def _events_callback(self, code, devnumber, data):
# _l.debug("event %s", (code, devnumber, data))
updated = False
if devnumber in self.devices:
devstatus = self.devices[devnumber]
if code == 0x10 and data[:1] == b'\x8F':
updated = True
self._device_status_changed(devstatus, C.STATUS.UNAVAILABLE)
elif code == 0x11:
status = devices.process_event(devstatus, data)
updated |= self._device_status_changed(devstatus, status)
else:
_l.warn("unknown event code %02x", code)
elif devnumber:
self._new_device(devnumber)
updated = True
else:
_l.warn("don't know how to handle event %s", (code, devnumber, data))
if updated:
self._update_status_text()
def _update_status_text(self):
last_status_text = self.status_text
if self.devices:
lines = []
if self.rstatus.code < C.STATUS.CONNECTED:
lines += (self.rstatus.text, '')
devstatuses = [self.devices[d] for d in range(1, 1 + self.rstatus.max_devices) if d in self.devices]
for devstatus in devstatuses:
if devstatus.text:
if ' ' in devstatus.text:
lines += ('<b>' + devstatus.name + '</b>', ' ' + devstatus.text)
else:
lines.append('<b>' + devstatus.name + '</b> ' + devstatus.text)
else:
lines.append('<b>' + devstatus.name + '</b>')
lines.append('')
self.status_text = '\n'.join(lines).rstrip('\n')
else:
self.status_text = self.rstatus.text
if self.status_text != last_status_text:
self.status_changed_callback(self.status_text, self.rstatus, self.devices)