initial implementation of pairing
This commit is contained in:
parent
f2dac70131
commit
b10ade4430
|
@ -0,0 +1,103 @@
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from logging import getLogger as _Logger
|
||||||
|
|
||||||
|
from receiver import DeviceInfo as _DeviceInfo
|
||||||
|
from logitech.devices.constants import (STATUS, NAMES)
|
||||||
|
|
||||||
|
_l = _Logger('pairing')
|
||||||
|
|
||||||
|
|
||||||
|
class State(object):
|
||||||
|
TICK = 300
|
||||||
|
PAIR_TIMEOUT = 60 * 1000 / TICK
|
||||||
|
|
||||||
|
def __init__(self, watcher):
|
||||||
|
self._watcher = watcher
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.success = None
|
||||||
|
self.detected_device = None
|
||||||
|
self._countdown = self.PAIR_TIMEOUT
|
||||||
|
|
||||||
|
def countdown(self, assistant):
|
||||||
|
if self._countdown == self.PAIR_TIMEOUT:
|
||||||
|
self.start_scan()
|
||||||
|
self._countdown -= 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
self._countdown -= 1
|
||||||
|
if self._countdown > 0 and self.success is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.stop_scan()
|
||||||
|
assistant.scan_complete(assistant, self.detected_device)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start_scan(self):
|
||||||
|
self.reset()
|
||||||
|
self._watcher.receiver.events_filter = self.filter_events
|
||||||
|
reply = self._watcher.receiver.request(0xFF, b'\x80\xB2', b'\x01')
|
||||||
|
_l.debug("start scan reply %s", repr(reply))
|
||||||
|
|
||||||
|
def stop_scan(self):
|
||||||
|
if self._countdown >= 0:
|
||||||
|
self._countdown = -1
|
||||||
|
|
||||||
|
reply = self._watcher.receiver.request(0xFF, b'\x80\xB2', b'\x02')
|
||||||
|
_l.debug("stop scan reply %s", repr(reply))
|
||||||
|
self._watcher.receiver.events_filter = None
|
||||||
|
|
||||||
|
def filter_events(self, event):
|
||||||
|
if event.devnumber == 0xFF:
|
||||||
|
if event.code == 0x10:
|
||||||
|
if event.data == b'\x4A\x01\x00\x00\x00':
|
||||||
|
_l.debug("receiver listening for device wakeup")
|
||||||
|
return True
|
||||||
|
if event.data == b'\x4A\x00\x01\x00\x00':
|
||||||
|
_l.debug("receiver gave up")
|
||||||
|
self.success = False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
if event.devnumber in self._watcher.receiver.devices:
|
||||||
|
return False
|
||||||
|
|
||||||
|
_l.debug("event for new device? %s", event)
|
||||||
|
if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
|
||||||
|
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 \
|
||||||
|
None
|
||||||
|
if state is None:
|
||||||
|
_l.warn("don't know how to handle status 0x%02x: %s", state_code, event)
|
||||||
|
elif event.devnumber < 1 or event.devnumber > self.max_devices:
|
||||||
|
_l.warn("got event for invalid device number %d: %s", event.devnumber, event)
|
||||||
|
else:
|
||||||
|
dev = _DeviceInfo(self._watcher.receiver, event.devnumber, state)
|
||||||
|
if state == STATUS.CONNECTED:
|
||||||
|
n, k = dev.name, dev.kind
|
||||||
|
_l.debug("detected active device %s", dev)
|
||||||
|
else:
|
||||||
|
# we can query the receiver for the device short name
|
||||||
|
dev_id = self.request(0xFF, b'\x83\xB5', event.data[4:5])
|
||||||
|
if dev_id:
|
||||||
|
shortname = str(dev_id[2:].rstrip(b'\x00'))
|
||||||
|
if shortname in NAMES:
|
||||||
|
dev._name, dev._kind = NAMES[shortname]
|
||||||
|
_l.debug("detected new device %s", dev)
|
||||||
|
else:
|
||||||
|
_l.warn("could not properly detect inactive device %d: %s", event.devnumber, shortname)
|
||||||
|
self.detected_device = dev
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def unpair(receiver, devnumber):
|
||||||
|
reply = receiver.request(0xFF, b'\x80\xB2', b'\x03' + chr(devnumber))
|
||||||
|
_l.debug("unpair %d reply %s", devnumber, repr(reply))
|
||||||
|
|
|
@ -66,7 +66,7 @@ class DeviceInfo(object):
|
||||||
def name(self):
|
def name(self):
|
||||||
if self._name is None:
|
if self._name is None:
|
||||||
if self._status >= STATUS.CONNECTED:
|
if self._status >= STATUS.CONNECTED:
|
||||||
self._name = self.receiver.request(_api.get_device_name, self.number, self.features)
|
self._name = self.receiver.call_api(_api.get_device_name, self.number, self.features)
|
||||||
return self._name or '?'
|
return self._name or '?'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -77,25 +77,25 @@ class DeviceInfo(object):
|
||||||
def kind(self):
|
def kind(self):
|
||||||
if self._kind is None:
|
if self._kind is None:
|
||||||
if self._status >= STATUS.CONNECTED:
|
if self._status >= STATUS.CONNECTED:
|
||||||
self._kind = self.receiver.request(_api.get_device_kind, self.number, self.features)
|
self._kind = self.receiver.call_api(_api.get_device_kind, self.number, self.features)
|
||||||
return self._kind or '?'
|
return self._kind or '?'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def firmware(self):
|
def firmware(self):
|
||||||
if self._firmware is None:
|
if self._firmware is None:
|
||||||
if self._status >= STATUS.CONNECTED:
|
if self._status >= STATUS.CONNECTED:
|
||||||
self._firmware = self.receiver.request(_api.get_device_firmware, self.number, self.features)
|
self._firmware = self.receiver.call_api(_api.get_device_firmware, self.number, self.features)
|
||||||
return self._firmware or ()
|
return self._firmware or ()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def features(self):
|
def features(self):
|
||||||
if self._features is None:
|
if self._features is None:
|
||||||
if self._status >= STATUS.CONNECTED:
|
if self._status >= STATUS.CONNECTED:
|
||||||
self._features = self.receiver.request(_api.get_device_features, self.number)
|
self._features = self.receiver.call_api(_api.get_device_features, self.number)
|
||||||
return self._features or ()
|
return self._features or ()
|
||||||
|
|
||||||
def ping(self):
|
def ping(self):
|
||||||
return self.receiver.request(_api.ping, self.number)
|
return self.receiver.call_api(_api.ping, self.number)
|
||||||
|
|
||||||
def process_event(self, code, data):
|
def process_event(self, code, data):
|
||||||
if code == 0x10 and data[:1] == b'\x8F':
|
if code == 0x10 and data[:1] == b'\x8F':
|
||||||
|
@ -155,12 +155,11 @@ class Receiver(_listener.EventsListener):
|
||||||
self.LOG.info("initializing")
|
self.LOG.info("initializing")
|
||||||
|
|
||||||
self.devices = {}
|
self.devices = {}
|
||||||
|
self.events_filter = None
|
||||||
self.events_handler = None
|
self.events_handler = None
|
||||||
|
|
||||||
init = (_base.request(handle, 0xFF, b'\x81\x00') and
|
if (_base.request(handle, 0xFF, b'\x81\x00') and
|
||||||
_base.request(handle, 0xFF, b'\x80\x00', b'\x00\x01') and
|
_base.request(handle, 0xFF, b'\x80\x00', b'\x00\x01')):
|
||||||
_base.request(handle, 0xFF, b'\x81\x02'))
|
|
||||||
if init:
|
|
||||||
self.LOG.info("initialized")
|
self.LOG.info("initialized")
|
||||||
else:
|
else:
|
||||||
self.LOG.warn("initialization failed")
|
self.LOG.warn("initialization failed")
|
||||||
|
@ -210,12 +209,18 @@ class Receiver(_listener.EventsListener):
|
||||||
def device_name(self):
|
def device_name(self):
|
||||||
return self.NAME
|
return self.NAME
|
||||||
|
|
||||||
|
def count_devices(self):
|
||||||
|
return self.call_api(_api.count_devices)
|
||||||
|
|
||||||
def _device_changed(self, dev, urgent=False):
|
def _device_changed(self, dev, urgent=False):
|
||||||
self.status_changed.reason = dev
|
self.status_changed.reason = dev
|
||||||
self.status_changed.urgent = urgent
|
self.status_changed.urgent = urgent
|
||||||
self.status_changed.set()
|
self.status_changed.set()
|
||||||
|
|
||||||
def _events_handler(self, event):
|
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.code == 0x10 and event.data[0:2] == b'\x41\x04':
|
||||||
state_code = ord(event.data[2:3]) & 0xF0
|
state_code = ord(event.data[2:3]) & 0xF0
|
||||||
state = STATUS.UNAVAILABLE if state_code == 0x60 else \
|
state = STATUS.UNAVAILABLE if state_code == 0x60 else \
|
||||||
|
@ -239,7 +244,7 @@ class Receiver(_listener.EventsListener):
|
||||||
n, k = dev.name, dev.kind
|
n, k = dev.name, dev.kind
|
||||||
else:
|
else:
|
||||||
# we can query the receiver for the device short name
|
# we can query the receiver for the device short name
|
||||||
dev_id = self.request(_base.request, 0xFF, b'\x83\xB5', event.data[4:5])
|
dev_id = self.request(0xFF, b'\x83\xB5', event.data[4:5])
|
||||||
if dev_id:
|
if dev_id:
|
||||||
shortname = str(dev_id[2:].rstrip(b'\x00'))
|
shortname = str(dev_id[2:].rstrip(b'\x00'))
|
||||||
if shortname in NAMES:
|
if shortname in NAMES:
|
||||||
|
@ -258,14 +263,15 @@ class Receiver(_listener.EventsListener):
|
||||||
self.devices = {}
|
self.devices = {}
|
||||||
self.status = STATUS.UNAVAILABLE
|
self.status = STATUS.UNAVAILABLE
|
||||||
return
|
return
|
||||||
self.LOG.warn("don't know how to handle event %s", event)
|
|
||||||
elif event.devnumber in self.devices:
|
elif event.devnumber in self.devices:
|
||||||
dev = self.devices[event.devnumber]
|
dev = self.devices[event.devnumber]
|
||||||
if dev.process_event(event.code, event.data):
|
if dev.process_event(event.code, event.data):
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.events_handler:
|
if self.events_handler and self.events_handler(event):
|
||||||
self.events_handler(event)
|
return
|
||||||
|
|
||||||
|
self.LOG.warn("don't know how to handle event %s", event)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'Receiver(%s,%x,%d:%d)' % (self.path, self._handle, self._active, self._status)
|
return 'Receiver(%s,%x,%d:%d)' % (self.path, self._handle, self._active, self._status)
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
__author__ = "Daniel Pavel <daniel.pavel@gmail.com>"
|
||||||
__version__ = '0.5'
|
__version__ = '0.5'
|
||||||
|
__license__ = "GPL"
|
||||||
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
APP_TITLE = 'Solaar'
|
APPNAME = 'Solaar'
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def _parse_arguments():
|
||||||
import argparse
|
import argparse
|
||||||
arg_parser = argparse.ArgumentParser(prog=APP_TITLE)
|
arg_parser = argparse.ArgumentParser(prog=APPNAME.lower())
|
||||||
arg_parser.add_argument('-v', '--verbose',
|
arg_parser.add_argument('-v', '--verbose',
|
||||||
action='count', default=0,
|
action='count', default=0,
|
||||||
help='increase the logger verbosity (may be repeated)')
|
help='increase the logger verbosity (may be repeated)')
|
||||||
|
@ -31,59 +33,50 @@ if __name__ == '__main__':
|
||||||
import logging
|
import logging
|
||||||
log_level = logging.root.level - 10 * args.verbose
|
log_level = logging.root.level - 10 * args.verbose
|
||||||
log_format='%(asctime)s.%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
log_format='%(asctime)s.%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
||||||
logging.basicConfig(level=log_level if log_level > 0 else 1, format=log_format, datefmt='%H:%M:%S')
|
logging.basicConfig(level=log_level if log_level > 0 else 1,
|
||||||
|
format=log_format,
|
||||||
|
datefmt='%H:%M:%S')
|
||||||
|
|
||||||
from gi.repository import GObject
|
return args
|
||||||
GObject.threads_init()
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
args = _parse_arguments()
|
||||||
|
|
||||||
import ui
|
import ui
|
||||||
|
|
||||||
|
# check if the notifications are available
|
||||||
args.notifications &= args.systray
|
args.notifications &= args.systray
|
||||||
if args.notifications:
|
if ui.notify.init(APPNAME):
|
||||||
args.notifications &= ui.notify.init(APP_TITLE)
|
ui.action.toggle_notifications.set_active(args.notifications)
|
||||||
|
else:
|
||||||
|
ui.action.toggle_notifications = None
|
||||||
|
|
||||||
import watcher
|
import watcher
|
||||||
tray_icon = None
|
|
||||||
window = ui.window.create(APP_TITLE,
|
|
||||||
watcher.DUMMY.NAME,
|
|
||||||
watcher.DUMMY.max_devices,
|
|
||||||
args.systray)
|
|
||||||
window.set_icon_name(APP_TITLE + '-init')
|
|
||||||
|
|
||||||
def _ui_update(receiver, tray_icon, window):
|
window = ui.main_window.create(APPNAME,
|
||||||
icon_name = APP_TITLE + '-fail' if receiver.status < 1 else APP_TITLE
|
watcher.DUMMY.NAME,
|
||||||
if window:
|
watcher.DUMMY.max_devices,
|
||||||
GObject.idle_add(ui.window.update, window, receiver, icon_name)
|
args.systray)
|
||||||
if tray_icon:
|
|
||||||
GObject.idle_add(ui.icon.update, tray_icon, receiver, icon_name)
|
|
||||||
|
|
||||||
def _notify(device):
|
|
||||||
GObject.idle_add(ui.notify.show, device)
|
|
||||||
|
|
||||||
w = watcher.Watcher(APP_TITLE,
|
|
||||||
lambda r: _ui_update(r, tray_icon, window),
|
|
||||||
_notify if args.notifications else None)
|
|
||||||
w.start()
|
|
||||||
|
|
||||||
if args.systray:
|
if args.systray:
|
||||||
def _toggle_notifications(item):
|
menu_actions = (ui.action.pair,
|
||||||
# logging.debug("toggle notifications %s", item)
|
ui.action.toggle_notifications,
|
||||||
if ui.notify.available:
|
ui.action.about)
|
||||||
if item.get_active():
|
icon = ui.status_icon.create(window, menu_actions)
|
||||||
ui.notify.init(APP_TITLE)
|
|
||||||
else:
|
|
||||||
ui.notify.uninit()
|
|
||||||
item.set_sensitive(ui.notify.available)
|
|
||||||
|
|
||||||
menu = (
|
|
||||||
('Notifications', _toggle_notifications if args.notifications else None, args.notifications),
|
|
||||||
)
|
|
||||||
|
|
||||||
tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window), menu)
|
|
||||||
tray_icon.set_from_icon_name(APP_TITLE + '-init')
|
|
||||||
else:
|
else:
|
||||||
|
icon = None
|
||||||
window.present()
|
window.present()
|
||||||
|
|
||||||
|
w = watcher.Watcher(APPNAME,
|
||||||
|
lambda r: ui.update(r, icon, window),
|
||||||
|
ui.notify.show if ui.notify.available else None)
|
||||||
|
w.start()
|
||||||
|
|
||||||
|
import pairing
|
||||||
|
ui.action.pair.connect('activate', ui.action._pair_device,
|
||||||
|
window, pairing.State(w))
|
||||||
|
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
Gtk.main()
|
Gtk.main()
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,58 @@
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
from . import (notify, icon, window, pair)
|
APPNAME = 'Solaar'
|
||||||
|
APPVERSION = '0.5'
|
||||||
|
|
||||||
|
from . import (notify, status_icon, main_window, pair_window, action)
|
||||||
|
|
||||||
|
from gi.repository import (GObject, Gtk)
|
||||||
|
GObject.threads_init()
|
||||||
|
|
||||||
|
|
||||||
|
def appicon(receiver_status):
|
||||||
|
return (APPNAME + '-fail' if receiver_status < 0 else
|
||||||
|
APPNAME + '-init' if receiver_status < 1 else
|
||||||
|
APPNAME)
|
||||||
|
|
||||||
|
|
||||||
|
_THEME = Gtk.IconTheme.get_default()
|
||||||
|
|
||||||
|
def get_icon(name, fallback):
|
||||||
|
return name if name and _THEME.has_icon(name) else fallback
|
||||||
|
|
||||||
|
def icon_file(name):
|
||||||
|
if name and _THEME.has_icon(name):
|
||||||
|
return _THEME.lookup_icon(name, 0, 0).get_filename()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_children(container, *child_names):
|
||||||
|
def _iterate_children(widget, names, result, count):
|
||||||
|
wname = widget.get_name()
|
||||||
|
if wname in names:
|
||||||
|
index = names.index(wname)
|
||||||
|
names[index] = None
|
||||||
|
result[index] = widget
|
||||||
|
count -= 1
|
||||||
|
|
||||||
|
if count > 0 and isinstance(widget, Gtk.Container):
|
||||||
|
for w in widget:
|
||||||
|
count = _iterate_children(w, names, result, count)
|
||||||
|
if count == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
names = list(child_names)
|
||||||
|
count = len(names)
|
||||||
|
result = [None] * count
|
||||||
|
_iterate_children(container, names, result, count)
|
||||||
|
return tuple(result) if count > 1 else result[0]
|
||||||
|
|
||||||
|
|
||||||
|
def update(receiver, icon, window):
|
||||||
|
GObject.idle_add(action.pair.set_sensitive, receiver.status > 0)
|
||||||
|
if window:
|
||||||
|
GObject.idle_add(main_window.update, window, receiver)
|
||||||
|
if icon:
|
||||||
|
GObject.idle_add(status_icon.update, icon, receiver)
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
import ui
|
||||||
|
|
||||||
|
|
||||||
|
def _action(name, label, function, *args):
|
||||||
|
action = Gtk.Action(name, label, label, None)
|
||||||
|
action.set_icon_name(name)
|
||||||
|
if function:
|
||||||
|
action.connect('activate', function, *args)
|
||||||
|
return action
|
||||||
|
|
||||||
|
|
||||||
|
def _toggle_action(name, label, function, *args):
|
||||||
|
action = Gtk.ToggleAction(name, label, label, None)
|
||||||
|
action.set_icon_name(name)
|
||||||
|
action.connect('activate', function, *args)
|
||||||
|
return action
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
def _toggle_notifications(action):
|
||||||
|
if action.get_active():
|
||||||
|
ui.notify.init(ui.APPNAME)
|
||||||
|
else:
|
||||||
|
ui.notify.uninit()
|
||||||
|
action.set_sensitive(ui.notify.available)
|
||||||
|
toggle_notifications = _toggle_action('notifications', 'Notifications', _toggle_notifications)
|
||||||
|
|
||||||
|
|
||||||
|
def _show_about_window(action):
|
||||||
|
about = Gtk.AboutDialog()
|
||||||
|
about.set_icon_name(ui.APPNAME)
|
||||||
|
about.set_program_name(ui.APPNAME)
|
||||||
|
about.set_logo_icon_name(ui.APPNAME)
|
||||||
|
about.set_version(ui.APPVERSION)
|
||||||
|
about.set_license_type(Gtk.License.GPL_2_0)
|
||||||
|
about.set_authors(('Daniel Pavel http://github.com/pwr', ))
|
||||||
|
about.set_website('http://github.com/pwr/Solaar/wiki')
|
||||||
|
about.run()
|
||||||
|
about.destroy()
|
||||||
|
about = _action('help-about', 'About ' + ui.APPNAME, _show_about_window)
|
||||||
|
|
||||||
|
|
||||||
|
def _pair_device(action, window, state):
|
||||||
|
action.set_sensitive(False)
|
||||||
|
pair_dialog = ui.pair_window.create(action, state)
|
||||||
|
# window.present()
|
||||||
|
# pair_dialog.set_transient_for(parent_window)
|
||||||
|
# pair_dialog.set_destroy_with_parent(parent_window)
|
||||||
|
# pair_dialog.set_modal(True)
|
||||||
|
pair_dialog.present()
|
||||||
|
pair = _action('add', 'Pair new device', None)
|
|
@ -1,79 +0,0 @@
|
||||||
#
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
|
|
||||||
def create(title, click_action=None, actions=None):
|
|
||||||
icon = Gtk.StatusIcon()
|
|
||||||
icon.set_title(title)
|
|
||||||
icon.set_name(title)
|
|
||||||
|
|
||||||
if click_action:
|
|
||||||
if type(click_action) == tuple:
|
|
||||||
function = click_action[0]
|
|
||||||
args = click_action[1:]
|
|
||||||
icon.connect('activate', function, *args)
|
|
||||||
else:
|
|
||||||
icon.connect('activate', click_action)
|
|
||||||
|
|
||||||
menu = Gtk.Menu()
|
|
||||||
|
|
||||||
if actions:
|
|
||||||
for name, activate, checked in actions:
|
|
||||||
if checked is None:
|
|
||||||
item = Gtk.MenuItem(name)
|
|
||||||
if activate is None:
|
|
||||||
item.set_sensitive(False)
|
|
||||||
else:
|
|
||||||
item.connect('activate', activate)
|
|
||||||
else:
|
|
||||||
item = Gtk.CheckMenuItem(name)
|
|
||||||
if activate is None:
|
|
||||||
item.set_sensitive(False)
|
|
||||||
else:
|
|
||||||
item.set_active(checked or False)
|
|
||||||
item.connect('toggled', activate)
|
|
||||||
|
|
||||||
menu.append(item)
|
|
||||||
menu.append(Gtk.SeparatorMenuItem())
|
|
||||||
|
|
||||||
quit_item = Gtk.MenuItem('Quit')
|
|
||||||
quit_item.connect('activate', Gtk.main_quit)
|
|
||||||
menu.append(quit_item)
|
|
||||||
|
|
||||||
menu.show_all()
|
|
||||||
|
|
||||||
icon.connect('popup_menu',
|
|
||||||
lambda icon, button, time, menu:
|
|
||||||
menu.popup(None, None, icon.position_menu, icon, button, time),
|
|
||||||
menu)
|
|
||||||
|
|
||||||
return icon
|
|
||||||
|
|
||||||
|
|
||||||
def update(icon, receiver, icon_name=None):
|
|
||||||
if icon_name is not None:
|
|
||||||
icon.set_from_icon_name(icon_name)
|
|
||||||
|
|
||||||
if receiver.devices:
|
|
||||||
lines = []
|
|
||||||
if receiver.status < 1:
|
|
||||||
lines += (receiver.status_text, '')
|
|
||||||
|
|
||||||
devlist = [receiver.devices[d] for d in range(1, 1 + receiver.max_devices) if d in receiver.devices]
|
|
||||||
for dev in devlist:
|
|
||||||
name = '<b>' + dev.name + '</b>'
|
|
||||||
if dev.status < 1:
|
|
||||||
lines.append(name + ' (' + dev.status_text + ')')
|
|
||||||
else:
|
|
||||||
lines.append(name)
|
|
||||||
if dev.status > 1:
|
|
||||||
lines.append(' ' + dev.status_text)
|
|
||||||
lines.append('')
|
|
||||||
|
|
||||||
text = '\n'.join(lines).rstrip('\n')
|
|
||||||
icon.set_tooltip_markup(text)
|
|
||||||
else:
|
|
||||||
icon.set_tooltip_text(receiver.status_text)
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
from gi.repository import (Gtk, Gdk)
|
from gi.repository import (Gtk, Gdk)
|
||||||
|
|
||||||
|
import ui
|
||||||
from logitech.devices.constants import (STATUS, PROPS)
|
from logitech.devices.constants import (STATUS, PROPS)
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,37 +14,10 @@ _STATUS_ICON_SIZE = Gtk.IconSize.DND
|
||||||
_PLACEHOLDER = '~'
|
_PLACEHOLDER = '~'
|
||||||
|
|
||||||
|
|
||||||
theme = Gtk.IconTheme.get_default()
|
|
||||||
|
|
||||||
|
|
||||||
def _find_children(container, *child_names):
|
|
||||||
def _iterate_children(widget, names, result, count):
|
|
||||||
wname = widget.get_name()
|
|
||||||
if wname in names:
|
|
||||||
index = names.index(wname)
|
|
||||||
names[index] = None
|
|
||||||
result[index] = widget
|
|
||||||
count -= 1
|
|
||||||
|
|
||||||
if count > 0 and isinstance(widget, Gtk.Container):
|
|
||||||
for w in widget:
|
|
||||||
count = _iterate_children(w, names, result, count)
|
|
||||||
if count == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
return count
|
|
||||||
|
|
||||||
names = list(child_names)
|
|
||||||
count = len(names)
|
|
||||||
result = [None] * count
|
|
||||||
_iterate_children(container, names, result, count)
|
|
||||||
return result if count > 1 else result[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _update_receiver_box(box, receiver):
|
def _update_receiver_box(box, receiver):
|
||||||
label, buttons = _find_children(box, 'label', 'buttons')
|
label, buttons = ui.find_children(box, 'label', 'buttons')
|
||||||
label.set_text(receiver.status_text or '')
|
label.set_text(receiver.status_text or '')
|
||||||
buttons.set_visible(receiver.status >= STATUS.CONNECTED)
|
# buttons.set_visible(receiver.status >= STATUS.CONNECTED)
|
||||||
|
|
||||||
|
|
||||||
def _update_device_box(frame, dev):
|
def _update_device_box(frame, dev):
|
||||||
|
@ -52,19 +26,16 @@ def _update_device_box(frame, dev):
|
||||||
frame.set_name(_PLACEHOLDER)
|
frame.set_name(_PLACEHOLDER)
|
||||||
return
|
return
|
||||||
|
|
||||||
icon, label = _find_children(frame, 'icon', 'label')
|
icon, label = ui.find_children(frame, 'icon', 'label')
|
||||||
|
|
||||||
frame.set_visible(True)
|
frame.set_visible(True)
|
||||||
if frame.get_name() != dev.name:
|
if frame.get_name() != dev.name:
|
||||||
frame.set_name(dev.name)
|
frame.set_name(dev.name)
|
||||||
if theme.has_icon(dev.name):
|
icon.set_from_icon_name(ui.get_icon(dev.name, dev.kind), _DEVICE_ICON_SIZE)
|
||||||
icon.set_from_icon_name(dev.name, _DEVICE_ICON_SIZE)
|
|
||||||
else:
|
|
||||||
icon.set_from_icon_name(dev.kind, _DEVICE_ICON_SIZE)
|
|
||||||
icon.set_tooltip_text(dev.name)
|
icon.set_tooltip_text(dev.name)
|
||||||
label.set_markup('<b>' + dev.name + '</b>')
|
label.set_markup('<b>' + dev.name + '</b>')
|
||||||
|
|
||||||
status = _find_children(frame, 'status')
|
status = ui.find_children(frame, 'status')
|
||||||
if dev.status < STATUS.CONNECTED:
|
if dev.status < STATUS.CONNECTED:
|
||||||
icon.set_sensitive(False)
|
icon.set_sensitive(False)
|
||||||
icon.set_tooltip_text(dev.status_text)
|
icon.set_tooltip_text(dev.status_text)
|
||||||
|
@ -111,10 +82,9 @@ def _update_device_box(frame, dev):
|
||||||
light_label.set_text('%d lux' % light_level)
|
light_label.set_text('%d lux' % light_level)
|
||||||
|
|
||||||
|
|
||||||
def update(window, receiver, icon_name=None):
|
def update(window, receiver):
|
||||||
if window and window.get_child():
|
if window and window.get_child():
|
||||||
if icon_name is not None:
|
window.set_icon_name(ui.appicon(receiver.status))
|
||||||
window.set_icon_name(icon_name)
|
|
||||||
|
|
||||||
vbox = window.get_child()
|
vbox = window.get_child()
|
||||||
controls = list(vbox.get_children())
|
controls = list(vbox.get_children())
|
||||||
|
@ -132,7 +102,7 @@ def update(window, receiver, icon_name=None):
|
||||||
def _receiver_box(name):
|
def _receiver_box(name):
|
||||||
box = _device_box(False, False)
|
box = _device_box(False, False)
|
||||||
|
|
||||||
icon, status_box = _find_children(box, 'icon', 'status')
|
icon, status_box = ui.find_children(box, 'icon', 'status')
|
||||||
icon.set_from_icon_name(name, _SMALL_DEVICE_ICON_SIZE)
|
icon.set_from_icon_name(name, _SMALL_DEVICE_ICON_SIZE)
|
||||||
icon.set_tooltip_text(name)
|
icon.set_tooltip_text(name)
|
||||||
|
|
||||||
|
@ -142,14 +112,10 @@ def _receiver_box(name):
|
||||||
toolbar.set_icon_size(Gtk.IconSize.MENU)
|
toolbar.set_icon_size(Gtk.IconSize.MENU)
|
||||||
toolbar.set_show_arrow(False)
|
toolbar.set_show_arrow(False)
|
||||||
|
|
||||||
pair_button = Gtk.ToolButton()
|
toolbar.insert(ui.action.pair.create_tool_item(), 0)
|
||||||
pair_button.set_icon_name('add')
|
|
||||||
pair_button.set_tooltip_text('Pair new device')
|
|
||||||
pair_button.set_sensitive(False)
|
|
||||||
toolbar.insert(pair_button, 0)
|
|
||||||
|
|
||||||
toolbar.show_all()
|
toolbar.show_all()
|
||||||
toolbar.set_visible(False)
|
# toolbar.set_visible(False)
|
||||||
status_box.pack_end(toolbar, False, False, 0)
|
status_box.pack_end(toolbar, False, False, 0)
|
||||||
|
|
||||||
return box
|
return box
|
||||||
|
@ -208,10 +174,26 @@ def _device_box(has_status_icons=True, has_frame=True):
|
||||||
return box
|
return box
|
||||||
|
|
||||||
|
|
||||||
|
def toggle(window, trigger):
|
||||||
|
# print 'window toggle', window, trigger
|
||||||
|
if window.get_visible():
|
||||||
|
position = window.get_position()
|
||||||
|
window.hide()
|
||||||
|
window.move(*position)
|
||||||
|
else:
|
||||||
|
if trigger and type(trigger) == Gtk.StatusIcon:
|
||||||
|
x, y = window.get_position()
|
||||||
|
if x == 0 and y == 0:
|
||||||
|
x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), trigger)
|
||||||
|
window.move(x, y)
|
||||||
|
window.present()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def create(title, name, max_devices, systray=False):
|
def create(title, name, max_devices, systray=False):
|
||||||
window = Gtk.Window()
|
window = Gtk.Window()
|
||||||
window.set_title(title)
|
window.set_title(title)
|
||||||
# window.set_icon_name(title)
|
window.set_icon_name(ui.appicon(0))
|
||||||
window.set_role('status-window')
|
window.set_role('status-window')
|
||||||
|
|
||||||
vbox = Gtk.VBox(homogeneous=False, spacing=4)
|
vbox = Gtk.VBox(homogeneous=False, spacing=4)
|
||||||
|
@ -227,46 +209,17 @@ def create(title, name, max_devices, systray=False):
|
||||||
window.add(vbox)
|
window.add(vbox)
|
||||||
|
|
||||||
geometry = Gdk.Geometry()
|
geometry = Gdk.Geometry()
|
||||||
geometry.min_width = 260
|
geometry.min_width = 300
|
||||||
geometry.min_height = 40
|
geometry.min_height = 40
|
||||||
window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE)
|
window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE)
|
||||||
window.set_resizable(False)
|
window.set_resizable(False)
|
||||||
|
|
||||||
|
window.toggle_visible = lambda i: toggle(window, i)
|
||||||
|
|
||||||
if systray:
|
if systray:
|
||||||
# def _state_event(w, e):
|
|
||||||
# if e.new_window_state & Gdk.WindowState.ICONIFIED:
|
|
||||||
# w.hide()
|
|
||||||
# w.deiconify()
|
|
||||||
# return True
|
|
||||||
# window.connect('window-state-event', _state_event)
|
|
||||||
|
|
||||||
window.set_keep_above(True)
|
window.set_keep_above(True)
|
||||||
window.set_deletable(False)
|
window.connect('delete-event', toggle)
|
||||||
# window.set_decorated(False)
|
|
||||||
# window.set_position(Gtk.WindowPosition.MOUSE)
|
|
||||||
# ulgy, but hides the minimize icon from the window
|
|
||||||
window.set_type_hint(Gdk.WindowTypeHint.MENU)
|
|
||||||
window.set_skip_taskbar_hint(True)
|
|
||||||
window.set_skip_pager_hint(True)
|
|
||||||
|
|
||||||
window.connect('delete-event', lambda w, e: toggle(None, w) or True)
|
|
||||||
else:
|
else:
|
||||||
# window.set_position(Gtk.WindowPosition.CENTER)
|
|
||||||
window.connect('delete-event', Gtk.main_quit)
|
window.connect('delete-event', Gtk.main_quit)
|
||||||
|
|
||||||
return window
|
return window
|
||||||
|
|
||||||
|
|
||||||
def toggle(icon, window):
|
|
||||||
if window.get_visible():
|
|
||||||
position = window.get_position()
|
|
||||||
window.hide()
|
|
||||||
window.move(*position)
|
|
||||||
else:
|
|
||||||
if icon:
|
|
||||||
x, y = window.get_position()
|
|
||||||
if x == 0 and y == 0:
|
|
||||||
x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), icon)
|
|
||||||
window.move(x, y)
|
|
||||||
window.present()
|
|
||||||
return True
|
|
|
@ -7,18 +7,16 @@ import logging
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from gi.repository import Notify
|
from gi.repository import Notify
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
|
import ui
|
||||||
from logitech.devices.constants import STATUS
|
from logitech.devices.constants import STATUS
|
||||||
|
|
||||||
# necessary because the notifications daemon does not know about our XDG_DATA_DIRS
|
# necessary because the notifications daemon does not know about our XDG_DATA_DIRS
|
||||||
theme = Gtk.IconTheme.get_default()
|
|
||||||
_icons = {}
|
_icons = {}
|
||||||
|
|
||||||
def _icon(title):
|
def _icon(title):
|
||||||
if title not in _icons:
|
if title not in _icons:
|
||||||
icon = theme.lookup_icon(title, 0, 0)
|
_icons[title] = ui.icon_file(title)
|
||||||
_icons[title] = icon.get_filename() if icon else None
|
|
||||||
|
|
||||||
return _icons.get(title)
|
return _icons.get(title)
|
||||||
|
|
||||||
|
@ -28,14 +26,14 @@ try:
|
||||||
_notifications = {}
|
_notifications = {}
|
||||||
|
|
||||||
|
|
||||||
def init(app_title=None):
|
def init(app_title):
|
||||||
"""Init the notifications system."""
|
"""Init the notifications system."""
|
||||||
global available
|
global available
|
||||||
if available:
|
if available:
|
||||||
logging.info("starting desktop notifications")
|
|
||||||
if not Notify.is_initted():
|
if not Notify.is_initted():
|
||||||
|
logging.info("starting desktop notifications")
|
||||||
try:
|
try:
|
||||||
return Notify.init(app_title or Notify.get_app_name())
|
return Notify.init(app_title)
|
||||||
except:
|
except:
|
||||||
logging.exception("initializing desktop notifications")
|
logging.exception("initializing desktop notifications")
|
||||||
available = False
|
available = False
|
||||||
|
@ -74,4 +72,4 @@ except ImportError:
|
||||||
available = False
|
available = False
|
||||||
init = lambda app_title: False
|
init = lambda app_title: False
|
||||||
uninit = lambda: None
|
uninit = lambda: None
|
||||||
show = lambda status_code, title, text: None
|
show = lambda dev: None
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
#
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
|
|
||||||
def create(parent_window, title):
|
|
||||||
window = Gtk.Dialog(title, parent_window, Gtk.DialogFlags.MODAL, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL))
|
|
||||||
|
|
||||||
Gtk.Window.set_default_icon_name('add')
|
|
||||||
window.set_resizable(False)
|
|
||||||
# window.set_role('pair-device')
|
|
||||||
|
|
||||||
return window
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from gi.repository import (Gtk, GObject)
|
||||||
|
|
||||||
|
import ui
|
||||||
|
|
||||||
|
|
||||||
|
def _create_page(assistant, text, kind):
|
||||||
|
p = Gtk.VBox(False, 12)
|
||||||
|
p.set_border_width(8)
|
||||||
|
|
||||||
|
if text:
|
||||||
|
label = Gtk.Label(text)
|
||||||
|
label.set_alignment(0, 0)
|
||||||
|
p.pack_start(label, False, True, 0)
|
||||||
|
|
||||||
|
assistant.append_page(p)
|
||||||
|
assistant.set_page_type(p, kind)
|
||||||
|
|
||||||
|
p.show_all()
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def _device_confirmed(entry, _2, trigger, assistant, page):
|
||||||
|
assistant.commit()
|
||||||
|
assistant.set_page_complete(page, True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _finish(assistant, action):
|
||||||
|
logging.debug("finish %s", assistant)
|
||||||
|
assistant.destroy()
|
||||||
|
action.set_sensitive(True)
|
||||||
|
|
||||||
|
def _cancel(assistant, action, state):
|
||||||
|
logging.debug("cancel %s", assistant)
|
||||||
|
state.stop_scan()
|
||||||
|
_finish(assistant, action)
|
||||||
|
|
||||||
|
def _prepare(assistant, page, state):
|
||||||
|
index = assistant.get_current_page()
|
||||||
|
logging.debug("prepare %s %d %s", assistant, index, page)
|
||||||
|
|
||||||
|
if index == 0:
|
||||||
|
state.reset()
|
||||||
|
GObject.timeout_add(state.TICK, state.countdown, assistant)
|
||||||
|
spinner = page.get_children()[-1]
|
||||||
|
spinner.start()
|
||||||
|
return
|
||||||
|
|
||||||
|
assistant.remove_page(0)
|
||||||
|
state.stop_scan()
|
||||||
|
|
||||||
|
def _scan_complete(assistant, device):
|
||||||
|
if device is None:
|
||||||
|
page = _create_page(assistant,
|
||||||
|
'No new device detected.\n'
|
||||||
|
'\n'
|
||||||
|
'Make sure your device is within range of the receiver,\nand it has a decent battery charge.\n',
|
||||||
|
Gtk.AssistantPageType.CONFIRM)
|
||||||
|
else:
|
||||||
|
page = _create_page(assistant,
|
||||||
|
None,
|
||||||
|
Gtk.AssistantPageType.CONFIRM)
|
||||||
|
|
||||||
|
hbox = Gtk.HBox(False, 16)
|
||||||
|
device_icon = Gtk.Image()
|
||||||
|
device_icon.set_from_icon_name(ui.get_icon(device.name, device.kind), Gtk.IconSize.DIALOG)
|
||||||
|
hbox.pack_start(device_icon, False, False, 0)
|
||||||
|
device_label = Gtk.Label(device.kind + '\n' + device.name)
|
||||||
|
hbox.pack_start(device_label, False, False, 0)
|
||||||
|
halign = Gtk.Alignment.new(0.5, 0.5, 0, 1)
|
||||||
|
halign.add(hbox)
|
||||||
|
page.pack_start(halign, False, True, 0)
|
||||||
|
|
||||||
|
hbox = Gtk.HBox(False, 16)
|
||||||
|
hbox.pack_start(Gtk.Entry(), False, False, 0)
|
||||||
|
hbox.pack_start(Gtk.ToggleButton('Test'), False, False, 0)
|
||||||
|
halign = Gtk.Alignment.new(0.5, 0.5, 0, 1)
|
||||||
|
halign.add(hbox)
|
||||||
|
page.pack_start(halign, False, False, 0)
|
||||||
|
|
||||||
|
entry_info = Gtk.Label('Use the controls above to confirm\n'
|
||||||
|
'this is the device you want to pair.')
|
||||||
|
entry_info.set_sensitive(False)
|
||||||
|
page.pack_start(entry_info, False, False, 0)
|
||||||
|
|
||||||
|
page.show_all()
|
||||||
|
assistant.set_page_complete(page, True)
|
||||||
|
|
||||||
|
assistant.next_page()
|
||||||
|
|
||||||
|
|
||||||
|
def create(action, state):
|
||||||
|
assistant = Gtk.Assistant()
|
||||||
|
assistant.set_title(action.get_label())
|
||||||
|
assistant.set_icon_name(action.get_icon_name())
|
||||||
|
|
||||||
|
assistant.set_size_request(440, 240)
|
||||||
|
assistant.set_resizable(False)
|
||||||
|
assistant.set_role('pair-device')
|
||||||
|
|
||||||
|
page_intro = _create_page(assistant,
|
||||||
|
'Turn on the device you want to pair.\n'
|
||||||
|
'\n'
|
||||||
|
'If the device is already turned on,\nturn if off and on again.',
|
||||||
|
Gtk.AssistantPageType.INTRO)
|
||||||
|
spinner = Gtk.Spinner()
|
||||||
|
spinner.set_visible(True)
|
||||||
|
page_intro.pack_end(spinner, True, True, 16)
|
||||||
|
|
||||||
|
assistant.scan_complete = _scan_complete
|
||||||
|
|
||||||
|
assistant.connect('prepare', _prepare, state)
|
||||||
|
assistant.connect('cancel', _cancel, action, state)
|
||||||
|
assistant.connect('close', _finish, action)
|
||||||
|
assistant.connect('apply', _finish, action)
|
||||||
|
|
||||||
|
return assistant
|
|
@ -0,0 +1,56 @@
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from gi.repository import Gtk
|
||||||
|
import ui
|
||||||
|
|
||||||
|
|
||||||
|
def create(window, menu_actions=None):
|
||||||
|
icon = Gtk.StatusIcon()
|
||||||
|
icon.set_title(window.get_title())
|
||||||
|
icon.set_name(window.get_title())
|
||||||
|
icon.set_from_icon_name(ui.appicon(0))
|
||||||
|
|
||||||
|
icon.connect('activate', window.toggle_visible)
|
||||||
|
|
||||||
|
menu = Gtk.Menu()
|
||||||
|
for action in menu_actions or ():
|
||||||
|
if action:
|
||||||
|
menu.append(action.create_menu_item())
|
||||||
|
|
||||||
|
quit_action = ui.action._action('exit', 'Quit', Gtk.main_quit)
|
||||||
|
menu.append(quit_action.create_menu_item())
|
||||||
|
menu.show_all()
|
||||||
|
|
||||||
|
icon.connect('popup_menu',
|
||||||
|
lambda icon, button, time, menu:
|
||||||
|
menu.popup(None, None, icon.position_menu, icon, button, time),
|
||||||
|
menu)
|
||||||
|
|
||||||
|
return icon
|
||||||
|
|
||||||
|
|
||||||
|
def update(icon, receiver):
|
||||||
|
icon.set_from_icon_name(ui.appicon(receiver.status))
|
||||||
|
|
||||||
|
if receiver.devices:
|
||||||
|
lines = []
|
||||||
|
if receiver.status < 1:
|
||||||
|
lines += (receiver.status_text, '')
|
||||||
|
|
||||||
|
devlist = [receiver.devices[d] for d in range(1, 1 + receiver.max_devices) if d in receiver.devices]
|
||||||
|
for dev in devlist:
|
||||||
|
name = '<b>' + dev.name + '</b>'
|
||||||
|
if dev.status < 1:
|
||||||
|
lines.append(name + ' (' + dev.status_text + ')')
|
||||||
|
else:
|
||||||
|
lines.append(name)
|
||||||
|
if dev.status > 1:
|
||||||
|
lines.append(' ' + dev.status_text)
|
||||||
|
lines.append('')
|
||||||
|
|
||||||
|
text = '\n'.join(lines).rstrip('\n')
|
||||||
|
icon.set_tooltip_markup(text)
|
||||||
|
else:
|
||||||
|
icon.set_tooltip_text(receiver.status_text)
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import time
|
import time
|
||||||
import logging
|
from logging import getLogger as _Logger
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from logitech.devices.constants import STATUS
|
from logitech.devices.constants import STATUS
|
||||||
|
@ -16,12 +16,14 @@ _DUMMY_RECEIVER.__nonzero__ = lambda _: False
|
||||||
_DUMMY_RECEIVER.device_name = Receiver.NAME
|
_DUMMY_RECEIVER.device_name = Receiver.NAME
|
||||||
DUMMY = _DUMMY_RECEIVER(Receiver.NAME, Receiver.NAME, STATUS.UNAVAILABLE, 'Receiver not found.', Receiver.max_devices, {})
|
DUMMY = _DUMMY_RECEIVER(Receiver.NAME, Receiver.NAME, STATUS.UNAVAILABLE, 'Receiver not found.', Receiver.max_devices, {})
|
||||||
|
|
||||||
|
_l = _Logger('watcher')
|
||||||
|
|
||||||
|
|
||||||
def _sleep(seconds, granularity, breakout=lambda: False):
|
def _sleep(seconds, granularity, breakout=lambda: False):
|
||||||
for index in range(0, int(seconds / granularity)):
|
slept = 0
|
||||||
if breakout():
|
while slept < seconds and not breakout():
|
||||||
return
|
|
||||||
time.sleep(granularity)
|
time.sleep(granularity)
|
||||||
|
slept += granularity
|
||||||
|
|
||||||
|
|
||||||
class Watcher(Thread):
|
class Watcher(Thread):
|
||||||
|
@ -30,77 +32,77 @@ class Watcher(Thread):
|
||||||
"""
|
"""
|
||||||
def __init__(self, apptitle, update_ui, notify=None):
|
def __init__(self, apptitle, update_ui, notify=None):
|
||||||
super(Watcher, self).__init__(group=apptitle, name='Watcher')
|
super(Watcher, self).__init__(group=apptitle, name='Watcher')
|
||||||
self.daemon = True
|
|
||||||
self._active = False
|
self._active = False
|
||||||
|
self._receiver = DUMMY
|
||||||
|
|
||||||
self.update_ui = update_ui
|
self.update_ui = update_ui
|
||||||
self.notify = notify or (lambda d: None)
|
self.notify = notify or (lambda d: None)
|
||||||
|
|
||||||
self.receiver = DUMMY
|
@property
|
||||||
|
def receiver(self):
|
||||||
|
return self._receiver
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self._active = True
|
self._active = True
|
||||||
notify_missing = True
|
notify_missing = True
|
||||||
|
|
||||||
while self._active:
|
while self._active:
|
||||||
if self.receiver == DUMMY:
|
if self._receiver == DUMMY:
|
||||||
r = Receiver.open()
|
r = Receiver.open()
|
||||||
if r:
|
if r is None:
|
||||||
logging.info("receiver %s ", r)
|
|
||||||
self.update_ui(r)
|
|
||||||
self.notify(r)
|
|
||||||
r.events_handler = self._events_callback
|
|
||||||
|
|
||||||
# give it some time to read all devices
|
|
||||||
r.status_changed.clear()
|
|
||||||
_sleep(8, 0.4, r.status_changed.is_set)
|
|
||||||
if r.devices:
|
|
||||||
logging.info("%d device(s) found", len(r.devices))
|
|
||||||
for d in r.devices.values():
|
|
||||||
self.notify(d)
|
|
||||||
else:
|
|
||||||
# if no devices found so far, assume none at all
|
|
||||||
logging.info("no devices found")
|
|
||||||
r.status = STATUS.CONNECTED
|
|
||||||
|
|
||||||
self.receiver = r
|
|
||||||
notify_missing = True
|
|
||||||
else:
|
|
||||||
if notify_missing:
|
if notify_missing:
|
||||||
_sleep(0.8, 0.4, lambda: not self._active)
|
_sleep(0.8, 0.4, lambda: not self._active)
|
||||||
notify_missing = False
|
notify_missing = False
|
||||||
self.update_ui(DUMMY)
|
if self._active:
|
||||||
self.notify(DUMMY)
|
self.update_ui(DUMMY)
|
||||||
|
self.notify(DUMMY)
|
||||||
_sleep(4, 0.4, lambda: not self._active)
|
_sleep(4, 0.4, lambda: not self._active)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
_l.info("receiver %s ", r)
|
||||||
|
self.update_ui(r)
|
||||||
|
self.notify(r)
|
||||||
|
|
||||||
|
if r.count_devices() > 0:
|
||||||
|
# give it some time to read all devices
|
||||||
|
r.status_changed.clear()
|
||||||
|
_sleep(8, 0.4, r.status_changed.is_set)
|
||||||
|
|
||||||
|
if r.devices:
|
||||||
|
_l.info("%d device(s) found", len(r.devices))
|
||||||
|
for d in r.devices.values():
|
||||||
|
self.notify(d)
|
||||||
|
else:
|
||||||
|
# if no devices found so far, assume none at all
|
||||||
|
_l.info("no devices found")
|
||||||
|
r.status = STATUS.CONNECTED
|
||||||
|
|
||||||
|
self._receiver = r
|
||||||
|
notify_missing = True
|
||||||
|
|
||||||
if self._active:
|
if self._active:
|
||||||
if self.receiver:
|
if self._receiver:
|
||||||
logging.debug("waiting for status_changed")
|
_l.debug("waiting for status_changed")
|
||||||
sc = self.receiver.status_changed
|
sc = self._receiver.status_changed
|
||||||
sc.wait()
|
sc.wait()
|
||||||
sc.clear()
|
sc.clear()
|
||||||
logging.debug("status_changed %s %d", sc.reason, sc.urgent)
|
_l.debug("status_changed %s %d", sc.reason, sc.urgent)
|
||||||
self.update_ui(self.receiver)
|
self.update_ui(self._receiver)
|
||||||
if sc.reason and sc.urgent:
|
if sc.reason and sc.urgent:
|
||||||
self.notify(sc.reason)
|
self.notify(sc.reason)
|
||||||
else:
|
else:
|
||||||
self.receiver = DUMMY
|
self._receiver = DUMMY
|
||||||
self.update_ui(DUMMY)
|
self.update_ui(DUMMY)
|
||||||
self.notify(DUMMY)
|
self.notify(DUMMY)
|
||||||
|
|
||||||
if self.receiver:
|
if self._receiver:
|
||||||
self.receiver.close()
|
self._receiver.close()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if self._active:
|
if self._active:
|
||||||
logging.info("stopping %s", self)
|
_l.info("stopping %s", self)
|
||||||
self._active = False
|
self._active = False
|
||||||
if self.receiver:
|
if self._receiver:
|
||||||
# break out of an eventual wait()
|
# break out of an eventual wait()
|
||||||
self.receiver.status_changed.reason = None
|
self._receiver.status_changed.reason = None
|
||||||
self.receiver.status_changed.set()
|
self._receiver.status_changed.set()
|
||||||
self.join()
|
|
||||||
|
|
||||||
def _events_callback(self, event):
|
|
||||||
logging.warn("don't know how to handle event %s", event)
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ def _module(device_name):
|
||||||
def default_request_status(devinfo, listener=None):
|
def default_request_status(devinfo, listener=None):
|
||||||
if FEATURE.BATTERY in devinfo.features:
|
if FEATURE.BATTERY in devinfo.features:
|
||||||
if listener:
|
if listener:
|
||||||
reply = listener.request(_api.get_device_battery_level, devinfo.number, features=devinfo.features)
|
reply = listener.call_api(_api.get_device_battery_level, devinfo.number, features=devinfo.features)
|
||||||
else:
|
else:
|
||||||
reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
|
reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ def default_request_status(devinfo, listener=None):
|
||||||
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
|
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
|
||||||
|
|
||||||
if listener:
|
if listener:
|
||||||
reply = listener.request(_api.ping, devinfo.number)
|
reply = listener.call_api(_api.ping, devinfo.number)
|
||||||
else:
|
else:
|
||||||
reply = _api.ping(devinfo.handle, devinfo.number)
|
reply = _api.ping(devinfo.handle, devinfo.number)
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ def request_status(devinfo, listener=None):
|
||||||
if listener is None:
|
if listener is None:
|
||||||
reply = _trigger_solar_charge_events(devinfo.handle, devinfo)
|
reply = _trigger_solar_charge_events(devinfo.handle, devinfo)
|
||||||
elif listener:
|
elif listener:
|
||||||
reply = listener.request(_trigger_solar_charge_events, devinfo)
|
reply = listener.call_api(_trigger_solar_charge_events, devinfo)
|
||||||
else:
|
else:
|
||||||
reply = 0
|
reply = 0
|
||||||
|
|
||||||
|
|
|
@ -38,22 +38,22 @@ close = _base.close
|
||||||
|
|
||||||
def get_receiver_info(handle):
|
def get_receiver_info(handle):
|
||||||
serial = None
|
serial = None
|
||||||
reply = _base.request(handle, 0xff, b'\x83\xB5', b'\x03')
|
reply = _base.request(handle, 0xFF, b'\x83\xB5', b'\x03')
|
||||||
if reply and reply[0:1] == b'\x03':
|
if reply and reply[0:1] == b'\x03':
|
||||||
serial = _hexlify(reply[1:5])
|
serial = _hexlify(reply[1:5])
|
||||||
|
|
||||||
firmware = '??.??'
|
firmware = '??.??'
|
||||||
reply = _base.request(handle, 0xff, b'\x81\xF1', b'\x01')
|
reply = _base.request(handle, 0xFF, b'\x81\xF1', b'\x01')
|
||||||
if reply and reply[0:1] == b'\x01':
|
if reply and reply[0:1] == b'\x01':
|
||||||
fw_version = _hexlify(reply[1:3])
|
fw_version = _hexlify(reply[1:3])
|
||||||
firmware = fw_version[0:2] + '.' + fw_version[2:4]
|
firmware = fw_version[0:2] + '.' + fw_version[2:4]
|
||||||
|
|
||||||
reply = _base.request(handle, 0xff, b'\x81\xF1', b'\x02')
|
reply = _base.request(handle, 0xFF, b'\x81\xF1', b'\x02')
|
||||||
if reply and reply[0:1] == b'\x02':
|
if reply and reply[0:1] == b'\x02':
|
||||||
firmware += '.B' + _hexlify(reply[1:3])
|
firmware += '.B' + _hexlify(reply[1:3])
|
||||||
|
|
||||||
bootloader = None
|
bootloader = None
|
||||||
reply = _base.request(handle, 0xff, b'\x81\xF1', b'\x04')
|
reply = _base.request(handle, 0xFF, b'\x81\xF1', b'\x04')
|
||||||
if reply and reply[0:1] == b'\x04':
|
if reply and reply[0:1] == b'\x04':
|
||||||
bl_version = _hexlify(reply[1:3])
|
bl_version = _hexlify(reply[1:3])
|
||||||
bootloader = bl_version[0:2] + '.' + bl_version[2:4]
|
bootloader = bl_version[0:2] + '.' + bl_version[2:4]
|
||||||
|
@ -61,6 +61,11 @@ def get_receiver_info(handle):
|
||||||
return (serial, firmware, bootloader)
|
return (serial, firmware, bootloader)
|
||||||
|
|
||||||
|
|
||||||
|
def count_devices(handle):
|
||||||
|
count = _base.request(handle, 0xFF, b'\x80\x02', b'\x02')
|
||||||
|
return 0 if count is None else ord(count[1:2])
|
||||||
|
|
||||||
|
|
||||||
def request(handle, devnumber, feature, function=b'\x00', params=b'', features=None):
|
def request(handle, devnumber, feature, function=b'\x00', params=b'', features=None):
|
||||||
"""Makes a feature call to the device, and returns the reply data.
|
"""Makes a feature call to the device, and returns the reply data.
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ class EventsListener(Thread):
|
||||||
Incoming packets will be passed to the callback function in sequence, by a
|
Incoming packets will be passed to the callback function in sequence, by a
|
||||||
separate thread.
|
separate thread.
|
||||||
|
|
||||||
While this listener is running, you must use the request() method to make
|
While this listener is running, you must use the call_api() method to make
|
||||||
regular UR API calls; otherwise the expected API replies are most likely to
|
regular UR API calls; otherwise the expected API replies are most likely to
|
||||||
be captured by the listener and delivered to the callback.
|
be captured by the listener and delivered to the callback.
|
||||||
"""
|
"""
|
||||||
|
@ -107,6 +107,7 @@ class EventsListener(Thread):
|
||||||
self._task_done.set()
|
self._task_done.set()
|
||||||
|
|
||||||
_base.close(self._handle)
|
_base.close(self._handle)
|
||||||
|
self._handle = 0
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Tells the listener to stop as soon as possible."""
|
"""Tells the listener to stop as soon as possible."""
|
||||||
|
@ -120,7 +121,10 @@ class EventsListener(Thread):
|
||||||
def handle(self):
|
def handle(self):
|
||||||
return self._handle
|
return self._handle
|
||||||
|
|
||||||
def request(self, api_function, *args, **kwargs):
|
def request(self, device, feature_function_index, params=b''):
|
||||||
|
return self.call_api(_base.request, device, feature_function_index, params)
|
||||||
|
|
||||||
|
def call_api(self, api_function, *args, **kwargs):
|
||||||
"""Make an UR API request through this listener's receiver.
|
"""Make an UR API request through this listener's receiver.
|
||||||
|
|
||||||
The api_function must have a receiver handle as a first agument, all
|
The api_function must have a receiver handle as a first agument, all
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.4 KiB |
Loading…
Reference in New Issue