diff --git a/homeassistant/actors.py b/homeassistant/actors.py index 1e22a86838f..aa38c550614 100644 --- a/homeassistant/actors.py +++ b/homeassistant/actors.py @@ -17,7 +17,7 @@ import dateutil.parser from phue import Bridge import requests -from .packages.pychromecast import pychromecast +from .packages import pychromecast, pykeyboard from . import track_state_change from .util import sanitize_filename @@ -35,7 +35,10 @@ EVENT_BROWSE_URL = "browse_url" EVENT_CHROMECAST_YOUTUBE_VIDEO = "chromecast.play_youtube_video" EVENT_TURN_LIGHT_ON = "turn_light_on" EVENT_TURN_LIGHT_OFF = "turn_light_off" - +EVENT_KEYBOARD_VOLUME_UP = "keyboard.volume_up" +EVENT_KEYBOARD_VOLUME_DOWN = "keyboard.volume_down" +EVENT_KEYBOARD_VOLUME_MUTE = "keyboard.volume_mute" +EVENT_KEYBOARD_MEDIA_PLAY_PAUSE = "keyboard.media_play_pause" def _hue_process_transition_time(transition_seconds): """ Transition time is in 1/10th seconds @@ -313,3 +316,20 @@ def setup_chromecast(eventbus, host): eventbus.listen(EVENT_CHROMECAST_YOUTUBE_VIDEO, lambda event: pychromecast.play_youtube_video(host, event.data['video'])) + +def setup_media_buttons(eventbus): + """ Listen for keyboard events. """ + keyboard = pykeyboard.PyKeyboard() + keyboard.special_key_assignment() + + eventbus.listen(EVENT_KEYBOARD_VOLUME_UP, + lambda event: keyboard.tap_key(keyboard.volume_up_key)) + + eventbus.listen(EVENT_KEYBOARD_VOLUME_DOWN, + lambda event: keyboard.tap_key(keyboard.volume_down_key)) + + eventbus.listen(EVENT_KEYBOARD_VOLUME_MUTE, + lambda event: keyboard.tap_key(keyboard.volume_mute_key)) + + eventbus.listen(EVENT_KEYBOARD_MEDIA_PLAY_PAUSE, + lambda event: keyboard.tap_key(keyboard.media_play_pause_key)) diff --git a/homeassistant/packages/__init__.py b/homeassistant/packages/__init__.py index 937bc365526..021eeacba10 100644 --- a/homeassistant/packages/__init__.py +++ b/homeassistant/packages/__init__.py @@ -2,4 +2,15 @@ Not all external Git repositories that we depend on are available as a package for pip. That is why we include them here. + +PyChromecast +------------ +https://github.com/balloob/pychromecast + +PyKeyboard +---------- +Forked from https://github.com/SavinaRoja/PyUserInput +Patched with some code to get it working on OS X: +https://github.com/balloob/PyUserInput + """ diff --git a/homeassistant/packages/pykeyboard/__init__.py b/homeassistant/packages/pykeyboard/__init__.py new file mode 100644 index 00000000000..2de415d8a60 --- /dev/null +++ b/homeassistant/packages/pykeyboard/__init__.py @@ -0,0 +1,37 @@ +#Copyright 2013 Paul Barton +# +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. +# +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU General Public License for more details. +# +#You should have received a copy of the GNU General Public License +#along with this program. If not, see . + +""" +The goal of PyMouse is to have a cross-platform way to control the mouse. +PyMouse should work on Windows, Mac and any Unix that has xlib. + +PyKeyboard is a part of PyUserInput, along with PyMouse, for more information +about this project, see: +http://github.com/SavinaRoja/PyUserInput +""" + +import sys + +if sys.platform.startswith('java'): + from .java_ import PyKeyboard + +elif sys.platform == 'darwin': + from .mac import PyKeyboard, PyKeyboardEvent + +elif sys.platform == 'win32': + from .windows import PyKeyboard, PyKeyboardEvent + +else: + from .x11 import PyKeyboard, PyKeyboardEvent diff --git a/homeassistant/packages/pykeyboard/base.py b/homeassistant/packages/pykeyboard/base.py new file mode 100644 index 00000000000..8a475efad9a --- /dev/null +++ b/homeassistant/packages/pykeyboard/base.py @@ -0,0 +1,102 @@ +#Copyright 2013 Paul Barton +# +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. +# +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU General Public License for more details. +# +#You should have received a copy of the GNU General Public License +#along with this program. If not, see . + +""" +As the base file, this provides a rough operational model along with the +framework to be extended by each platform. +""" + +import time +from threading import Thread + +class PyKeyboardMeta(object): + """ + The base class for PyKeyboard. Represents basic operational model. + """ + + def press_key(self, character=''): + """Press a given character key.""" + raise NotImplementedError + + def release_key(self, character=''): + """Release a given character key.""" + raise NotImplementedError + + def tap_key(self, character='', n=1, interval=0): + """Press and release a given character key n times.""" + for i in xrange(n): + self.press_key(character) + self.release_key(character) + time.sleep(interval) + + def type_string(self, char_string, interval=0): + """A convenience method for typing longer strings of characters.""" + for i in char_string: + time.sleep(interval) + self.tap_key(i) + + def special_key_assignment(self): + """Makes special keys more accessible.""" + raise NotImplementedError + + def lookup_character_value(self, character): + """ + If necessary, lookup a valid API value for the key press from the + character. + """ + raise NotImplementedError + + def is_char_shifted(self, character): + """Returns True if the key character is uppercase or shifted.""" + if character.isupper(): + return True + if character in '<>?:"{}|~!@#$%^&*()_+': + return True + return False + +class PyKeyboardEventMeta(Thread): + """ + The base class for PyKeyboard. Represents basic operational model. + """ + def __init__(self, capture=False): + Thread.__init__(self) + self.daemon = True + self.capture = capture + self.state = True + + def run(self): + self.state = True + + def stop(self): + self.state = False + + def handler(self): + raise NotImplementedError + + def key_press(self, key): + """Subclass this method with your key press event handler.""" + pass + + def key_release(self, key): + """Subclass this method with your key release event handler.""" + pass + + def escape_code(self): + """ + Defines a means to signal a stop to listening. Subclass this with your + escape behavior. + """ + escape = None + return escape diff --git a/homeassistant/packages/pykeyboard/java_.py b/homeassistant/packages/pykeyboard/java_.py new file mode 100644 index 00000000000..be3b9576ce6 --- /dev/null +++ b/homeassistant/packages/pykeyboard/java_.py @@ -0,0 +1,14 @@ +#Copyright 2013 Paul Barton +# +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. +# +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU General Public License for more details. +# +#You should have received a copy of the GNU General Public License +#along with this program. If not, see . diff --git a/homeassistant/packages/pykeyboard/mac.py b/homeassistant/packages/pykeyboard/mac.py new file mode 100644 index 00000000000..8b378657fea --- /dev/null +++ b/homeassistant/packages/pykeyboard/mac.py @@ -0,0 +1,201 @@ +#Copyright 2013 Paul Barton +# +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. +# +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU General Public License for more details. +# +#You should have received a copy of the GNU General Public License +#along with this program. If not, see . + +import time +from Quartz import * +from AppKit import NSEvent +from .base import PyKeyboardMeta, PyKeyboardEventMeta + +# Taken from events.h +# /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h +character_translate_table = { + 'a': 0x00, + 's': 0x01, + 'd': 0x02, + 'f': 0x03, + 'h': 0x04, + 'g': 0x05, + 'z': 0x06, + 'x': 0x07, + 'c': 0x08, + 'v': 0x09, + 'b': 0x0b, + 'q': 0x0c, + 'w': 0x0d, + 'e': 0x0e, + 'r': 0x0f, + 'y': 0x10, + 't': 0x11, + '1': 0x12, + '2': 0x13, + '3': 0x14, + '4': 0x15, + '6': 0x16, + '5': 0x17, + '=': 0x18, + '9': 0x19, + '7': 0x1a, + '-': 0x1b, + '8': 0x1c, + '0': 0x1d, + ']': 0x1e, + 'o': 0x1f, + 'u': 0x20, + '[': 0x21, + 'i': 0x22, + 'p': 0x23, + 'l': 0x25, + 'j': 0x26, + '\'': 0x27, + 'k': 0x28, + ';': 0x29, + '\\': 0x2a, + ',': 0x2b, + '/': 0x2c, + 'n': 0x2d, + 'm': 0x2e, + '.': 0x2f, + '`': 0x32, + ' ': 0x31, + '\r': 0x24, + '\t': 0x30, + 'shift': 0x38 +} + +# Taken from ev_keymap.h +# http://www.opensource.apple.com/source/IOHIDFamily/IOHIDFamily-86.1/IOHIDSystem/IOKit/hidsystem/ev_keymap.h +special_key_translate_table = { + 'KEYTYPE_SOUND_UP': 0, + 'KEYTYPE_SOUND_DOWN': 1, + 'KEYTYPE_BRIGHTNESS_UP': 2, + 'KEYTYPE_BRIGHTNESS_DOWN': 3, + 'KEYTYPE_CAPS_LOCK': 4, + 'KEYTYPE_HELP': 5, + 'POWER_KEY': 6, + 'KEYTYPE_MUTE': 7, + 'UP_ARROW_KEY': 8, + 'DOWN_ARROW_KEY': 9, + 'KEYTYPE_NUM_LOCK': 10, + 'KEYTYPE_CONTRAST_UP': 11, + 'KEYTYPE_CONTRAST_DOWN': 12, + 'KEYTYPE_LAUNCH_PANEL': 13, + 'KEYTYPE_EJECT': 14, + 'KEYTYPE_VIDMIRROR': 15, + 'KEYTYPE_PLAY': 16, + 'KEYTYPE_NEXT': 17, + 'KEYTYPE_PREVIOUS': 18, + 'KEYTYPE_FAST': 19, + 'KEYTYPE_REWIND': 20, + 'KEYTYPE_ILLUMINATION_UP': 21, + 'KEYTYPE_ILLUMINATION_DOWN': 22, + 'KEYTYPE_ILLUMINATION_TOGGLE': 23 +} + +class PyKeyboard(PyKeyboardMeta): + def press_key(self, key): + if key in special_key_translate_table: + self._press_special_key(key, True) + else: + self._press_normal_key(key, True) + + def release_key(self, key): + if key in special_key_translate_table: + self._press_special_key(key, False) + else: + self._press_normal_key(key, False) + + def special_key_assignment(self): + self.volume_mute_key = 'KEYTYPE_MUTE' + self.volume_down_key = 'KEYTYPE_SOUND_DOWN' + self.volume_up_key = 'KEYTYPE_SOUND_UP' + self.media_play_pause_key = 'KEYTYPE_PLAY' + + # Doesn't work :( + # self.media_next_track_key = 'KEYTYPE_NEXT' + # self.media_prev_track_key = 'KEYTYPE_PREVIOUS' + + def _press_normal_key(self, key, down): + try: + if self.is_char_shifted(key): + key_code = character_translate_table[key.lower()] + + event = CGEventCreateKeyboardEvent(None, + character_translate_table['shift'], down) + CGEventPost(kCGHIDEventTap, event) + # Tiny sleep to let OS X catch up on us pressing shift + time.sleep(.01) + + else: + key_code = character_translate_table[key] + + + event = CGEventCreateKeyboardEvent(None, key_code, down) + CGEventPost(kCGHIDEventTap, event) + + + except KeyError: + raise RuntimeError("Key {} not implemented.".format(key)) + + def _press_special_key(self, key, down): + """ Helper method for special keys. + + Source: http://stackoverflow.com/questions/11045814/emulate-media-key-press-on-mac + """ + key_code = special_key_translate_table[key] + + ev = NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_( + NSSystemDefined, # type + (0,0), # location + 0xa00 if down else 0xb00, # flags + 0, # timestamp + 0, # window + 0, # ctx + 8, # subtype + (key_code << 16) | ((0xa if down else 0xb) << 8), # data1 + -1 # data2 + ) + + CGEventPost(0, ev.CGEvent()) + +class PyKeyboardEvent(PyKeyboardEventMeta): + def run(self): + tap = CGEventTapCreate( + kCGSessionEventTap, + kCGHeadInsertEventTap, + kCGEventTapOptionDefault, + CGEventMaskBit(kCGEventKeyDown) | + CGEventMaskBit(kCGEventKeyUp), + self.handler, + None) + + loopsource = CFMachPortCreateRunLoopSource(None, tap, 0) + loop = CFRunLoopGetCurrent() + CFRunLoopAddSource(loop, loopsource, kCFRunLoopDefaultMode) + CGEventTapEnable(tap, True) + + while self.state: + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 5, False) + + def handler(self, proxy, type, event, refcon): + key = CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode) + if type == kCGEventKeyDown: + self.key_press(key) + elif type == kCGEventKeyUp: + self.key_release(key) + + if self.capture: + CGEventSetType(event, kCGEventNull) + + return event diff --git a/homeassistant/packages/pykeyboard/mir.py b/homeassistant/packages/pykeyboard/mir.py new file mode 100644 index 00000000000..be3b9576ce6 --- /dev/null +++ b/homeassistant/packages/pykeyboard/mir.py @@ -0,0 +1,14 @@ +#Copyright 2013 Paul Barton +# +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. +# +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU General Public License for more details. +# +#You should have received a copy of the GNU General Public License +#along with this program. If not, see . diff --git a/homeassistant/packages/pykeyboard/wayland.py b/homeassistant/packages/pykeyboard/wayland.py new file mode 100644 index 00000000000..be3b9576ce6 --- /dev/null +++ b/homeassistant/packages/pykeyboard/wayland.py @@ -0,0 +1,14 @@ +#Copyright 2013 Paul Barton +# +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. +# +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU General Public License for more details. +# +#You should have received a copy of the GNU General Public License +#along with this program. If not, see . diff --git a/homeassistant/packages/pykeyboard/windows.py b/homeassistant/packages/pykeyboard/windows.py new file mode 100644 index 00000000000..2ba33b93221 --- /dev/null +++ b/homeassistant/packages/pykeyboard/windows.py @@ -0,0 +1,316 @@ +#Copyright 2013 Paul Barton +# +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. +# +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU General Public License for more details. +# +#You should have received a copy of the GNU General Public License +#along with this program. If not, see . + +from ctypes import * +import win32api +from win32con import * +import pythoncom, pyHook + +from .base import PyKeyboardMeta, PyKeyboardEventMeta + +import time + +class SupportError(Exception): + """For keys not supported on this system""" + def __init__(self, value): + self.value = value + + def __str__(self): + return('The {0} key is not supported in Windows'.format(self.value)) + +class PyKeyboard(PyKeyboardMeta): + """ + The PyKeyboard implementation for Windows systems. This allows one to + simulate keyboard input. + """ + def __init__(self): + PyKeyboardMeta.__init__(self) + self.special_key_assignment() + + def press_key(self, character=''): + """ + Press a given character key. + """ + try: + shifted = self.is_char_shifted(character) + except AttributeError: + win32api.keybd_event(character, 0, 0, 0) + else: + if shifted: + win32api.keybd_event(self.shift_key, 0, 0, 0) + char_vk = win32api.VkKeyScan(character) + win32api.keybd_event(char_vk, 0, 0, 0) + + def release_key(self, character=''): + """ + Release a given character key. + """ + try: + shifted = self.is_char_shifted(character) + except AttributeError: + win32api.keybd_event(character, 0, KEYEVENTF_KEYUP, 0) + else: + if shifted: + win32api.keybd_event(self.shift_key, 0, KEYEVENTF_KEYUP, 0) + char_vk = win32api.VkKeyScan(character) + win32api.keybd_event(char_vk, 0, KEYEVENTF_KEYUP, 0) + + def special_key_assignment(self): + """ + Special Key assignment for windows + """ + #As defined by Microsoft, refer to: + #http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx + self.backspace_key = VK_BACK + self.tab_key = VK_TAB + self.clear_key = VK_CLEAR + self.return_key = VK_RETURN + self.enter_key = self.return_key # Because many keyboards call it "Enter" + self.shift_key = VK_SHIFT + self.shift_l_key = VK_LSHIFT + self.shift_r_key = VK_RSHIFT + self.control_key = VK_CONTROL + self.control_l_key = VK_LCONTROL + self.control_r_key = VK_RCONTROL + #Windows uses "menu" to refer to Alt... + self.menu_key = VK_MENU + self.alt_l_key = VK_LMENU + self.alt_r_key = VK_RMENU + self.alt_key = self.alt_l_key + self.pause_key = VK_PAUSE + self.caps_lock_key = VK_CAPITAL + self.capital_key = self.caps_lock_key + self.num_lock_key = VK_NUMLOCK + self.scroll_lock_key = VK_SCROLL + #Windows Language Keys, + self.kana_key = VK_KANA + self.hangeul_key = VK_HANGEUL # old name - should be here for compatibility + self.hangul_key = VK_HANGUL + self.junjua_key = VK_JUNJA + self.final_key = VK_FINAL + self.hanja_key = VK_HANJA + self.kanji_key = VK_KANJI + self.convert_key = VK_CONVERT + self.nonconvert_key = VK_NONCONVERT + self.accept_key = VK_ACCEPT + self.modechange_key = VK_MODECHANGE + #More Keys + self.escape_key = VK_ESCAPE + self.space_key = VK_SPACE + self.prior_key = VK_PRIOR + self.next_key = VK_NEXT + self.page_up_key = self.prior_key + self.page_down_key = self.next_key + self.home_key = VK_HOME + self.up_key = VK_UP + self.down_key = VK_DOWN + self.left_key = VK_LEFT + self.right_key = VK_RIGHT + self.end_key = VK_END + self.select_key = VK_SELECT + self.print_key = VK_PRINT + self.snapshot_key = VK_SNAPSHOT + self.print_screen_key = self.snapshot_key + self.execute_key = VK_EXECUTE + self.insert_key = VK_INSERT + self.delete_key = VK_DELETE + self.help_key = VK_HELP + self.windows_l_key = VK_LWIN + self.super_l_key = self.windows_l_key + self.windows_r_key = VK_RWIN + self.super_r_key = self.windows_r_key + self.apps_key = VK_APPS + #Numpad + self.keypad_keys = {'Space': None, + 'Tab': None, + 'Enter': None, # Needs Fixing + 'F1': None, + 'F2': None, + 'F3': None, + 'F4': None, + 'Home': VK_NUMPAD7, + 'Left': VK_NUMPAD4, + 'Up': VK_NUMPAD8, + 'Right': VK_NUMPAD6, + 'Down': VK_NUMPAD2, + 'Prior': None, + 'Page_Up': VK_NUMPAD9, + 'Next': None, + 'Page_Down': VK_NUMPAD3, + 'End': VK_NUMPAD1, + 'Begin': None, + 'Insert': VK_NUMPAD0, + 'Delete': VK_DECIMAL, + 'Equal': None, # Needs Fixing + 'Multiply': VK_MULTIPLY, + 'Add': VK_ADD, + 'Separator': VK_SEPARATOR, + 'Subtract': VK_SUBTRACT, + 'Decimal': VK_DECIMAL, + 'Divide': VK_DIVIDE, + 0: VK_NUMPAD0, + 1: VK_NUMPAD1, + 2: VK_NUMPAD2, + 3: VK_NUMPAD3, + 4: VK_NUMPAD4, + 5: VK_NUMPAD5, + 6: VK_NUMPAD6, + 7: VK_NUMPAD7, + 8: VK_NUMPAD8, + 9: VK_NUMPAD9} + self.numpad_keys = self.keypad_keys + #FKeys + self.function_keys = [None, VK_F1, VK_F2, VK_F3, VK_F4, VK_F5, VK_F6, + VK_F7, VK_F8, VK_F9, VK_F10, VK_F11, VK_F12, + VK_F13, VK_F14, VK_F15, VK_F16, VK_F17, VK_F18, + VK_F19, VK_F20, VK_F21, VK_F22, VK_F23, VK_F24, + None, None, None, None, None, None, None, None, + None, None, None] #Up to 36 as in x11 + #Miscellaneous + self.cancel_key = VK_CANCEL + self.break_key = self.cancel_key + self.mode_switch_key = VK_MODECHANGE + self.browser_back_key = VK_BROWSER_BACK + self.browser_forward_key = VK_BROWSER_FORWARD + self.processkey_key = VK_PROCESSKEY + self.attn_key = VK_ATTN + self.crsel_key = VK_CRSEL + self.exsel_key = VK_EXSEL + self.ereof_key = VK_EREOF + self.play_key = VK_PLAY + self.zoom_key = VK_ZOOM + self.noname_key = VK_NONAME + self.pa1_key = VK_PA1 + self.oem_clear_key = VK_OEM_CLEAR + self.volume_mute_key = VK_VOLUME_MUTE + self.volume_down_key = VK_VOLUME_DOWN + self.volume_up_key = VK_VOLUME_UP + self.media_next_track_key = VK_MEDIA_NEXT_TRACK + self.media_prev_track_key = VK_MEDIA_PREV_TRACK + self.media_play_pause_key = VK_MEDIA_PLAY_PAUSE + self.begin_key = self.home_key + #LKeys - Unsupported + self.l_keys = [None] * 11 + #RKeys - Unsupported + self.r_keys = [None] * 16 + + #Other unsupported Keys from X11 + self.linefeed_key = None + self.find_key = None + self.meta_l_key = None + self.meta_r_key = None + self.sys_req_key = None + self.hyper_l_key = None + self.hyper_r_key = None + self.undo_key = None + self.redo_key = None + self.script_switch_key = None + +class PyKeyboardEvent(PyKeyboardEventMeta): + """ + The PyKeyboardEvent implementation for Windows Systems. This allows one + to listen for keyboard input. + """ + def __init__(self): + PyKeyboardEventMeta.__init__(self) + self.hm = pyHook.HookManager() + self.shift_state = 0 # 0 is off, 1 is on + self.alt_state = 0 # 0 is off, 2 is on + + def run(self): + """Begin listening for keyboard input events.""" + self.state = True + self.hm.KeyAll = self.handler + self.hm.HookKeyboard() + while self.state: + time.sleep(0.01) + pythoncom.PumpWaitingMessages() + + def stop(self): + """Stop listening for keyboard input events.""" + self.hm.UnhookKeyboard() + self.state = False + + def handler(self, reply): + """Upper level handler of keyboard events.""" + if reply.Message == pyHook.HookConstants.WM_KEYDOWN: + self._key_press(reply) + elif reply.Message == pyHook.HookConstants.WM_KEYUP: + self._key_release(reply) + elif reply.Message == pyHook.HookConstants.WM_SYSKEYDOWN: + self._key_press(reply) + elif reply.Message == pyHook.HookConstants.WM.SYSKEYUP: + self._key_release(reply) + else: + print('Keyboard event message unhandled: {0}'.format(reply.Message)) + return not self.capture + + def _key_press(self, event): + if self.escape_code(event): #Quit if this returns True + self.stop() + if event.GetKey() in ['Shift', 'Lshift', 'Rshift', 'Capital']: + self.toggle_shift_state() + if event.GetKey() in ['Menu', 'Lmenu', 'Rmenu']: + self.toggle_alt_state() + #print('Key Pressed!') + #print('GetKey: {0}'.format(event.GetKey())) # Name of the virtual keycode, str + #print('IsAlt: {0}'.format(event.IsAlt())) # Was the alt key depressed?, bool + #print('IsExtended: {0}'.format(event.IsExtended())) # Is this an extended key?, bool + #print('IsInjected: {0}'.format(event.IsInjected())) # Was this event generated programmatically?, bool + #print('IsTransition: {0}'.format(event.IsTransition())) #Is this a transition from up to down or vice versa?, bool + #print('ASCII: {0}'.format(event.Ascii)) # ASCII value, if one exists, str + #print('KeyID: {0}'.format(event.KeyID)) # Virtual key code, int + #print('ScanCode: {0}'.format(event.ScanCode)) # Scan code, int + + def _key_release(self, event): + if event.GetKey() in ['Shift', 'Lshift', 'Rshift', 'Capital']: + self.toggle_shift_state() + if event.GetKey() in ['Menu', 'Lmenu', 'Rmenu']: + self.toggle_alt_state() + self.key_release() + #print('Key Released!') + #print('GetKey: {0}'.format(event.GetKey())) # Name of the virtual keycode, str + #print('IsAlt: {0}'.format(event.IsAlt())) # Was the alt key depressed?, bool + #print('IsExtended: {0}'.format(event.IsExtended())) # Is this an extended key?, bool + #print('IsInjected: {0}'.format(event.IsInjected())) # Was this event generated programmatically?, bool + #print('IsTransition: {0}'.format(event.IsTransition())) #Is this a transition from up to down or vice versa?, bool + #print('ASCII: {0}'.format(event.Ascii)) # ASCII value, if one exists, str + #print('KeyID: {0}'.format(event.KeyID)) # Virtual key code, int + #print('ScanCode: {0}'.format(event.ScanCode)) # Scan code, int + + def escape_code(self, event): + if event.KeyID == VK_ESCAPE: + return True + return False + + def toggle_shift_state(self): + '''Does toggling for the shift state.''' + if self.shift_state == 0: + self.shift_state = 1 + elif self.shift_state == 1: + self.shift_state = 0 + else: + return False + return True + + def toggle_alt_state(self): + '''Does toggling for the alt state.''' + if self.alt_state == 0: + self.alt_state = 2 + elif self.alt_state == 2: + self.alt_state = 0 + else: + return False + return True \ No newline at end of file diff --git a/homeassistant/packages/pykeyboard/x11.py b/homeassistant/packages/pykeyboard/x11.py new file mode 100644 index 00000000000..c19149f20fb --- /dev/null +++ b/homeassistant/packages/pykeyboard/x11.py @@ -0,0 +1,363 @@ +#Copyright 2013 Paul Barton +# +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. +# +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU General Public License for more details. +# +#You should have received a copy of the GNU General Public License +#along with this program. If not, see . + +from Xlib.display import Display +from Xlib import X +from Xlib.ext.xtest import fake_input +from Xlib.XK import string_to_keysym, keysym_to_string +from Xlib.ext import record +from Xlib.protocol import rq + +from .base import PyKeyboardMeta, PyKeyboardEventMeta + +import time + +special_X_keysyms = { + ' ': "space", + '\t': "Tab", + '\n': "Return", # for some reason this needs to be cr, not lf + '\r': "Return", + '\e': "Escape", + '!': "exclam", + '#': "numbersign", + '%': "percent", + '$': "dollar", + '&': "ampersand", + '"': "quotedbl", + '\'': "apostrophe", + '(': "parenleft", + ')': "parenright", + '*': "asterisk", + '=': "equal", + '+': "plus", + ',': "comma", + '-': "minus", + '.': "period", + '/': "slash", + ':': "colon", + ';': "semicolon", + '<': "less", + '>': "greater", + '?': "question", + '@': "at", + '[': "bracketleft", + ']': "bracketright", + '\\': "backslash", + '^': "asciicircum", + '_': "underscore", + '`': "grave", + '{': "braceleft", + '|': "bar", + '}': "braceright", + '~': "asciitilde" + } + +class PyKeyboard(PyKeyboardMeta): + """ + The PyKeyboard implementation for X11 systems (mostly linux). This + allows one to simulate keyboard input. + """ + def __init__(self, display=None): + PyKeyboardMeta.__init__(self) + self.display = Display(display) + self.display2 = Display(display) + self.special_key_assignment() + + def press_key(self, character=''): + """ + Press a given character key. Also works with character keycodes as + integers, but not keysyms. + """ + try: # Detect uppercase or shifted character + shifted = self.is_char_shifted(character) + except AttributeError: # Handle the case of integer keycode argument + fake_input(self.display, X.KeyPress, character) + self.display.sync() + else: + if shifted: + fake_input(self.display, X.KeyPress, self.shift_key) + char_val = self.lookup_character_value(character) + fake_input(self.display, X.KeyPress, char_val) + self.display.sync() + + def release_key(self, character=''): + """ + Release a given character key. Also works with character keycodes as + integers, but not keysyms. + """ + try: # Detect uppercase or shifted character + shifted = self.is_char_shifted(character) + except AttributeError: # Handle the case of integer keycode argument + fake_input(self.display, X.KeyRelease, character) + self.display.sync() + else: + if shifted: + fake_input(self.display, X.KeyRelease, self.shift_key) + char_val = self.lookup_character_value(character) + fake_input(self.display, X.KeyRelease, char_val) + self.display.sync() + + def special_key_assignment(self): + """ + Determines the keycodes for common special keys on the keyboard. These + are integer values and can be passed to the other key methods. + Generally speaking, these are non-printable codes. + """ + #This set of keys compiled using the X11 keysymdef.h file as reference + #They comprise a relatively universal set of keys, though there may be + #exceptions which may come up for other OSes and vendors. Countless + #special cases exist which are not handled here, but may be extended. + #TTY Function Keys + self.backspace_key = self.lookup_character_value('BackSpace') + self.tab_key = self.lookup_character_value('Tab') + self.linefeed_key = self.lookup_character_value('Linefeed') + self.clear_key = self.lookup_character_value('Clear') + self.return_key = self.lookup_character_value('Return') + self.enter_key = self.return_key # Because many keyboards call it "Enter" + self.pause_key = self.lookup_character_value('Pause') + self.scroll_lock_key = self.lookup_character_value('Scroll_Lock') + self.sys_req_key = self.lookup_character_value('Sys_Req') + self.escape_key = self.lookup_character_value('Escape') + self.delete_key = self.lookup_character_value('Delete') + #Modifier Keys + self.shift_l_key = self.lookup_character_value('Shift_L') + self.shift_r_key = self.lookup_character_value('Shift_R') + self.shift_key = self.shift_l_key # Default Shift is left Shift + self.alt_l_key = self.lookup_character_value('Alt_L') + self.alt_r_key = self.lookup_character_value('Alt_R') + self.alt_key = self.alt_l_key # Default Alt is left Alt + self.control_l_key = self.lookup_character_value('Control_L') + self.control_r_key = self.lookup_character_value('Control_R') + self.control_key = self.control_l_key # Default Ctrl is left Ctrl + self.caps_lock_key = self.lookup_character_value('Caps_Lock') + self.capital_key = self.caps_lock_key # Some may know it as Capital + self.shift_lock_key = self.lookup_character_value('Shift_Lock') + self.meta_l_key = self.lookup_character_value('Meta_L') + self.meta_r_key = self.lookup_character_value('Meta_R') + self.super_l_key = self.lookup_character_value('Super_L') + self.windows_l_key = self.super_l_key # Cross-support; also it's printed there + self.super_r_key = self.lookup_character_value('Super_R') + self.windows_r_key = self.super_r_key # Cross-support; also it's printed there + self.hyper_l_key = self.lookup_character_value('Hyper_L') + self.hyper_r_key = self.lookup_character_value('Hyper_R') + #Cursor Control and Motion + self.home_key = self.lookup_character_value('Home') + self.up_key = self.lookup_character_value('Up') + self.down_key = self.lookup_character_value('Down') + self.left_key = self.lookup_character_value('Left') + self.right_key = self.lookup_character_value('Right') + self.end_key = self.lookup_character_value('End') + self.begin_key = self.lookup_character_value('Begin') + self.page_up_key = self.lookup_character_value('Page_Up') + self.page_down_key = self.lookup_character_value('Page_Down') + self.prior_key = self.lookup_character_value('Prior') + self.next_key = self.lookup_character_value('Next') + #Misc Functions + self.select_key = self.lookup_character_value('Select') + self.print_key = self.lookup_character_value('Print') + self.print_screen_key = self.print_key # Seems to be the same thing + self.snapshot_key = self.print_key # Another name for printscreen + self.execute_key = self.lookup_character_value('Execute') + self.insert_key = self.lookup_character_value('Insert') + self.undo_key = self.lookup_character_value('Undo') + self.redo_key = self.lookup_character_value('Redo') + self.menu_key = self.lookup_character_value('Menu') + self.apps_key = self.menu_key # Windows... + self.find_key = self.lookup_character_value('Find') + self.cancel_key = self.lookup_character_value('Cancel') + self.help_key = self.lookup_character_value('Help') + self.break_key = self.lookup_character_value('Break') + self.mode_switch_key = self.lookup_character_value('Mode_switch') + self.script_switch_key = self.lookup_character_value('script_switch') + self.num_lock_key = self.lookup_character_value('Num_Lock') + #Keypad Keys: Dictionary structure + keypad = ['Space', 'Tab', 'Enter', 'F1', 'F2', 'F3', 'F4', 'Home', + 'Left', 'Up', 'Right', 'Down', 'Prior', 'Page_Up', 'Next', + 'Page_Down', 'End', 'Begin', 'Insert', 'Delete', 'Equal', + 'Multiply', 'Add', 'Separator', 'Subtract', 'Decimal', + 'Divide', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + self.keypad_keys = {k: self.lookup_character_value('KP_'+str(k)) for k in keypad} + self.numpad_keys = self.keypad_keys + #Function Keys/ Auxilliary Keys + #FKeys + self.function_keys = [None] + [self.lookup_character_value('F'+str(i)) for i in xrange(1,36)] + #LKeys + self.l_keys = [None] + [self.lookup_character_value('L'+str(i)) for i in xrange(1,11)] + #RKeys + self.r_keys = [None] + [self.lookup_character_value('R'+str(i)) for i in xrange(1,16)] + + #Unsupported keys from windows + self.kana_key = None + self.hangeul_key = None # old name - should be here for compatibility + self.hangul_key = None + self.junjua_key = None + self.final_key = None + self.hanja_key = None + self.kanji_key = None + self.convert_key = None + self.nonconvert_key = None + self.accept_key = None + self.modechange_key = None + self.sleep_key = None + + def lookup_character_value(self, character): + """ + Looks up the keysym for the character then returns the keycode mapping + for that keysym. + """ + ch_keysym = string_to_keysym(character) + if ch_keysym == 0: + ch_keysym = string_to_keysym(special_X_keysyms[character]) + return self.display.keysym_to_keycode(ch_keysym) + +class PyKeyboardEvent(PyKeyboardEventMeta): + """ + The PyKeyboardEvent implementation for X11 systems (mostly linux). This + allows one to listen for keyboard input. + """ + def __init__(self, display=None): + PyKeyboardEventMeta.__init__(self) + self.display = Display(display) + self.display2 = Display(display) + self.ctx = self.display2.record_create_context( + 0, + [record.AllClients], + [{ + 'core_requests': (0, 0), + 'core_replies': (0, 0), + 'ext_requests': (0, 0, 0, 0), + 'ext_replies': (0, 0, 0, 0), + 'delivered_events': (0, 0), + 'device_events': (X.KeyPress, X.KeyRelease), + 'errors': (0, 0), + 'client_started': False, + 'client_died': False, + }]) + self.shift_state = 0 # 0 is off, 1 is on + self.alt_state = 0 # 0 is off, 2 is on + self.mod_keycodes = self.get_mod_keycodes() + + def run(self): + """Begin listening for keyboard input events.""" + self.state = True + if self.capture: + self.display2.screen().root.grab_keyboard(True, X.KeyPressMask | X.KeyReleaseMask, X.GrabModeAsync, X.GrabModeAsync, 0, 0, X.CurrentTime) + + self.display2.record_enable_context(self.ctx, self.handler) + self.display2.record_free_context(self.ctx) + + def stop(self): + """Stop listening for keyboard input events.""" + self.state = False + self.display.record_disable_context(self.ctx) + self.display.ungrab_keyboard(X.CurrentTime) + self.display.flush() + self.display2.record_disable_context(self.ctx) + self.display2.ungrab_keyboard(X.CurrentTime) + self.display2.flush() + + def handler(self, reply): + """Upper level handler of keyboard events.""" + data = reply.data + while len(data): + event, data = rq.EventField(None).parse_binary_value(data, self.display.display, None, None) + if event.type == X.KeyPress: + if self.escape_code(event): # Quit if this returns True + self.stop() + else: + self._key_press(event.detail) + elif event.type == X.KeyRelease: + self._key_release(event.detail) + else: + print('WTF: {0}'.format(event.type)) + + def _key_press(self, keycode): + """A key has been pressed, do stuff.""" + #Alter modification states + if keycode in self.mod_keycodes['Shift'] or keycode in self.mod_keycodes['Lock']: + self.toggle_shift_state() + elif keycode in self.mod_keycodes['Alt']: + self.toggle_alt_state() + else: + self.key_press(keycode) + + def _key_release(self, keycode): + """A key has been released, do stuff.""" + #Alter modification states + if keycode in self.mod_keycodes['Shift']: + self.toggle_shift_state() + elif keycode in self.mod_keycodes['Alt']: + self.toggle_alt_state() + else: + self.key_release(keycode) + + def escape_code(self, event): + if event.detail == self.lookup_character_value('Escape'): + return True + return False + + def lookup_char_from_keycode(self, keycode): + keysym =self.display.keycode_to_keysym(keycode, self.shift_state + self.alt_state) + if keysym: + char = self.display.lookup_string(keysym) + return char + else: + return None + + def get_mod_keycodes(self): + """ + Detects keycodes for modifiers and parses them into a dictionary + for easy access. + """ + modifier_mapping = self.display.get_modifier_mapping() + modifier_dict = {} + nti = [('Shift', X.ShiftMapIndex), + ('Control', X.ControlMapIndex), ('Mod1', X.Mod1MapIndex), + ('Alt', X.Mod1MapIndex), ('Mod2', X.Mod2MapIndex), + ('Mod3', X.Mod3MapIndex), ('Mod4', X.Mod4MapIndex), + ('Mod5', X.Mod5MapIndex), ('Lock', X.LockMapIndex)] + for n, i in nti: + modifier_dict[n] = list(modifier_mapping[i]) + return modifier_dict + + def lookup_character_value(self, character): + """ + Looks up the keysym for the character then returns the keycode mapping + for that keysym. + """ + ch_keysym = string_to_keysym(character) + if ch_keysym == 0: + ch_keysym = string_to_keysym(special_X_keysyms[character]) + return self.display.keysym_to_keycode(ch_keysym) + + def toggle_shift_state(self): + '''Does toggling for the shift state.''' + if self.shift_state == 0: + self.shift_state = 1 + elif self.shift_state == 1: + self.shift_state = 0 + else: + return False + return True + + def toggle_alt_state(self): + '''Does toggling for the alt state.''' + if self.alt_state == 0: + self.alt_state = 2 + elif self.alt_state == 2: + self.alt_state = 0 + else: + return False + return True diff --git a/start.py b/start.py index 018721dca88..d51419521de 100644 --- a/start.py +++ b/start.py @@ -35,6 +35,7 @@ actors.LightTrigger(eventbus, statemachine, actors.setup_chromecast(eventbus, config.get("chromecast", "host")) actors.setup_file_downloader(eventbus, config.get("downloader", "download_dir")) actors.setup_webbrowser(eventbus) +actors.setup_media_buttons(eventbus) # Init HTTP interface HTTPInterface(eventbus, statemachine, config.get("common","api_password"))