aboutsummaryrefslogtreecommitdiff
path: root/parrot_zik
diff options
context:
space:
mode:
authorMarek Siarkowicz <mareksiarkowicz@gmail.com>2015-06-15 11:33:33 +0200
committerMarek Siarkowicz <mareksiarkowicz@gmail.com>2015-06-15 12:01:43 +0200
commit947a9f7d10bc3c54939cfa72dab27b2e64034318 (patch)
tree052108b258f5179c457b762e2134bf9fff1dccfe /parrot_zik
parent55255db75378f39807371bb7e434cc943fd66f27 (diff)
downloadpyParrotZikTCP-947a9f7d10bc3c54939cfa72dab27b2e64034318.tar.xz
pyParrotZikTCP-947a9f7d10bc3c54939cfa72dab27b2e64034318.zip
Create packet.
Diffstat (limited to 'parrot_zik')
-rw-r--r--parrot_zik/__init__.py0
-rw-r--r--parrot_zik/bluetooth_paired_devices.py107
-rw-r--r--parrot_zik/indicator.py254
-rw-r--r--parrot_zik/message.py30
-rw-r--r--parrot_zik/parrot_zik_model.py222
-rwxr-xr-xparrot_zik/parrot_zik_tray472
-rw-r--r--parrot_zik/resource_manager.py176
-rw-r--r--parrot_zik/status_app_mac.py25
8 files changed, 1286 insertions, 0 deletions
diff --git a/parrot_zik/__init__.py b/parrot_zik/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/parrot_zik/__init__.py
diff --git a/parrot_zik/bluetooth_paired_devices.py b/parrot_zik/bluetooth_paired_devices.py
new file mode 100644
index 0000000..eb1ec09
--- /dev/null
+++ b/parrot_zik/bluetooth_paired_devices.py
@@ -0,0 +1,107 @@
+import sys
+import re
+import os
+
+from parrot_zik.resource_manager import GenericResourceManager
+
+if sys.platform == "darwin":
+ from binplist import binplist
+ import lightblue
+else:
+ import bluetooth
+ if sys.platform == "win32":
+ import _winreg
+
+
+def get_parrot_zik_mac():
+ p = re.compile('90:03:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}|'
+ 'A0:14:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}')
+ if sys.platform == "linux2":
+ bluetooth_on = int(os.popen('bluez-test-adapter powered').read())
+ if bluetooth_on == 1:
+ out = os.popen("bluez-test-device list").read()
+ res = p.findall(out)
+ if len(res) > 0:
+ return res[0]
+ else:
+ raise DeviceNotConnected
+ else:
+ raise BluetoothIsNotOn
+ elif sys.platform == "darwin":
+ fd = open("/Library/Preferences/com.apple.Bluetooth.plist", "rb")
+ plist = binplist.BinaryPlist(file_obj=fd)
+ parsed_plist = plist.Parse()
+ try:
+ for mac in parsed_plist['PairedDevices']:
+ if p.match(mac.replace("-", ":")):
+ return mac.replace("-", ":")
+ else:
+ raise DeviceNotConnected
+ except Exception:
+ pass
+
+ elif sys.platform == "win32":
+ aReg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
+ aKey = _winreg.OpenKey(
+ aReg, 'SYSTEM\CurrentControlSet\Services\
+ BTHPORT\Parameters\Devices')
+ for i in range(10):
+ try:
+ asubkey_name = _winreg.EnumKey(aKey, i)
+ mac = ':'.join(asubkey_name[i:i+2] for i in range(0, 12, 2))
+ res = p.findall(mac)
+ if len(res) > 0:
+ return res[0]
+ else:
+ raise DeviceNotConnected
+ except EnvironmentError:
+ pass
+
+
+def connect():
+ mac = get_parrot_zik_mac()
+ if sys.platform == "darwin":
+ service_matches = lightblue.findservices(
+ name="Parrot RFcomm service", addr=mac)
+ else:
+ uuids = ["0ef0f502-f0ee-46c9-986c-54ed027807fb",
+ "8B6814D3-6CE7-4498-9700-9312C1711F63"]
+ service_matches = []
+ for uuid in uuids:
+ try:
+ service_matches = bluetooth.find_service(uuid=uuid, address=mac)
+ except bluetooth.btcommon.BluetoothError:
+ pass
+ if service_matches:
+ break
+
+ if len(service_matches) == 0:
+ raise ConnectionFailure
+ first_match = service_matches[0]
+
+ if sys.platform == "darwin":
+ host = first_match[0]
+ port = first_match[1]
+ sock = lightblue.socket()
+ else:
+ port = first_match["port"]
+ host = first_match["host"]
+ sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
+
+ sock.connect((host, port))
+
+ sock.send('\x00\x03\x00')
+ sock.recv(1024)
+ return GenericResourceManager(sock)
+
+
+class DeviceNotConnected(Exception):
+ pass
+
+
+class ConnectionFailure(Exception):
+ pass
+
+
+class BluetoothIsNotOn(Exception):
+ pass
diff --git a/parrot_zik/indicator.py b/parrot_zik/indicator.py
new file mode 100644
index 0000000..2691113
--- /dev/null
+++ b/parrot_zik/indicator.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python
+
+import sys
+import os
+import tempfile
+
+if sys.platform == "linux2" or sys.platform == "win32":
+ import gtk
+elif sys.platform == "darwin":
+ from Foundation import *
+ from AppKit import *
+ from PyObjCTools import AppHelper
+ from status_app_mac import StatusApp
+
+
+class BaseIndicator(object):
+ def __init__(self, icon, menu, statusicon):
+ self.menu = menu
+ self.statusicon = statusicon
+ self.setIcon(icon)
+
+ def gtk_right_click_event(self, icon, button, time):
+ if not self.menu_shown:
+ self.menu_shown = True
+ self.menu.popup(None, None, gtk.status_icon_position_menu,
+ button, time, self.statusicon)
+ else:
+ self.menu_shown = False
+ self.menu.popdown()
+
+ def setIcon(self, name):
+ raise NotImplementedError
+
+ def main(self):
+ raise NotImplementedError
+
+ def show_about_dialog(self, widget):
+ raise NotImplementedError
+
+
+class WindowsIndicator(BaseIndicator):
+ def __init__(self, icon, menu):
+ self.icon_directory = (
+ os.path.dirname(os.path.realpath(sys.argv[0])) + os.path.sep + '..'
+ + os.path.sep + 'share' + os.path.sep + 'icons'
+ + os.path.sep +'zik' + os.path.sep)
+ self.menu_shown = False
+ sys.stdout = open(tempfile.gettempdir()
+ + os.path.sep + "zik_tray_stdout.log", "w")
+ sys.stderr = open(tempfile.gettempdir()
+ + os.path.sep + "zik_tray_stderr.log", "w")
+ statusicon = gtk.StatusIcon()
+ statusicon.connect("popup-menu", self.gtk_right_click_event)
+ statusicon.set_tooltip("Parrot Zik")
+ super(WindowsIndicator, self).__init__(icon, menu, statusicon)
+
+ def setIcon(self, name):
+ self.statusicon.set_from_file(self.icon_directory + name + '.png')
+
+ def main(self):
+ gtk.main()
+
+ def show_about_dialog(self, widget):
+ about_dialog = gtk.AboutDialog()
+ about_dialog.set_destroy_with_parent(True)
+ about_dialog.set_name("Parrot Zik Tray")
+ about_dialog.set_version("0.3")
+ about_dialog.set_authors(["Dmitry Moiseev m0sia@m0sia.ru"])
+ about_dialog.run()
+ about_dialog.destroy()
+
+
+class LinuxIndicator(BaseIndicator):
+ def __init__(self, icon, menu):
+ import appindicator
+ self.icon_directory = (os.path.sep + 'usr' + os.path.sep + 'share'
+ + os.path.sep + 'icons' + os.path.sep+'zik'
+ + os.path.sep)
+ if not os.path.isdir(self.icon_directory):
+ self.icon_directory = (os.path.dirname(sys.argv[0]) + os.path.sep + '..'
+ + os.path.sep + 'share' + os.path.sep
+ + 'icons' + os.path.sep+'zik'
+ + os.path.sep)
+ statusicon = appindicator.Indicator(
+ "new-parrotzik-indicator", "indicator-messages",
+ appindicator.CATEGORY_APPLICATION_STATUS)
+ statusicon.set_status(appindicator.STATUS_ACTIVE)
+ statusicon.set_icon_theme_path(self.icon_directory)
+ statusicon.set_menu(menu.gtk_menu)
+ super(LinuxIndicator, self).__init__(icon, menu, statusicon)
+
+ def setIcon(self, name):
+ self.statusicon.set_icon(name)
+
+ def main(self):
+ gtk.main()
+
+ def show_about_dialog(self, widget):
+ about_dialog = gtk.AboutDialog()
+ about_dialog.set_destroy_with_parent(True)
+ about_dialog.set_name("Parrot Zik Tray")
+ about_dialog.set_version("0.3")
+ about_dialog.set_authors(["Dmitry Moiseev m0sia@m0sia.ru"])
+ about_dialog.run()
+ about_dialog.destroy()
+
+
+class DarwinIndicator(BaseIndicator):
+ def __init__(self, icon, menu):
+ self.icon_directory = (
+ os.path.dirname(os.path.realpath(sys.argv[0])) + os.path.sep + '..' + os.path.sep
+ + 'share' + os.path.sep + 'icons' + os.path.sep + 'zik'
+ + os.path.sep)
+ statusicon = StatusApp.sharedApplication()
+ statusicon.initMenu(menu)
+ super(DarwinIndicator, self).__init__(icon, menu, statusicon)
+
+ def setIcon(self, name):
+ self.statusicon.setIcon(name, self.icon_directory)
+
+ def main(self):
+ AppHelper.runEventLoop()
+
+ def show_about_dialog(self, widget):
+ pass
+
+
+class NSMenu(object):
+ def __init__(self):
+ self.actions = {}
+ self.menubarMenu = NSMenu.alloc().init()
+ self.menubarMenu.setAutoenablesItems_(False)
+
+ def append(self, menu_item):
+ self.actions[menu_item.title] = menu_item.action
+ self.menubarMenu.addItem_(menu_item.nsmenu_item)
+
+ def reposition(self):
+ # TODO
+ pass
+
+class GTKMenu(object):
+ def __init__(self):
+ self.gtk_menu = gtk.Menu()
+
+ def append(self, menu_item):
+ self.gtk_menu.append(menu_item.base_item)
+
+ def reposition(self):
+ self.gtk_menu.reposition()
+
+
+class MenuItemBase(object):
+ def __init__(self, base_item, sensitive, visible):
+ self.base_item = base_item
+ self.set_sensitive(sensitive)
+ if visible:
+ self.show()
+ else:
+ self.hide()
+
+ def set_sensitive(self, option):
+ raise NotImplementedError
+
+ def set_active(self, option):
+ raise NotImplementedError
+
+ def get_active(self):
+ raise NotImplementedError
+
+ def set_label(self, option):
+ raise NotImplementedError
+
+ def show(self):
+ self.base_item.show()
+
+ def hide(self):
+ self.base_item.hide()
+
+ def set_submenu(self, menu):
+ raise NotImplementedError
+
+class GTKMenuItem(MenuItemBase):
+ def __init__(self, name, action, sensitive=True, checkitem=False, visible=True):
+ if checkitem:
+ gtk_item = gtk.CheckMenuItem(name)
+ else:
+ gtk_item = gtk.MenuItem(name)
+ if action:
+ gtk_item.connect("activate", action)
+ super(GTKMenuItem, self).__init__(gtk_item, sensitive, visible)
+
+ def set_sensitive(self, option):
+ return self.base_item.set_sensitive(option)
+
+ def set_active(self, option):
+ return self.base_item.set_active(option)
+
+ def get_active(self):
+ return self.base_item.get_active()
+
+ def set_label(self, option):
+ return self.base_item.set_label(option)
+
+ def set_submenu(self, menu):
+ self.base_item.set_submenu(menu.gtk_menu)
+
+
+class NSMenuItem(MenuItemBase):
+ def __init__(self, name, action, sensitive=True, checkitem=False, visible=True):
+ self.title = name
+ self.action = action
+ nsmenu_item = (
+ NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
+ name, 'clicked:', ''))
+ super(NSMenuItem, self).__init__(nsmenu_item, sensitive, visible)
+
+ def set_sensitive(self, option):
+ self.base_item.setEnabled_(option)
+
+ def set_active(self, option):
+ self.base_item.setState_(option)
+
+ def get_active(self):
+ return self.base_item.state
+
+ def set_label(self, option):
+ self.title = option
+ self.base_item.setTitle_(option)
+
+if sys.platform == 'linux2':
+ SysIndicator = LinuxIndicator
+ Menu = GTKMenu
+ MenuItem = GTKMenuItem
+elif sys.platform == 'win32':
+ SysIndicator = WindowsIndicator
+ Menu = GTKMenu
+ MenuItem = GTKMenuItem
+elif sys.platform == 'darwin':
+ SysIndicator = DarwinIndicator
+ Menu = NSMenu
+ MenuItem = NSMenuItem
+else:
+ raise Exception('Platform not supported')
+
+if __name__ == "__main__":
+
+ quit_item = MenuItem("Quit", sys.exit, True)
+
+ menu = Menu()
+ menu.append(quit_item)
+
+ indicator = SysIndicator(icon="zik-audio-headset", menu=menu)
+ indicator.main()
diff --git a/parrot_zik/message.py b/parrot_zik/message.py
new file mode 100644
index 0000000..214b6f9
--- /dev/null
+++ b/parrot_zik/message.py
@@ -0,0 +1,30 @@
+class Message:
+ def __init__(self, resource, method, arg=None):
+ self.method = method
+ self.resource = resource
+ self.arg = arg
+
+ def __str__(self):
+ return str(self.request)
+
+ @property
+ def request(self):
+ message = bytearray()
+ message.extend(self.header)
+ message.extend(bytearray(self.request_string))
+ return message
+
+ @property
+ def header(self):
+ header = bytearray([0])
+ header.append(len(self.request_string) + 3)
+ header.append("\x80")
+ return header
+
+ @property
+ def request_string(self):
+ if self.method == 'set':
+ return 'SET {}/{}?arg={}'.format(self.resource, self.method,
+ str(self.arg).lower())
+ else:
+ return 'GET {}/{}'.format(self.resource, self.method)
diff --git a/parrot_zik/parrot_zik_model.py b/parrot_zik/parrot_zik_model.py
new file mode 100644
index 0000000..e15200f
--- /dev/null
+++ b/parrot_zik/parrot_zik_model.py
@@ -0,0 +1,222 @@
+from parrot_zik.resource_manager import Version1ResourceManager
+from parrot_zik.resource_manager import Version2ResourceManager
+
+
+class BatteryStates:
+ CHARGED = 'charged'
+ IN_USE = 'in_use'
+ CHARGING = 'charging'
+ representation = {
+ CHARGED: 'Charged',
+ IN_USE: 'In Use',
+ CHARGING: 'Charging',
+ }
+
+class Rooms:
+ CONCERT_HALL = 'concert'
+ JAZZ_CLUB = 'jazz'
+ LIVING_ROOM = 'living'
+ SILENT_ROOM = 'silent'
+ representation = {
+ CONCERT_HALL: 'Concert Hall',
+ JAZZ_CLUB: 'Jazz Club',
+ LIVING_ROOM: 'Living Room',
+ SILENT_ROOM: 'Silent Room',
+ }
+
+class NoiseControl(object):
+ def __init__(self, type, value):
+ self.type = type
+ self.value = value
+
+ @classmethod
+ def from_noise_control(cls, noise_control):
+ return cls(noise_control['type'], int(noise_control['value']))
+
+ def __eq__(self, other):
+ return self.type == other.type and self.value == other.value
+
+ def __str__(self):
+ return '{}++{}'.format(self.type, self.value)
+
+class NoiseControlTypes:
+ NOISE_CONTROL_MAX = NoiseControl('anc', 2)
+ NOISE_CONTROL_ON = NoiseControl('anc', 1)
+ NOISE_CONTROL_OFF = NoiseControl('off', 1)
+ STREET_MODE = NoiseControl('aoc', 1)
+ STREET_MODE_MAX = NoiseControl('aoc', 2)
+
+
+class ParrotZikBase(object):
+ def __init__(self, resource_manager):
+ self.resource_manager = resource_manager
+
+ @property
+ def version(self):
+ return self.resource_manager.api_version
+
+ def refresh_battery(self):
+ self.resource_manager.fetch('/api/system/battery')
+
+ @property
+ def battery_state(self):
+ answer = self.resource_manager.get("/api/system/battery")
+ return answer.system.battery["state"]
+
+ def get_battery_level(self, field_name):
+ answer = self.resource_manager.get("/api/system/battery")
+ return int(answer.system.battery[field_name])
+
+ @property
+ def friendly_name(self):
+ answer = self.resource_manager.get("/api/bluetooth/friendlyname")
+ return answer.bluetooth["friendlyname"]
+
+ @property
+ def auto_connect(self):
+ answer = self.resource_manager.get("/api/system/auto_connection/enabled")
+ return self._result_to_bool(
+ answer.system.auto_connection["enabled"])
+
+ @auto_connect.setter
+ def auto_connect(self, arg):
+ self.resource_manager.set("/api/system/auto_connection/enabled", arg)
+
+ @property
+ def anc_phone_mode(self):
+ answer = self.resource_manager.get("/api/system/anc_phone_mode/enabled")
+ return self._result_to_bool(
+ answer.system.anc_phone_mode["enabled"])
+
+ def _result_to_bool(self, result):
+ if result == "true":
+ return True
+ elif result == "false":
+ return False
+ else:
+ raise AssertionError(result)
+
+
+class ParrotZikVersion1(ParrotZikBase):
+ def __init__(self, resource_manager):
+ super(ParrotZikVersion1, self).__init__(
+ resource_manager.get_resource_manager(
+ Version1ResourceManager))
+
+ @property
+ def version(self):
+ answer = self.resource_manager.get('/api/software/version')
+ return answer.software['version']
+
+ @property
+ def battery_level(self):
+ return int(self.get_battery_level('level'))
+
+ @property
+ def lou_reed_mode(self):
+ answer = self.resource_manager.get("/api/audio/specific_mode/enabled")
+ return self._result_to_bool(
+ answer.audio.specific_mode["enabled"])
+
+ @lou_reed_mode.setter
+ def lou_reed_mode(self, arg):
+ self.resource_manager.get("/api/audio/specific_mode/enabled", arg)
+
+ @property
+ def concert_hall(self):
+ answer = self.resource_manager.get("/api/audio/sound_effect/enabled")
+ return self._result_to_bool(
+ answer.audio.sound_effect["enabled"])
+
+ @concert_hall.setter
+ def concert_hall(self, arg):
+ self.resource_manager.get("/api/audio/sound_effect/enabled", arg)
+
+ @property
+ def cancel_noise(self):
+ answer = self.resource_manager.get("/api/audio/noise_cancellation/enabled")
+ return self._result_to_bool(
+ answer.audio.noise_cancellation["enabled"])
+
+ @cancel_noise.setter
+ def cancel_noise(self, arg):
+ self.resource_manager.set("/api/audio/noise_cancellation/enabled", arg)
+
+
+class ParrotZikVersion2(ParrotZikBase):
+ def __init__(self, resource_manager):
+ super(ParrotZikVersion2, self).__init__(
+ resource_manager.get_resource_manager(
+ Version2ResourceManager))
+
+ @property
+ def version(self):
+ answer = self.resource_manager.get('/api/software/version')
+ return answer.software['sip6']
+
+ @property
+ def battery_level(self):
+ return self.get_battery_level('percent')
+
+ @property
+ def flight_mode(self):
+ answer = self.resource_manager.get('/api/flight_mode')
+ return self._result_to_bool(answer.flight_mode['enabled'])
+
+ @flight_mode.setter
+ def flight_mode(self, arg):
+ if arg:
+ self.resource_manager.toggle_on('/api/flight_mode')
+ else:
+ self.resource_manager.toggle_off('/api/flight_mode')
+
+ @property
+ def sound_effect(self):
+ answer = self.resource_manager.get('/api/audio/sound_effect/enabled')
+ return self._result_to_bool(answer.audio.sound_effect['enabled'])
+
+ @sound_effect.setter
+ def sound_effect(self, arg):
+ self.resource_manager.set('/api/audio/sound_effect/enabled', arg)
+
+ @property
+ def room(self):
+ answer = self.resource_manager.get('/api/audio/sound_effect/room_size')
+ return answer.audio.sound_effect['room_size']
+
+ @room.setter
+ def room(self, arg):
+ self.resource_manager.set('/api/audio/sound_effect/room_size', arg)
+
+ @property
+ def external_noise(self):
+ answer = self.resource_manager.get('/api/audio/noise')
+ return int(answer.audio.noise['external'])
+
+ @property
+ def internal_noise(self):
+ answer = self.resource_manager.get('/api/audio/noise')
+ return int(answer.audio.noise['internal'])
+
+ @property
+ def angle(self):
+ answer = self.resource_manager.get('/api/audio/sound_effect/angle')
+ return int(answer.audio.sound_effect['angle'])
+
+ @angle.setter
+ def angle(self, arg):
+ self.resource_manager.set('/api/audio/sound_effect/angle', arg)
+
+ @property
+ def noise_control(self):
+ answer = self.resource_manager.get('/api/audio/noise_control')
+ return NoiseControl.from_noise_control(answer.audio.noise_control)
+
+ @noise_control.setter
+ def noise_control(self, arg):
+ pass
+
+ @property
+ def noise_control_enabled(self):
+ answer = self.resource_manager.get('/api/audio/noise_control/enabled')
+ return self._result_to_bool(answer.audio.noise_control['enabled'])
diff --git a/parrot_zik/parrot_zik_tray b/parrot_zik/parrot_zik_tray
new file mode 100755
index 0000000..dcdacd0
--- /dev/null
+++ b/parrot_zik/parrot_zik_tray
@@ -0,0 +1,472 @@
+#!/usr/bin/env python
+import functools
+from threading import Lock
+import gtk
+
+from parrot_zik import resource_manager
+from parrot_zik import bluetooth_paired_devices
+from parrot_zik.parrot_zik_model import BatteryStates
+from parrot_zik.parrot_zik_model import ParrotZikVersion1
+from parrot_zik.parrot_zik_model import ParrotZikVersion2
+from parrot_zik.parrot_zik_model import NoiseControlTypes
+from parrot_zik.parrot_zik_model import Rooms
+from parrot_zik.indicator import MenuItem
+from parrot_zik.indicator import Menu
+from parrot_zik.indicator import SysIndicator
+
+REFRESH_FREQUENCY = 30000
+RECONNECT_FREQUENCY = 5000
+
+
+class repeat(object):
+ def __init__(self, f):
+ self.f = f
+ self.id = None
+ self.lock = Lock()
+
+ def __call__(self, cls):
+ self.f(cls)
+
+ def start(self, cls, frequency):
+ self.lock.acquire()
+ if not self.id:
+ def run():
+ self.f(cls)
+ return True
+
+ self.id = gtk.timeout_add(frequency, run)
+ self.lock.release()
+
+ def stop(self):
+ self.lock.acquire()
+ if self.id:
+ gtk.timeout_remove(self.id)
+ self.id = None
+ self.lock.release()
+
+
+class ParrotZikIndicator(SysIndicator):
+ def __init__(self):
+
+ self.menu = Menu()
+
+ self.info_item = MenuItem("Parrot Zik Not connected",
+ None, sensitive=False)
+ self.menu.append(self.info_item)
+
+ self.version_1_interface = ParrotZikVersion1Interface(self)
+ self.version_2_interface = ParrotZikVersion2Interface(self)
+ self.quit = MenuItem("Quit", gtk.main_quit, checkitem=True)
+ self.menu.append(self.quit)
+
+ SysIndicator.__init__(self, icon="zik-audio-headset", menu=self.menu)
+
+ self.active_interface = None
+
+ @repeat
+ def reconnect(self):
+ if self.active_interface:
+ self.reconnect.stop()
+ else:
+ self.info("Trying to connect")
+ try:
+ manager = bluetooth_paired_devices.connect()
+ except bluetooth_paired_devices.BluetoothIsNotOn:
+ self.info("Bluetooth is turned off")
+ except bluetooth_paired_devices.DeviceNotConnected:
+ self.info("Parrot Zik Not connected")
+ except bluetooth_paired_devices.ConnectionFailure:
+ self.info("Failed to connect")
+ else:
+ if manager.api_version.startswith('1'):
+ interface = self.version_1_interface
+ else:
+ interface = self.version_2_interface
+ try:
+ interface.activate(manager)
+ except resource_manager.DeviceDisconnected:
+ interface.deactivate()
+ else:
+ self.autorefresh(self)
+ self.autorefresh.start(self, REFRESH_FREQUENCY)
+ self.reconnect.stop()
+
+ def info(self, message):
+ self.info_item.set_label(message)
+ print(message)
+
+ @repeat
+ def autorefresh(self):
+ if self.active_interface:
+ self.active_interface.refresh()
+ else:
+ self.reconnect.start(self, RECONNECT_FREQUENCY)
+ self.autorefresh.stop()
+
+ def main(self):
+ self.reconnect.start(self, RECONNECT_FREQUENCY)
+ SysIndicator.main(self)
+
+class ParrotZikBaseInterface(object):
+ def __init__(self, indicator):
+ self.indicator = indicator
+ self.parrot = None
+ self.battery_level = MenuItem("Battery Level:", None, sensitive=False,
+ visible=False)
+ self.battery_state = MenuItem("Battery State:", None, sensitive=False,
+ visible=False)
+ self.firmware_version = MenuItem("Firmware Version:", None,
+ sensitive=False, visible=False)
+ self.auto_connection = MenuItem("Auto Connection", self.toggle_auto_connection,
+ checkitem=True, visible=False)
+ self.indicator.menu.append(self.battery_level)
+ self.indicator.menu.append(self.battery_state)
+ self.indicator.menu.append(self.firmware_version)
+ self.indicator.menu.append(self.auto_connection)
+
+ def activate(self, manager):
+ self.parrot = self.parrot_class(manager)
+ self.read_battery()
+ self.indicator.info("Connected to: " + self.parrot.friendly_name)
+ self.firmware_version.set_label(
+ "Firmware version: " + self.parrot.version)
+ self.auto_connection.set_active(self.parrot.auto_connect)
+ self.battery_level.show()
+ self.battery_state.show()
+ self.firmware_version.show()
+ self.auto_connection.show()
+ self.indicator.active_interface = self
+ self.indicator.menu.reposition()
+
+ @property
+ def parrot_class(self):
+ raise NotImplementedError
+
+ def deactivate(self):
+ self.parrot = None
+ self.battery_level.hide()
+ self.battery_state.hide()
+ self.firmware_version.hide()
+ self.auto_connection.hide()
+ self.indicator.menu.reposition()
+ self.indicator.active_interface = None
+ self.indicator.setIcon("zik-audio-headset")
+ self.indicator.info('Lost Connection')
+ self.indicator.reconnect.start(self.indicator, RECONNECT_FREQUENCY)
+
+ def toggle_auto_connection(self, widget):
+ try:
+ self.parrot.auto_connect = self.auto_connection.get_active()
+ self.auto_connection.set_active(self.parrot.auto_connect)
+ except resource_manager.DeviceDisconnected:
+ self.deactivate()
+
+ def refresh(self):
+ self.read_battery()
+
+ def read_battery(self):
+ try:
+ self.parrot.refresh_battery()
+ battery_level = self.parrot.battery_level
+ battery_state = self.parrot.battery_state
+ except resource_manager.DeviceDisconnected:
+ self.deactivate()
+ else:
+ if battery_state == BatteryStates.CHARGING:
+ self.indicator.setIcon("zik-battery-charging")
+ elif battery_level > 80:
+ self.indicator.setIcon("zik-battery-100")
+ elif battery_level > 60:
+ self.indicator.setIcon("zik-battery-080")
+ elif battery_level > 40:
+ self.indicator.setIcon("zik-battery-060")
+ elif battery_level > 20:
+ self.indicator.setIcon("zik-battery-040")
+ else:
+ self.indicator.setIcon("zik-battery-low")
+
+ self.battery_state.set_label(
+ "State: " + BatteryStates.representation[battery_state])
+ self.battery_level.set_label(
+ "Battery Level: " + str(battery_level))
+
+
+class ParrotZikVersion1Interface(ParrotZikBaseInterface):
+ parrot_class = ParrotZikVersion1
+
+ def __init__(self, indicator):
+ super(ParrotZikVersion1Interface, self).__init__(indicator)
+ self.noise_cancelation = MenuItem(
+ "Noise Cancellation", self.toggle_noise_cancelation,
+ checkitem=True, visible=False)
+ self.lou_reed_mode = MenuItem("Lou Reed Mode", self.toggle_lou_reed_mode,
+ checkitem=True, visible=False)
+ self.concert_hall_mode = MenuItem(
+ "Concert Hall Mode", self.toggle_parrot_concert_hall,
+ checkitem=True, visible=False)
+ self.indicator.menu.append(self.noise_cancelation)
+ self.indicator.menu.append(self.lou_reed_mode)
+ self.indicator.menu.append(self.concert_hall_mode)
+
+ def activate(self, manager):
+ self.noise_cancelation.show()
+ self.lou_reed_mode.show()
+ self.concert_hall_mode.show()
+ super(ParrotZikVersion1Interface, self).activate(manager)
+ self.noise_cancelation.set_active(self.parrot.cancel_noise)
+ self.lou_reed_mode.set_active(self.parrot.lou_reed_mode)
+ self.concert_hall_mode.set_active(self.parrot.concert_hall)
+
+ def deactivate(self):
+ self.noise_cancelation.hide()
+ self.lou_reed_mode.hide()
+ self.concert_hall_mode.hide()
+ super(ParrotZikVersion1Interface, self).deactivate()
+
+ def toggle_noise_cancelation(self, widget):
+ try:
+ self.parrot.cancel_noise = self.noise_cancelation.get_active()
+ self.noise_cancelation.set_active(self.parrot.cancel_noise)
+ except resource_manager.DeviceDisconnected:
+ self.deactivate()
+
+ def toggle_lou_reed_mode(self, widget):
+ try:
+ self.parrot.lou_reed_mode = self.lou_reed_mode.get_active()
+ self.lou_reed_mode.set_active(self.parrot.lou_reed_mode)
+ self.concert_hall_mode.set_active(self.parrot.concert_hall)
+ self.concert_hall_mode.set_sensitive(
+ not self.lou_reed_mode.get_active())
+ except resource_manager.DeviceDisconnected:
+ self.deactivate()
+
+ def toggle_parrot_concert_hall(self, widget):
+ try:
+ self.parrot.concert_hall = self.concert_hall_mode.get_active()
+ self.concert_hall_mode.set_active(self.parrot.concert_hall)
+ except resource_manager.DeviceDisconnected:
+ self.deactivate()
+
+
+class ParrotZikVersion2Interface(ParrotZikBaseInterface):
+ parrot_class = ParrotZikVersion2
+
+ def __init__(self, indicator):
+ self.room_dirty = False
+ self.angle_dirty = False
+ self.noise_cancelation_dirty = False
+ super(ParrotZikVersion2Interface, self).__init__(indicator)
+ self.noise_cancelation = MenuItem("Noise Control", None, visible=False)
+ self.noise_cancelation_submenu = Menu()
+ self.noise_cancelation.set_submenu(self.noise_cancelation_submenu)
+
+ self.noise_control_cancelation_max = MenuItem(
+ "Max Calcelation", functools.partial(
+ self.toggle_noise_cancelation,
+ NoiseControlTypes.NOISE_CONTROL_MAX), checkitem=True, sensitive=False)
+ self.noise_control_cancelation_on = MenuItem(
+ "Normal Cancelation", functools.partial(
+ self.toggle_noise_cancelation,
+ NoiseControlTypes.NOISE_CONTROL_ON), checkitem=True, sensitive=False)
+ self.noise_control_off = MenuItem(
+ "Off", functools.partial(
+ self.toggle_noise_cancelation,
+ NoiseControlTypes.NOISE_CONTROL_OFF), checkitem=True, sensitive=False)
+ self.noise_control_street_mode = MenuItem(
+ "Street Mode", functools.partial(
+ self.toggle_noise_cancelation,
+ NoiseControlTypes.STREET_MODE), checkitem=True, sensitive=False)
+ self.noise_control_street_mode_max = MenuItem(
+ "Street Mode Max", functools.partial(
+ self.toggle_noise_cancelation,
+ NoiseControlTypes.STREET_MODE_MAX), checkitem=True, sensitive=False)
+ self.noise_cancelation_submenu.append(self.noise_control_cancelation_max)
+ self.noise_cancelation_submenu.append(self.noise_control_cancelation_on)
+ self.noise_cancelation_submenu.append(self.noise_control_off)
+ self.noise_cancelation_submenu.append(self.noise_control_street_mode)
+ self.noise_cancelation_submenu.append(self.noise_control_street_mode_max)
+
+ self.room_sound_effect = MenuItem(
+ "Room Sound Effect", None, visible=False)
+ self.room_sound_effect_submenu = Menu()
+ self.room_sound_effect.set_submenu(self.room_sound_effect_submenu)
+
+ self.room_sound_effect_enabled = MenuItem(
+ "Enabled", self.toggle_room_sound_effect, checkitem=True)
+ self.rooms = MenuItem("Rooms", None, checkitem=False)
+ self.angle = MenuItem("Angle", None, checkitem=False)
+ self.room_sound_effect_submenu.append(self.room_sound_effect_enabled)
+ self.room_sound_effect_submenu.append(self.rooms)
+ self.room_sound_effect_submenu.append(self.angle)
+
+ self.rooms_submenu = Menu()
+ self.rooms.set_submenu(self.rooms_submenu)
+
+ self.concert_hall_mode = MenuItem(
+ "Concert Hall", functools.partial(self.toggle_room, Rooms.CONCERT_HALL), checkitem=True)
+ self.jazz_mode = MenuItem(
+ "Jazz Club", functools.partial(self.toggle_room, Rooms.JAZZ_CLUB), checkitem=True)
+ self.living_mode = MenuItem(
+ "Living Room", functools.partial(self.toggle_room, Rooms.LIVING_ROOM), checkitem=True)
+ self.silent_mode = MenuItem(
+ "Silent Room", functools.partial(self.toggle_room, Rooms.SILENT_ROOM), checkitem=True)
+ self.rooms_submenu.append(self.concert_hall_mode)
+ self.rooms_submenu.append(self.jazz_mode)
+ self.rooms_submenu.append(self.living_mode)
+ self.rooms_submenu.append(self.silent_mode)
+
+ self.angle_submenu = Menu()
+ self.angle.set_submenu(self.angle_submenu)
+ self.angle_30 = MenuItem(
+ "30", functools.partial(self.toggle_angle, 30), checkitem=True)
+ self.angle_60 = MenuItem(
+ "60", functools.partial(self.toggle_angle, 60), checkitem=True)
+ self.angle_90 = MenuItem(
+ "90", functools.partial(self.toggle_angle, 90), checkitem=True)
+ self.angle_120 = MenuItem(
+ "120", functools.partial(self.toggle_angle, 120), checkitem=True)
+ self.angle_150 = MenuItem(
+ "150", functools.partial(self.toggle_angle, 150), checkitem=True)
+ self.angle_180 = MenuItem(
+ "180", functools.partial(self.toggle_angle, 180), checkitem=True)
+ self.angle_submenu.append(self.angle_30)
+ self.angle_submenu.append(self.angle_60)
+ self.angle_submenu.append(self.angle_90)
+ self.angle_submenu.append(self.angle_120)
+ self.angle_submenu.append(self.angle_150)
+ self.angle_submenu.append(self.angle_180)
+
+ self.flight_mode = MenuItem("Flight Mode", self.toggle_flight_mode,
+ checkitem=True, visible=False)
+ self.indicator.menu.append(self.room_sound_effect)
+ self.indicator.menu.append(self.noise_cancelation)
+ self.indicator.menu.append(self.flight_mode)
+
+ def activate(self, manager):
+ self.noise_cancelation.show()
+ self.flight_mode.show()
+ self.room_sound_effect.show()
+ super(ParrotZikVersion2Interface, self).activate(manager)
+ self._read_noise_cancelation()
+ self.flight_mode.set_active(self.parrot.flight_mode)
+ self._read_sound_effect_room()
+ self._read_sound_effect_angle()
+ sound_effect = self.parrot.sound_effect
+
+ self.room_sound_effect_enabled.set_active(sound_effect)
+ self.concert_hall_mode.set_sensitive(sound_effect)
+ self.jazz_mode.set_sensitive(sound_effect)
+ self.living_mode.set_sensitive(sound_effect)
+ self.silent_mode.set_sensitive(sound_effect)
+
+ self.angle_30.set_sensitive(sound_effect)
+ self.angle_60.set_sensitive(sound_effect)
+ self.angle_90.set_sensitive(sound_effect)
+ self.angle_120.set_sensitive(sound_effect)
+ self.angle_150.set_sensitive(sound_effect)
+ self.angle_180.set_sensitive(sound_effect)
+
+ def deactivate(self):
+ self.noise_cancelation.hide()
+ self.flight_mode.hide()
+ self.room_sound_effect.hide()
+ super(ParrotZikVersion2Interface, self).deactivate()
+
+ def toggle_flight_mode(self, widget):
+ try:
+ self.parrot.flight_mode = self.flight_mode.get_active()
+ self.flight_mode.set_active(self.parrot.flight_mode)
+ except resource_manager.DeviceDisconnected:
+ self.deactivate()
+
+ def toggle_room(self, room, widget):
+ try:
+ if not self.room_dirty:
+ self.parrot.room = room
+ self.room_dirty = True
+ self._read_sound_effect_room()
+ self.room_dirty = False
+ except resource_manager.DeviceDisconnected:
+ self.deactivate()
+
+ def _read_sound_effect_room(self):
+ active_room = self.parrot.room
+ room_to_menuitem_map = (
+ (Rooms.CONCERT_HALL, self.concert_hall_mode),
+ (Rooms.JAZZ_CLUB, self.jazz_mode),
+ (Rooms.LIVING_ROOM, self.living_mode),
+ (Rooms.SILENT_ROOM, self.silent_mode),
+ )
+ for room, menu_item in room_to_menuitem_map:
+ menu_item.set_active(room == active_room)
+
+ def toggle_room_sound_effect(self, widget):
+ try:
+ self.parrot.sound_effect = self.room_sound_effect_enabled.get_active()
+ sound_effect = self.parrot.sound_effect
+ self.room_sound_effect_enabled.set_active(sound_effect)
+ self.concert_hall_mode.set_sensitive(sound_effect)
+ self.jazz_mode.set_sensitive(sound_effect)
+ self.living_mode.set_sensitive(sound_effect)
+ self.silent_mode.set_sensitive(sound_effect)
+ self.angle_30.set_sensitive(sound_effect)
+ self.angle_60.set_sensitive(sound_effect)
+ self.angle_90.set_sensitive(sound_effect)
+ self.angle_120.set_sensitive(sound_effect)
+ self.angle_150.set_sensitive(sound_effect)
+ self.angle_180.set_sensitive(sound_effect)
+ except resource_manager.DeviceDisconnected:
+ self.deactivate()
+
+ def toggle_angle(self, angle, widget):
+ try:
+ if not self.angle_dirty:
+ self.parrot.angle = angle
+ self.angle_dirty = True
+ self._read_sound_effect_angle()
+ self.angle_dirty = False
+ except resource_manager.DeviceDisconnected:
+ self.deactivate()
+
+ def _read_sound_effect_angle(self):
+ active_angle = self.parrot.angle
+ angle_to_menuitem_map = (
+ (30, self.angle_30),
+ (60, self.angle_60),
+ (90, self.angle_90),
+ (120, self.angle_120),
+ (150, self.angle_150),
+ (180, self.angle_180),
+ )
+ for angle, menu_item in angle_to_menuitem_map:
+ menu_item.set_active(angle == active_angle)
+
+ def toggle_noise_cancelation(self, noise_calcelation, widget):
+ try:
+ if not self.noise_cancelation_dirty:
+ self.parrot.noise_control = noise_calcelation
+ self.noise_cancelation_dirty = True
+ self._read_noise_cancelation()
+ self.noise_cancelation_dirty = False
+ except resource_manager.DeviceDisconnected:
+ self.deactivate()
+
+ def _read_noise_cancelation(self):
+ active_noise_control = self.parrot.noise_control
+ noise_control_to_menuitem_map = (
+ (NoiseControlTypes.NOISE_CONTROL_MAX, self.noise_control_cancelation_max),
+ (NoiseControlTypes.NOISE_CONTROL_ON, self.noise_control_cancelation_on),
+ (NoiseControlTypes.NOISE_CONTROL_OFF, self.noise_control_off),
+ (NoiseControlTypes.STREET_MODE, self.noise_control_street_mode),
+ (NoiseControlTypes.STREET_MODE_MAX, self.noise_control_street_mode_max),
+ )
+ for noise_control, menu_item in noise_control_to_menuitem_map:
+ menu_item.set_active(active_noise_control == noise_control)
+
+
+if __name__ == "__main__":
+ try:
+ indicator = ParrotZikIndicator()
+ indicator.main()
+ except KeyboardInterrupt:
+ pass
diff --git a/parrot_zik/resource_manager.py b/parrot_zik/resource_manager.py
new file mode 100644
index 0000000..8b63e8c
--- /dev/null
+++ b/parrot_zik/resource_manager.py
@@ -0,0 +1,176 @@
+import bluetooth
+from operator import itemgetter
+import sys
+
+from BeautifulSoup import BeautifulSoup
+
+from parrot_zik.message import Message
+
+
+class ResourceManagerBase(object):
+ resources = [
+ ]
+
+ def __init__(self, socket, resource_values=None):
+ self.sock = socket
+ self.resource_values = resource_values or {}
+
+ def get(self, resource):
+ try:
+ return self.resource_values[resource]
+ except KeyError:
+ return self.fetch(resource)
+
+ def fetch(self, resource):
+ result = self.send_message(self._create_message(resource, 'get'))
+ self.resource_values[resource] = result
+ return result
+
+ def toggle_on(self, resource):
+ self.send_message(self._create_message(resource, 'enable'))
+ self.fetch(resource)
+
+ def toggle_off(self, resource):
+ self.send_message(self._create_message(resource, 'disable'))
+ self.fetch(resource)
+
+ def set(self, resource, arg):
+ self.send_message(self._create_message(resource, 'set', arg))
+ self.fetch(resource)
+
+ def _create_message(self, resource, method, arg=None):
+ assert resource in self.resources, 'Unknown resource {}'.format(resource)
+ assert method in self.resources[resource], 'Unhandled method {} for {}'.format(method, resource)
+ return Message(resource, method, arg)
+
+ def send_message(self, message):
+ try:
+ self.sock.send(str(message))
+ return self.get_answer(message)
+ except bluetooth.btcommon.BluetoothError:
+ raise DeviceDisconnected
+
+ def get_answer(self, message):
+ data = self.receive_message()
+ notifications = []
+ while not data.answer:
+ if data.notify:
+ notifications.append(data.notify)
+ else:
+ raise AssertionError('Unknown response')
+ data = self.receive_message()
+ self.handle_notifications(notifications, message.resource)
+ return data.answer
+
+ def handle_notifications(self, notifications, resource):
+ paths = map(itemgetter('path'), notifications)
+ clean_paths = set(map(self._clean_path, paths))
+ for path in clean_paths:
+ if resource != path:
+ self.fetch(path)
+
+ def _clean_path(self, path):
+ return path.rsplit('/', 1)[0].encode('utf-8')
+
+ def receive_message(self):
+ if sys.platform == "darwin":
+ self.sock.recv(30)
+ else:
+ self.sock.recv(7)
+ return BeautifulSoup(self.sock.recv(1024))
+
+ def close(self):
+ self.sock.close()
+
+
+class GenericResourceManager(ResourceManagerBase):
+ resources = {
+ '/api/software/version': ['get'],
+ }
+
+ def __init__(self, sock):
+ super(GenericResourceManager, self).__init__(sock)
+ self.notifications = []
+
+ def handle_notification(self, notification):
+ self.notifications.append(notification)
+
+ def get_resource_manager(self, resource_manager_class):
+ resource_manager = resource_manager_class(self.sock, self.resource_values)
+ resource_manager.handle_notifications(self.notifications, '/api/software/version')
+ return resource_manager
+
+ @property
+ def api_version(self):
+ answer = self.get("/api/software/version")
+ try:
+ return answer.software["version"]
+ except KeyError:
+ return answer.software['sip6']
+
+
+class Version1ResourceManager(ResourceManagerBase):
+ resources = {
+ '/api/software/version': ['get'],
+ '/api/system/battery': ['get'],
+ '/api/bluetooth/friendlyname': ['get'],
+ '/api/system/auto_connection/enabled': ['get', 'set'],
+ '/api/system/anc_phone_mode/enabled': ['get', 'set'],
+ '/api/audio/specific_mode/enabled': ['get', 'set'],
+ '/api/audio/sound_effect/enabled': ['get', 'set'],
+ '/api/audio/noise_cancellation/enabled': ['get', 'set'],
+ }
+
+class Version2ResourceManager(ResourceManagerBase):
+ resources = {
+ '/api/account/username': ['get', 'set'],
+ '/api/appli_version': ['set'],
+ '/api/audio/counter': ['get'],
+ '/api/audio/equalizer/enabled': ['get', 'set'],
+ '/api/audio/equalizer/preset_id': ['set'],
+ '/api/audio/equalizer/preset_value': ['set'],
+ '/api/audio/noise_cancellation/enabled': ['get', 'set'],
+ '/api/audio/noise_control/enabled': ['get', 'set'],
+ '/api/audio/noise_control': ['get'],
+ '/api/audio/noise_control/phone_mode': ['get', 'set'],
+ '/api/audio/noise': ['get'],
+ '/api/audio/param_equalizer/value': ['set'],
+ '/api/audio/preset/bypass': ['get', 'set'],
+ '/api/audio/preset/': ['clear_all'],
+ '/api/audio/preset/counter': ['get'],
+ '/api/audio/preset/current': ['get'],
+ '/api/audio/preset': ['download', 'activate', 'save', 'remove', 'cancel_producer'],
+ '/api/audio/preset/synchro': ['start', 'stop'],
+ '/api/audio/smart_audio_tune': ['get', 'set'],
+ '/api/audio/sound_effect/angle': ['get', 'set'],
+ '/api/audio/sound_effect/enabled': ['get', 'set'],
+ '/api/audio/sound_effect': ['get'],
+ '/api/audio/sound_effect/room_size': ['get', 'set'],
+ '/api/audio/source': ['get'],
+ '/api/audio/specific_mode/enabled': ['get', 'set'],
+ '/api/audio/thumb_equalizer/value': ['get', 'set'],
+ '/api/audio/track/metadata': ['get', 'force'],
+ '/api/bluetooth/friendlyname': ['get', 'set'],
+ '/api/flight_mode': ['get', 'enable', 'disable'],
+ '/api/software/download_check_state': ['get'],
+ '/api/software/download_size': ['set'],
+ '/api/software/tts': ['get', 'enable', 'disable'],
+ '/api/software/version_checking': ['get'],
+ '/api/software/version': ['get'],
+ '/api/system/anc_phone_mode/enabled': ['get', 'set'],
+ '/api/system/auto_connection/enabled': ['get', 'set'],
+ '/api/system/auto_power_off': ['get', 'set'],
+ '/api/system/auto_power_off/presets_list': ['get'],
+ '/api/system/battery/forecast': ['get'],
+ '/api/system/battery': ['get'],
+ '/api/system/bt_address': ['get'],
+ '/api/system': ['calibrate'],
+ '/api/system/color': ['get'],
+ '/api/system/device_type': ['get'],
+ '/api/system/': ['factory_reset'],
+ '/api/system/head_detection/enabled': ['get', 'set'],
+ '/api/system/pi': ['get'],
+ }
+
+class DeviceDisconnected(Exception):
+ pass
diff --git a/parrot_zik/status_app_mac.py b/parrot_zik/status_app_mac.py
new file mode 100644
index 0000000..dccffe4
--- /dev/null
+++ b/parrot_zik/status_app_mac.py
@@ -0,0 +1,25 @@
+from Foundation import *
+from AppKit import *
+
+class StatusApp(NSApplication):
+
+ def initMenu(self, menu):
+ statusbar = NSStatusBar.systemStatusBar()
+ self.statusitem = statusbar.statusItemWithLength_(
+ NSVariableStatusItemLength)
+
+ self.mymenu = menu
+ #add menu to statusitem
+ self.statusitem.setMenu_(menu.menubarMenu)
+ self.statusitem.setToolTip_('Parrot Zik Indicator')
+
+ def setIcon(self,icon,icon_directory):
+ self.icon = NSImage.alloc().initByReferencingFile_(
+ icon_directory + icon + '.png')
+ self.icon.setScalesWhenResized_(True)
+ self.icon.setSize_((20, 20))
+ self.statusitem.setImage_(self.icon)
+
+ def clicked_(self, notification):
+ self.mymenu.actions[notification._.title]()
+ NSLog('clicked!')