├── .gitignore ├── LICENSE.txt ├── README.md ├── addon.xml ├── changelog.txt ├── default.py ├── fanart.jpg ├── icon.png └── resources ├── __init__.py ├── language └── English │ └── strings.xml ├── lib ├── __init__.py ├── colorpicker.py ├── colorutils.py ├── debugout.py ├── htmlcolors.py ├── menulist.py ├── openhab1.py ├── openhab2.py ├── ordereddict.py └── selectdialog.py ├── settings.xml └── skins └── Default ├── 720p ├── colorpicker.xml ├── menulist.xml └── selectdialog.xml └── media ├── colors ├── FF000000.png ├── FF222222.png ├── FF326492.png ├── FF3989D3.png ├── FF444444.png ├── FF48885D.png ├── FF488B8E.png ├── FF5E3F7A.png ├── FF5FC27D.png ├── FF61CACE.png ├── FF63A2DC.png ├── FF659247.png ├── FF666666.png ├── FF7F50AB.png ├── FF80CE98.png ├── FF83D4D7.png ├── FF883C77.png ├── FF888888.png ├── FF8DD45D.png ├── FF9975BD.png ├── FFA5DC7E.png ├── FFAACBEC.png ├── FFAC3832.png ├── FFB3AA3F.png ├── FFB78538.png ├── FFB9E4C8.png ├── FFB9E7E9.png ├── FFC24BA9.png ├── FFC7B2DA.png ├── FFCBEAB8.png ├── FFCE72BA.png ├── FFE3B0D9.png ├── FFECE351.png ├── FFF86B64.png ├── FFF9ADAA.png ├── FFFA443B.png ├── FFFF9845.png ├── FFFFAf51.png ├── FFFFD39F.png ├── FFFFF65F.png ├── FFFFF9A5.png ├── FFaaaaaa.png ├── FFcccccc.png ├── FFeeeeee.png └── FFffffff.png ├── default ├── ContentPanel.png ├── ContentPanelMirror.png ├── DialogBack.png ├── DialogCloseButton-focus.png ├── DialogCloseButton.png ├── HomeIcon-Focus.png ├── HomeIcon.png ├── MenuItemFO.png ├── MenuItemNF.png ├── about.png ├── black-back2.png ├── blue_line.png ├── button-focus.png ├── button-focus2.png ├── button-nofocus.png ├── dialogheader.png ├── floor.png ├── header_oe.png ├── openhab-logo.png ├── osd_slider_bg.png ├── osd_slider_bg_2.png ├── osd_slider_nib.png ├── osd_slider_nibNF.png ├── radiobutton-focus.png ├── radiobutton-nofocus.png ├── scroll-down-2.png ├── scroll-down-focus-2.png ├── scroll-stop-focus.png ├── scroll-stop.png ├── scroll-up-2.png ├── scroll-up-focus-2.png └── separator2.png └── icons ├── do.png └── next.png /.gitignore: -------------------------------------------------------------------------------- 1 | # build dir 2 | build/ 3 | # compiled python files 4 | *.pyc 5 | *.pyo 6 | 7 | # backup files 8 | *.bak 9 | 10 | # PyCharm 11 | .idea/ 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | openHAB for Kodi 2 | 3 | Copyright (C) 2015 Steffen Zimmermann 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #openHAB for Kodi 2 | 3 | openHAB for Kodi is a frontend to control an openHAB server over a network connection. It implements a lot of elements from the openHAB Classic UI web interface just using the Kodi GUI. 4 | 5 | With openHAB for Kodi you can navigate through your openHAB sitemaps, watch all your item's states and set any item state, e.g. boolean switches, dimmers, set-points, colors, etc. 6 | 7 | 8 | ##Credits: 9 | 10 | This plugin is a rewrite of the plugin created by Bianco Jean-François, Poustis Rebecca located at http://air.imag.fr/index.php/Extensions_XBMC#Plugin_openHab. 11 | 12 | The main window uses technics from the OpenELEC settings addon (https://github.com/OpenELEC/service.openelec.settings). 13 | 14 | The color picker dialog uses elements from the Mimic skin for Kodi (https://github.com/bryanbrazil/skin.mimic). 15 | -------------------------------------------------------------------------------- /addon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | executable 10 | 11 | 12 | 13 | openHAB 14 | openHAB for Kodi is a frontend to control an openHAB server over a network connection. It implements a lot of elements from the openHAB Classic UI web interface just using the Kodi GUI.[CR]With openHAB for Kodi you can navigate through your openHAB sitemaps, watch all your item's states and set any item state, e.g. boolean switches, dimmers, set-points, colors, etc. 15 | 16 | Kodi and openHAB are trademarks of their respective owners. 17 | en 18 | all 19 | GNU GENERAL PUBLIC LICENSE. Version 3, 29 June 2007 20 | 21 | 22 | https://github.com/mampfes/script.module.openhab 23 | 24 | 25 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | V2.0.1 (11-Jun-2017) 2 | (fix) handle ImageWidget without linkedPage 3 | (fix) set openHAB2 as default server 4 | (fix) moved selection list and color picker to left (PR49e3d14) 5 | (fix) altered colors (PR49e3d14) 6 | 7 | V2.0.0 (10-Mar-2017) 8 | (fix) fixed krypton support 9 | (add) openhab2 support 10 | 11 | V1.0.2 (25-Feb-2016) 12 | (fix) fixed jarvis support 13 | 14 | V1.0.1 (12-Nov-2015) 15 | (add) improved error handling 16 | (fix) handle GroupWidget without linkedPage 17 | 18 | V1.0.0 19 | (add) initial version 20 | -------------------------------------------------------------------------------- /default.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import xbmc 4 | import xbmcaddon 5 | import xbmcgui 6 | import sys 7 | import requests 8 | import resources.lib.menulist as menulist 9 | import resources.lib.openhab1 as openhab1 10 | import resources.lib.openhab2 as openhab2 11 | from resources.lib.debugout import debugPrint 12 | 13 | ADDON = xbmcaddon.Addon() 14 | 15 | 16 | def getServer(): 17 | if ADDON.getSetting('server') == '0': # openhab1 18 | return openhab1 19 | elif ADDON.getSetting('server') == '1': # openhab2 20 | return openhab2 21 | 22 | 23 | class MainWindow(menulist.MainWindow): 24 | class WindowStackEntry(object): 25 | def __init__(self, widgets, title, position=None): 26 | self.widgets = widgets 27 | self.title = title 28 | self.position = position 29 | 30 | def __init__(self): 31 | super(MainWindow, self).__init__() 32 | self.windowStack = [] 33 | self.oh = None 34 | self.homepage = None 35 | 36 | def build_menu(self): 37 | self.oh = getServer().Server(ADDON.getSetting('host'), ADDON.getSetting('port')) 38 | self.oh.terminate_callback.append(lambda oh: self.connection_lost()) 39 | 40 | #if ADDON.getSetting('auto_update') == 'true': 41 | self.oh.poll_pages = True # always enable auto-update 42 | 43 | if ADDON.getSetting('authentication') == '1': 44 | self.oh.set_basic_auth(ADDON.getSetting('auth_basic_username'), ADDON.getSetting('auth_basic_password')) 45 | 46 | PROXY_MAP = {'0': 'system', '1': 'none'} 47 | self.oh.set_proxy(PROXY_MAP[ADDON.getSetting('proxy')]) 48 | 49 | try: 50 | self.oh.load_sitemaps() 51 | try: 52 | sitemap = self.oh.sitemaps[ADDON.getSetting('sitemap')] 53 | except KeyError: 54 | # invalid sitemap -> close window immediately 55 | debugPrint(1, "build_menu failed, host=%s, port=%s, auth=%s, sitemap=%s sitemaps=%s" % 56 | (ADDON.getSetting('host'), ADDON.getSetting('port'), 57 | ADDON.getSetting('authentication'), ADDON.getSetting('sitemap'), self.oh.sitemaps)) 58 | xbmcgui.Dialog().ok(ADDON.getLocalizedString(30007), ADDON.getLocalizedString(30206)) 59 | self.close() 60 | ADDON.openSettings() 61 | self.homepage = sitemap.load_page() 62 | except requests.exceptions.RequestException as e: 63 | # no connection to openhab server -> close window immediately 64 | debugPrint(1, "build_menu failed, host=%s, port=%s, auth=%s, e=%s" % 65 | (ADDON.getSetting('host'), ADDON.getSetting('port'), 66 | ADDON.getSetting('authentication'), repr(e))) 67 | xbmcgui.Dialog().ok(ADDON.getLocalizedString(30007), ADDON.getLocalizedString(30201)) 68 | self.close() 69 | ADDON.openSettings() 70 | self.enter_sub_menu(self.homepage) 71 | 72 | def enter_sub_menu(self, page): 73 | # store current focus position 74 | if self.windowStack: 75 | self.windowStack[-1].position = self.list.get_selected_position() 76 | # add new page to stack 77 | self.windowStack.append(self.WindowStackEntry(page.widgets, page.attribs['title'], None)) 78 | # open last entry on stack 79 | self.load_widgets_from_stack() 80 | 81 | def load_widgets_from_stack(self): 82 | # get last entry on window stack 83 | e = self.windowStack[-1] 84 | # clear list control 85 | self.list.reset() 86 | # load all widgets 87 | self.load_widgets(e.widgets) 88 | # recover last focus position before opening submenu 89 | if e.position is not None: 90 | self.list.select_item(e.position) 91 | else: 92 | self.list.select_first_item() 93 | # set breadcrumb as title 94 | self.setProperty('title', ' > '.join([x.title for x in self.windowStack])) 95 | 96 | def load_widgets(self, widgets): 97 | for w in widgets: 98 | li = None 99 | subordinate_widgets = None 100 | if w.type_ == 'Colorpicker': 101 | li = menulist.ListItemColor(w.item) 102 | elif w.type_ == 'Chart': 103 | li = menulist.ListItemLabel() 104 | li.subscribe(lambda control, url=w.attribs['url']: self.show_image(url)) 105 | li.set_show_next_icon(True) 106 | elif w.type_ == 'Frame': 107 | if w.attribs['label']: 108 | li = menulist.ListItemSeparator() 109 | subordinate_widgets = w.widgets 110 | elif w.type_ == 'Group': 111 | li = menulist.ListItemText() 112 | if w.page is not None: 113 | li.subscribe(lambda control, page=w.page: self.enter_sub_menu(page)) 114 | li.set_show_next_icon(True) 115 | elif w.type_ == 'Image': 116 | li = menulist.ListItemLabel() 117 | li.subscribe(lambda control, url=w.attribs['url']: self.show_image(url)) 118 | li.set_show_next_icon(True) 119 | elif w.type_ == 'Selection': 120 | li = menulist.ListItemSelection(w.item) 121 | elif w.type_ == 'Setpoint': 122 | li = menulist.ListItemSetPoint(w.item) 123 | elif w.type_ == 'Slider': 124 | li = menulist.ListItemSlider(w.item) 125 | elif w.type_ == 'Switch': 126 | item_type = w.item.type_ 127 | 128 | if item_type.endswith('Item'): # remove trailing 'Item' from openhab1 129 | item_type = item_type[:-4] 130 | 131 | if item_type == 'Switch': 132 | li = menulist.ListItemSwitch(w.item) 133 | elif item_type == 'Rollershutter': 134 | li = menulist.ListItemRollerShutter(w.item) 135 | elif item_type == 'Number': 136 | li = menulist.ListItemSelection(w.item) 137 | elif item_type == 'Group': 138 | li = menulist.ListItemSelection(w.item) 139 | else: 140 | debugPrint(1, 'SwitchWidget [%s]: unsupported item type: %s' % (w.widgetId, w.item.type_)) 141 | elif w.type_ == 'Text': 142 | li = menulist.ListItemText() 143 | if w.page is not None: 144 | li.subscribe(lambda control, page=w.page: self.enter_sub_menu(page)) 145 | li.set_show_next_icon(True) 146 | elif w.type_ == 'Video': 147 | li = menulist.ListItemLabel() 148 | li.subscribe(lambda control, url=w.attribs['url']: self.show_video(url)) 149 | li.set_show_next_icon(True) 150 | elif w.type_ == 'Mapview': 151 | li = menulist.ListItemLabel() 152 | elif w.type_ == 'Webview': 153 | li = menulist.ListItemLabel() 154 | else: 155 | debugPrint(1, 'unknown widget type=%s, widgetId=%s' % (w.type_, w.widgetId)) 156 | continue 157 | 158 | if li is not None: 159 | self.list.add_item(li) 160 | w.set_proxy(li) 161 | else: 162 | self.list.add_separator_line_to_last_item() 163 | 164 | if subordinate_widgets: 165 | self.load_widgets(subordinate_widgets) 166 | 167 | def go_back(self): 168 | if len(self.windowStack) <= 1: 169 | # no more widgets on the stack => close the window 170 | self.oh.alive = False 171 | self.close() 172 | else: 173 | self.windowStack.pop() 174 | self.load_widgets_from_stack() 175 | 176 | def connection_lost(self): 177 | xbmcgui.Dialog().notification(ADDON.getLocalizedString(30007), 178 | ADDON.getLocalizedString(30205), 179 | xbmcgui.NOTIFICATION_WARNING) 180 | self.close() 181 | 182 | def show_image(self, url): 183 | self.close() 184 | xbmc.executebuiltin('ShowPicture(%s)' % url) 185 | 186 | def show_video(self, url): 187 | self.close() 188 | xbmc.Player().play(url) 189 | 190 | 191 | def show_sitemaps(): 192 | # show sitemap selection dialog instead of main window if called from settings dialog 193 | oh = getServer().Server(ADDON.getSetting('host'), ADDON.getSetting('port')) 194 | if ADDON.getSetting('authentication') == '1': 195 | oh.set_basic_auth(ADDON.getSetting('auth_basic_username'), ADDON.getSetting('auth_basic_password')) 196 | 197 | PROXY_MAP = {'0': 'system', '1': 'none'} 198 | oh.set_proxy(PROXY_MAP[ADDON.getSetting('proxy')]) 199 | 200 | try: 201 | sitemaps = sorted(oh.load_sitemaps().iterkeys()) 202 | except requests.exceptions.RequestException as e: 203 | debugPrint(1, "show_sitemaps failed, host=%s, port=%s, auth=%s, e=%s" % 204 | (ADDON.getSetting('host'), ADDON.getSetting('port'), 205 | ADDON.getSetting('authentication'), repr(e))) 206 | xbmcgui.Dialog().ok(ADDON.getLocalizedString(30007), ADDON.getLocalizedString(30008)) 207 | return 208 | 209 | value = xbmcgui.Dialog().select(ADDON.getLocalizedString(30006), sitemaps) 210 | if value >= 0: 211 | ADDON.setSetting('sitemap', sitemaps[value]) 212 | 213 | 214 | if 'show_sitemaps' in sys.argv: 215 | show_sitemaps() 216 | else: 217 | mw = MainWindow() 218 | mw.doModal() 219 | del mw 220 | -------------------------------------------------------------------------------- /fanart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/fanart.jpg -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/icon.png -------------------------------------------------------------------------------- /resources/__init__.py: -------------------------------------------------------------------------------- 1 | # Dummy 2 | -------------------------------------------------------------------------------- /resources/language/English/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Server Address 6 | Server Port 7 | Sitemap 8 | Authentication 9 | Username 10 | Password 11 | Select Sitemap... 12 | Error 13 | Selecting sitemap failed due to communication problems with openHAB server. Please check server address and port and store settings once before selecting sitemap. 14 | Auto Update 15 | Debug 16 | Proxy 17 | Server 18 | 19 | None 20 | Basic 21 | 22 | System 23 | None 24 | 25 | OpenHAB1 26 | OpenHAB2 27 | 28 | openHAB 29 | Can't connect to openHAB server. Please check settings. 30 | Up 31 | Stop 32 | Down 33 | Connection to openHAB server lost. 34 | Invalid sitemap name. Please check settings. 35 | 36 | -------------------------------------------------------------------------------- /resources/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # Dummy 2 | -------------------------------------------------------------------------------- /resources/lib/colorpicker.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import xbmcaddon 4 | import xbmcgui 5 | 6 | 7 | # selectdialog.xml control ID's 8 | CONTROL_ID_PANEL = 100 9 | 10 | # xbmcgui action codes to go up/back in a window 11 | WINDOW_EXIT_CODES = frozenset([xbmcgui.ACTION_PARENT_DIR, 12 | xbmcgui.ACTION_PREVIOUS_MENU, 13 | xbmcgui.ACTION_NAV_BACK, 14 | xbmcgui.KEY_BUTTON_BACK]) 15 | 16 | 17 | class ColorPicker(xbmcgui.WindowXMLDialog): 18 | def __new__(cls, title, color): 19 | return super(ColorPicker, cls).__new__(cls, "colorpicker.xml", xbmcaddon.Addon().getAddonInfo('path')) 20 | 21 | # init window 22 | def __init__(self, title, color): 23 | super(ColorPicker, self).__init__(title, color) 24 | self.control = None 25 | self.title = title 26 | self.color = color 27 | self.result = None 28 | self.listitems = [] 29 | self.colors = ['AC3832', 30 | 'FA443B', 31 | 'F86B64', 32 | 'F9ADAA', 33 | '000000', 34 | 'B78538', 35 | 'FF9845', 36 | 'FFAF51', 37 | 'FFD39F', 38 | '222222', 39 | 'B3AA3F', 40 | 'ECE351', 41 | 'FFF65F', 42 | 'FFF9A5', 43 | '444444', 44 | '659247', 45 | '8DD45D', 46 | 'A5DC7E', 47 | 'CBEAB8', 48 | '666666', 49 | '48885D', 50 | '5FC27D', 51 | '80CE98', 52 | 'B9E4C8', 53 | '888888', 54 | '488B8E', 55 | '61CACE', 56 | '83D4D7', 57 | 'B9E7E9', 58 | 'AAAAAA', 59 | '326492', 60 | '3989D3', 61 | '63A2DC', 62 | 'AACBEC', 63 | 'CCCCCC', 64 | '5E3F7A', 65 | '7F50AB', 66 | '9975BD', 67 | 'C7B2DA', 68 | 'EEEEEE', 69 | '883C77', 70 | 'C24BA9', 71 | 'CE72BA', 72 | 'E3B0D9', 73 | 'FFFFFF'] 74 | 75 | def show(self): 76 | self.doModal() 77 | return self.result 78 | 79 | def set_title(self, title): 80 | self.title = title 81 | if self.control: 82 | self.setProperty('title', self.title) 83 | 84 | def set_color(self, color): 85 | try: 86 | index = self.colors.index(self.color) 87 | self.listitems[index].select(False) 88 | except ValueError: 89 | pass 90 | 91 | self.color = color 92 | 93 | try: 94 | index = self.colors.index(self.color) 95 | self.listitems[index].select(True) 96 | except ValueError: 97 | pass 98 | 99 | # window init callback 100 | def onInit(self): 101 | self.control = self.getControl(CONTROL_ID_PANEL) 102 | self.setProperty('title', self.title) 103 | self.build_list() 104 | self.setFocusId(CONTROL_ID_PANEL) 105 | 106 | def build_list(self): 107 | self.control.reset() 108 | for c in self.colors: 109 | li = xbmcgui.ListItem(label=c, thumbnailImage='colors/FF' + c + '.png') 110 | self.listitems.append(li) 111 | self.control.addItem(li) 112 | 113 | try: 114 | index = self.colors.index(self.color) 115 | self.listitems[index].select(True) # set selected flag 116 | self.control.selectItem(index) # move focus to selected item 117 | except ValueError: 118 | pass 119 | 120 | # window action callback 121 | def onAction(self, action): 122 | if action.getId() in WINDOW_EXIT_CODES: 123 | self.close() 124 | 125 | # mouse click action 126 | def onClick(self, controlId): 127 | if controlId == CONTROL_ID_PANEL: 128 | self.result = self.colors[self.control.getSelectedPosition()] 129 | self.close() -------------------------------------------------------------------------------- /resources/lib/colorutils.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import colorsys 4 | 5 | 6 | def hex_str_to_tuple(hex_str): 7 | """Returns a tuple representing the given hex string (as used for RGB) 8 | 9 | >>> hex_str_to_tuple('CC0000') 10 | (204, 0, 0) 11 | """ 12 | if hex_str.startswith('#'): 13 | hex_str = hex_str[1:] 14 | return tuple([int(hex_str[i:i + 2], 16) for i in xrange(0, len(hex_str), 2)]) 15 | 16 | 17 | def tuple_to_hex_str(rgb): 18 | """Converts an rgb tuple to hex string for web. 19 | 20 | >>> tuple_to_hex_str((204, 0, 0)) 21 | 'CC0000' 22 | """ 23 | return ''.join(["%0.2X" % c for c in rgb]) 24 | 25 | 26 | def scale_rgb_tuple_down(rgb): 27 | """Scales an RGB tuple down to values between 0 and 1. 28 | 29 | >>> scale_rgb_tuple_down((204, 0, 0)) 30 | (.80, 0, 0) 31 | """ 32 | return tuple([round(float(c)/255, 2) for c in rgb]) 33 | 34 | 35 | def scale_rgb_tuple_up(rgb): 36 | """Scales an RGB tuple up from values between 0 and 1. 37 | 38 | >>> scale_rgb_tuple_up((.80, 0, 0)) 39 | (204, 0, 0) 40 | """ 41 | return tuple([int(round(c*255)) for c in rgb]) 42 | 43 | 44 | def scale_hsv_tuple_down(hsv): 45 | """Scales a HSV tuple down to values between 0 and 1. 46 | 47 | >>> scale_hsv_tuple_down((360, 100, 100)) 48 | (1., 1., 1.) 49 | """ 50 | return hsv[0]/360, hsv[1]/100, hsv[2]/100 51 | 52 | 53 | def scale_hsv_tuple_up(hsv): 54 | """Scales a HSV tuple up to values between 360° for H and 100% for S and V. 55 | 56 | >>> scale_hsv_tuple_up((1., 1., 1.)) 57 | (360., 100., 100.) 58 | """ 59 | return hsv[0]*360, hsv[1]*100, hsv[2]*100 60 | 61 | 62 | def hsv_degree_to_rgb_hex_str(hsv): 63 | hsv_down = scale_hsv_tuple_down(hsv) 64 | rgb = colorsys.hsv_to_rgb(*hsv_down) 65 | rgb_hex = scale_rgb_tuple_up(rgb) 66 | return tuple_to_hex_str(rgb_hex) 67 | 68 | 69 | def rgb_hex_str_to_hsv_degree(rgb_hex_str): 70 | rgb_hex = hex_str_to_tuple(rgb_hex_str) 71 | rgb2 = scale_rgb_tuple_down(rgb_hex) 72 | hsv_down2 = colorsys.rgb_to_hsv(*rgb2) 73 | return scale_hsv_tuple_up(hsv_down2) 74 | -------------------------------------------------------------------------------- /resources/lib/debugout.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import xbmc 4 | import xbmcaddon 5 | 6 | ADDON = xbmcaddon.Addon() 7 | 8 | 9 | def debugPrint(level, msg): 10 | msg = unicode(msg) 11 | if level == 0: 12 | level = xbmc.LOGERROR 13 | elif level < 3: 14 | level = xbmc.LOGWARNING 15 | elif level < 5: 16 | level = xbmc.LOGNOTICE 17 | else: 18 | if ADDON.getSetting('debug') == 'true': 19 | level = xbmc.LOGNOTICE 20 | else: 21 | level = xbmc.LOGDEBUG 22 | xbmc.log(u"{0}".format(msg).encode('ascii', 'xmlcharrefreplace'), level) -------------------------------------------------------------------------------- /resources/lib/htmlcolors.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | HTML_COLORS = {'black': '#000000', 4 | 'navy': '#000080', 5 | 'darkblue': '#00008b', 6 | 'mediumblue': '#0000cd', 7 | 'blue': '#0000ff', 8 | 'darkgreen': '#006400', 9 | 'green': '#008000', 10 | 'teal': '#008080', 11 | 'darkcyan': '#008b8b', 12 | 'deepskyblue': '#00bfff', 13 | 'darkturquoise': '#00ced1', 14 | 'mediumspringgreen': '#00fa9a', 15 | 'lime': '#00ff00', 16 | 'springgreen': '#00ff7f', 17 | 'aqua': '#00ffff', 18 | 'cyan': '#00ffff', 19 | 'midnightblue': '#191970', 20 | 'dodgerblue': '#1e90ff', 21 | 'lightseagreen': '#20b2aa', 22 | 'forestgreen': '#228b22', 23 | 'seagreen': '#2e8b57', 24 | 'darkslategray': '#2f4f4f', 25 | 'limegreen': '#32cd32', 26 | 'mediumseagreen': '#3cb371', 27 | 'turquoise': '#40e0d0', 28 | 'royalblue': '#4169e1', 29 | 'steelblue': '#4682b4', 30 | 'darkslateblue': '#483d8b', 31 | 'mediumturquoise': '#48d1cc', 32 | 'indigo ': '#4b0082', 33 | 'darkolivegreen': '#556b2f', 34 | 'cadetblue': '#5f9ea0', 35 | 'cornflowerblue': '#6495ed', 36 | 'rebeccapurple': '#663399', 37 | 'mediumaquamarine': '#66cdaa', 38 | 'dimgray': '#696969', 39 | 'slateblue': '#6a5acd', 40 | 'olivedrab': '#6b8e23', 41 | 'slategray': '#708090', 42 | 'lightslategray': '#778899', 43 | 'mediumslateblue': '#7b68ee', 44 | 'lawngreen': '#7cfc00', 45 | 'chartreuse': '#7fff00', 46 | 'aquamarine': '#7fffd4', 47 | 'maroon': '#800000', 48 | 'purple': '#800080', 49 | 'olive': '#808000', 50 | 'gray': '#808080', 51 | 'skyblue': '#87ceeb', 52 | 'lightskyblue': '#87cefa', 53 | 'blueviolet': '#8a2be2', 54 | 'darkred': '#8b0000', 55 | 'darkmagenta': '#8b008b', 56 | 'saddlebrown': '#8b4513', 57 | 'darkseagreen': '#8fbc8f', 58 | 'lightgreen': '#90ee90', 59 | 'mediumpurple': '#9370db', 60 | 'darkviolet': '#9400d3', 61 | 'palegreen': '#98fb98', 62 | 'darkorchid': '#9932cc', 63 | 'yellowgreen': '#9acd32', 64 | 'sienna': '#a0522d', 65 | 'brown': '#a52a2a', 66 | 'darkgray': '#a9a9a9', 67 | 'lightblue': '#add8e6', 68 | 'greenyellow': '#adff2f', 69 | 'paleturquoise': '#afeeee', 70 | 'lightsteelblue': '#b0c4de', 71 | 'powderblue': '#b0e0e6', 72 | 'firebrick': '#b22222', 73 | 'darkgoldenrod': '#b8860b', 74 | 'mediumorchid': '#ba55d3', 75 | 'rosybrown': '#bc8f8f', 76 | 'darkkhaki': '#bdb76b', 77 | 'silver': '#c0c0c0', 78 | 'mediumvioletred': '#c71585', 79 | 'indianred ': '#cd5c5c', 80 | 'peru': '#cd853f', 81 | 'chocolate': '#d2691e', 82 | 'tan': '#d2b48c', 83 | 'lightgray': '#d3d3d3', 84 | 'thistle': '#d8bfd8', 85 | 'orchid': '#da70d6', 86 | 'goldenrod': '#daa520', 87 | 'palevioletred': '#db7093', 88 | 'crimson': '#dc143c', 89 | 'gainsboro': '#dcdcdc', 90 | 'plum': '#dda0dd', 91 | 'burlywood': '#deb887', 92 | 'lightcyan': '#e0ffff', 93 | 'lavender': '#e6e6fa', 94 | 'darksalmon': '#e9967a', 95 | 'violet': '#ee82ee', 96 | 'palegoldenrod': '#eee8aa', 97 | 'lightcoral': '#f08080', 98 | 'khaki': '#f0e68c', 99 | 'aliceblue': '#f0f8ff', 100 | 'honeydew': '#f0fff0', 101 | 'azure': '#f0ffff', 102 | 'sandybrown': '#f4a460', 103 | 'wheat': '#f5deb3', 104 | 'beige': '#f5f5dc', 105 | 'whitesmoke': '#f5f5f5', 106 | 'mintcream': '#f5fffa', 107 | 'ghostwhite': '#f8f8ff', 108 | 'salmon': '#fa8072', 109 | 'antiquewhite': '#faebd7', 110 | 'linen': '#faf0e6', 111 | 'lightgoldenrodyellow': '#fafad2', 112 | 'oldlace': '#fdf5e6', 113 | 'red': '#ff0000', 114 | 'fuchsia': '#ff00ff', 115 | 'magenta': '#ff00ff', 116 | 'deeppink': '#ff1493', 117 | 'orangered': '#ff4500', 118 | 'tomato': '#ff6347', 119 | 'hotpink': '#ff69b4', 120 | 'coral': '#ff7f50', 121 | 'darkorange': '#ff8c00', 122 | 'lightsalmon': '#ffa07a', 123 | 'orange': '#ffa500', 124 | 'lightpink': '#ffb6c1', 125 | 'pink': '#ffc0cb', 126 | 'gold': '#ffd700', 127 | 'peachpuff': '#ffdab9', 128 | 'navajowhite': '#ffdead', 129 | 'moccasin': '#ffe4b5', 130 | 'bisque': '#ffe4c4', 131 | 'mistyrose': '#ffe4e1', 132 | 'blanchedalmond': '#ffebcd', 133 | 'papayawhip': '#ffefd5', 134 | 'lavenderblush': '#fff0f5', 135 | 'seashell': '#fff5ee', 136 | 'cornsilk': '#fff8dc', 137 | 'lemonchiffon': '#fffacd', 138 | 'floralwhite': '#fffaf0', 139 | 'snow': '#fffafa', 140 | 'yellow': '#ffff00', 141 | 'lightyellow': '#ffffe0', 142 | 'ivory': '#fffff0', 143 | 'white': '#ffffff'} 144 | -------------------------------------------------------------------------------- /resources/lib/menulist.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import xbmcaddon 4 | import xbmcgui 5 | import weakref 6 | from selectdialog import SelectDialog 7 | from colorpicker import ColorPicker 8 | from colorutils import hsv_degree_to_rgb_hex_str, rgb_hex_str_to_hsv_degree 9 | from htmlcolors import HTML_COLORS 10 | from debugout import debugPrint 11 | 12 | ADDON = xbmcaddon.Addon() 13 | 14 | # menulist.xml control ID's 15 | CONTROL_ID_LIST = 1100 16 | 17 | # xbmcgui action codes to go up/back in a window 18 | WINDOW_EXIT_CODES = frozenset([xbmcgui.ACTION_PARENT_DIR, 19 | xbmcgui.ACTION_PREVIOUS_MENU, 20 | xbmcgui.ACTION_NAV_BACK, 21 | xbmcgui.KEY_BUTTON_BACK]) 22 | 23 | # xbmcgui action codes for movements within a control list 24 | FOCUS_CHANGED_CODES = {xbmcgui.ACTION_MOVE_RIGHT: 1, 25 | xbmcgui.ACTION_MOVE_UP: -1, 26 | xbmcgui.ACTION_MOVE_DOWN: 1} 27 | 28 | 29 | # range function with Decimal (and float) support 30 | def drange(start, stop, step): 31 | r = start 32 | while r <= stop: 33 | yield r 34 | r += step 35 | 36 | 37 | def get_item_index(array, element): 38 | try: 39 | return array.index(element) 40 | except ValueError: 41 | return None 42 | 43 | 44 | def get_color_string(label, color): 45 | if color is None: 46 | return label 47 | 48 | if color[0] == '#': 49 | color = color[1:] 50 | elif color.lower() == 'lightgray': 51 | return label # lightgray is typically the default color 52 | else: # use HTML colors 53 | try: 54 | color = HTML_COLORS[color.lower()][1:] 55 | except KeyError: 56 | return label 57 | 58 | return '[COLOR FF%s]%s[/COLOR]' % (color, label) 59 | 60 | 61 | class ListItem(object): 62 | def __init__(self, typ, proxy): 63 | self.control = xbmcgui.ListItem() 64 | self.control.setProperty(u"type", typ) 65 | self.attribs = dict() 66 | self.proxy = weakref.ref(proxy) if proxy else lambda: None 67 | self.callbacks = [] 68 | 69 | def subscribe(self, cb): 70 | self.callbacks.append(cb) 71 | 72 | def unsubscribe(self, cb): 73 | self.callbacks.remove(cb) 74 | 75 | def update(self, changed, deleted): 76 | # update local dictionary 77 | self.attribs.update(changed) 78 | for key in deleted: 79 | del self.attribs[key] 80 | 81 | if 'widget_label' in changed or 'widget_label_color' in changed: 82 | self.control.setLabel(get_color_string(self.attribs['widget_label'], self.attribs.get('widget_label_color'))) 83 | if 'widget_icon' in changed: 84 | self.control.setProperty('iconurl', changed['widget_icon']) 85 | 86 | 87 | def set_separator_line(self, visible): 88 | self.control.setProperty('separator_line', '1' if visible else '0') 89 | 90 | def set_show_next_icon(self, visible): 91 | self.control.setProperty('show_next', '1' if visible else '0') 92 | 93 | def onAction(self, action): 94 | # base class does not support any actions 95 | pass 96 | 97 | def onClick(self): 98 | # base class does not support click actions 99 | pass 100 | 101 | 102 | class ListItemSeparator(ListItem): 103 | def __init__(self): 104 | super(ListItemSeparator, self).__init__("separator", None) 105 | self.set_separator_line(True) 106 | 107 | 108 | class ListItemLabel(ListItem): 109 | def __init__(self): 110 | super(ListItemLabel, self).__init__("label", None) 111 | 112 | def onClick(self): 113 | for cb in self.callbacks: 114 | cb(self) 115 | 116 | 117 | class ListItemSwitch(ListItem): 118 | def __init__(self, proxy): 119 | super(ListItemSwitch, self).__init__("bool", proxy) 120 | 121 | def update(self, changed, deleted): 122 | super(ListItemSwitch, self).update(changed, deleted) 123 | if 'item_state' in changed: 124 | self.control.setProperty("value", '1' if changed['item_state'] else '0') 125 | 126 | def onClick(self): 127 | proxy = self.proxy() 128 | if proxy: 129 | new_value = not self.attribs['item_state'] 130 | proxy.cmd_on() if new_value else proxy.cmd_off() 131 | 132 | def onAction(self, action): 133 | proxy = self.proxy() 134 | if proxy: 135 | if action.getId() == xbmcgui.ACTION_TELETEXT_RED: 136 | proxy.cmd_off() 137 | elif action.getId() == xbmcgui.ACTION_TELETEXT_GREEN: 138 | proxy.cmd_on() 139 | 140 | 141 | class ListItemWithValue(ListItem): 142 | """List item with a value. The value is always of type str or unicode. The actual value is stored as nativeValue! 143 | """ 144 | def __init__(self, typ, proxy): 145 | super(ListItemWithValue, self).__init__(typ, proxy) 146 | self.value = None 147 | 148 | def update(self, changed, deleted): 149 | super(ListItemWithValue, self).update(changed, deleted) 150 | if 'widget_value' in changed or 'widget_value_color' in changed: 151 | self.control.setProperty("value", get_color_string(self.attribs['widget_value'], self.attribs.get('widget_value_color'))) 152 | 153 | 154 | class ListItemText(ListItemWithValue): 155 | def __init__(self): 156 | super(ListItemText, self).__init__("text", None) 157 | 158 | def onClick(self): 159 | for cb in self.callbacks: 160 | cb(self) 161 | 162 | 163 | class ListItemColor(ListItemWithValue): 164 | def __init__(self, proxy): 165 | super(ListItemColor, self).__init__("text", proxy) 166 | self.set_show_next_icon(True) 167 | self.dialog = None 168 | 169 | def update(self, changed, deleted): 170 | super(ListItemColor, self).update(changed, deleted) 171 | if self.dialog: 172 | if 'widget_label' in changed: 173 | self.dialog.set_title(changed['widget_label']) 174 | if 'item_state' in changed: 175 | self.dialog.set_color(hsv_degree_to_rgb_hex_str(self.attribs['item_state'])) 176 | 177 | def onClick(self): 178 | proxy = self.proxy() 179 | if proxy: 180 | self.dialog = ColorPicker(self.attribs['widget_label'], 181 | hsv_degree_to_rgb_hex_str(self.attribs['item_state'])) 182 | color = self.dialog.show() 183 | self.dialog = None 184 | if color is not None: 185 | proxy.cmd_set_hsv(rgb_hex_str_to_hsv_degree(color)) 186 | 187 | 188 | class ListItemSelection(ListItemWithValue): 189 | def __init__(self, proxy): 190 | super(ListItemSelection, self).__init__("text", proxy) 191 | self.set_show_next_icon(True) 192 | self.dialog = None 193 | 194 | def update(self, changed, deleted): 195 | super(ListItemSelection, self).update(changed, deleted) 196 | if self.dialog: 197 | if 'widget_label' in changed: 198 | self.dialog.set_title(changed['widget_label']) 199 | if 'widget_mapping' in changed: 200 | self.dialog.set_items(changed['widget_mapping'].itervalues()) 201 | if 'item_state' in changed: 202 | self.dialog.set_index(get_item_index(self.attribs['widget_mapping'].keys(), 203 | self.attribs['item_state'])) 204 | 205 | def onClick(self): 206 | proxy = self.proxy() 207 | if proxy: 208 | self.dialog = SelectDialog(self.attribs['widget_label'], 209 | self.attribs['widget_mapping'].itervalues(), 210 | get_item_index(self.attribs['widget_mapping'].keys(), 211 | self.attribs['item_state'])) 212 | pos = self.dialog.show() 213 | self.dialog = None 214 | if pos is not None: 215 | (state, value) = self.attribs['widget_mapping'].items()[pos] 216 | proxy.cmd_set(state) 217 | 218 | 219 | class ListItemSetPoint(ListItemWithValue): 220 | def __init__(self, proxy): 221 | super(ListItemSetPoint, self).__init__("text", proxy) 222 | self.set_show_next_icon(True) 223 | self.dialog = None 224 | 225 | def update(self, changed, deleted): 226 | super(ListItemSetPoint, self).update(changed, deleted) 227 | if self.dialog: 228 | if 'widget_label' in changed: 229 | self.dialog.set_title(changed['widget_label']) 230 | if 'widget_min_value' in changed or 'widget_max_value' in changed or 'widget_step' in changed: 231 | selections = list(reversed(list(drange(self.attribs['widget_min_value'], 232 | self.attribs['widget_max_value'], 233 | self.attribs['widget_step'])))) 234 | self.dialog.set_items([str(x) for x in selections]) 235 | if 'item_state' in changed: 236 | selections = list(reversed(list(drange(self.attribs['widget_min_value'], 237 | self.attribs['widget_max_value'], 238 | self.attribs['widget_step'])))) 239 | self.dialog.set_index(get_item_index(selections, self.attribs['item_state'])) 240 | 241 | def onAction(self, action): 242 | proxy = self.proxy() 243 | if proxy: 244 | state = None 245 | if action.getId() == xbmcgui.ACTION_CHANNEL_DOWN: 246 | state = self.attribs['item_state'] - self.attribs['widget_step'] 247 | elif action.getId() == xbmcgui.ACTION_CHANNEL_UP: 248 | state = self.attribs['item_state'] + self.attribs['widget_step'] 249 | 250 | if state is not None: 251 | if self.attribs['widget_min_value'] <= state <= self.attribs['widget_max_value']: 252 | proxy.cmd_set(state) 253 | 254 | def onClick(self): 255 | proxy = self.proxy() 256 | if proxy: 257 | selections = list(reversed(list(drange(self.attribs['widget_min_value'], 258 | self.attribs['widget_max_value'], 259 | self.attribs['widget_step'])))) 260 | self.dialog = SelectDialog(self.attribs['widget_label'], 261 | [str(x) for x in selections], 262 | get_item_index(selections, self.attribs['item_state'])) 263 | pos = self.dialog.show() 264 | self.dialog = None 265 | if pos is not None: 266 | proxy.cmd_set(selections[pos]) 267 | 268 | 269 | class ListItemSlider(ListItemWithValue): 270 | def __init__(self, proxy): 271 | super(ListItemSlider, self).__init__("text", proxy) 272 | self.set_show_next_icon(True) 273 | self.dialog = None 274 | 275 | def update(self, changed, deleted): 276 | super(ListItemSlider, self).update(changed, deleted) 277 | if self.dialog: 278 | if 'widget_label' in changed: 279 | self.dialog.set_title(changed['widget_label']) 280 | if 'widget_mapping' in changed: 281 | self.dialog.set_items(changed['widget_mapping'].itervalues()) 282 | if 'item_state' in changed: 283 | self.dialog.set_index(get_item_index(self.attribs['widget_mapping'].keys(), self.attribs['item_state'])) 284 | 285 | def onAction(self, action): 286 | proxy = self.proxy() 287 | if proxy: 288 | state = None 289 | if action.getId() == xbmcgui.ACTION_TELETEXT_RED: 290 | proxy.cmd_off() 291 | elif action.getId() == xbmcgui.ACTION_TELETEXT_GREEN: 292 | proxy.cmd_on() 293 | elif action.getId() == xbmcgui.ACTION_CHANNEL_DOWN: 294 | proxy.cmd_decrement() 295 | elif action.getId() == xbmcgui.ACTION_CHANNEL_UP: 296 | proxy.cmd_increment() 297 | 298 | def onClick(self): 299 | proxy = self.proxy() 300 | if proxy: 301 | selections = list(reversed(list(drange(self.attribs['widget_min_value'], 302 | self.attribs['widget_max_value'], 303 | self.attribs['widget_step'])))) 304 | self.dialog = SelectDialog(self.attribs['widget_label'], 305 | [str(x) for x in selections], 306 | get_item_index(selections, self.attribs['item_state'])) 307 | pos = self.dialog.show() 308 | self.dialog = None 309 | if pos is not None: 310 | proxy.cmd_set(selections[pos]) 311 | 312 | 313 | class ListItemRollerShutter(ListItemWithValue): 314 | def __init__(self, proxy): 315 | super(ListItemRollerShutter, self).__init__("text", proxy) 316 | self.set_show_next_icon(True) 317 | self.dialog = None 318 | 319 | def onAction(self, action): 320 | proxy = self.proxy() 321 | if proxy: 322 | if action.getId() == xbmcgui.ACTION_CHANNEL_DOWN: 323 | proxy.cmd_down() 324 | elif action.getId() == xbmcgui.ACTION_CHANNEL_UP: 325 | proxy.cmd_up() 326 | elif action.getId() == xbmcgui.ACTION_STOP: 327 | proxy.cmd_stop() 328 | elif action.getId() == xbmcgui.ACTION_PLAY: 329 | proxy.cmd_move() 330 | 331 | def onClick(self): 332 | proxy = self.proxy() 333 | if proxy: 334 | selection = [ADDON.getLocalizedString(30202), 335 | ADDON.getLocalizedString(30203), 336 | ADDON.getLocalizedString(30204)] 337 | self.dialog = SelectDialog(self.attribs['widget_label'], selection, 1) 338 | pos = self.dialog.show() 339 | self.dialog = None 340 | if pos is not None: 341 | if pos == 0: 342 | proxy.cmd_up() 343 | elif pos == 1: 344 | proxy.cmd_stop() 345 | elif pos == 2: 346 | proxy.cmd_down() 347 | 348 | 349 | class WidgetList(object): 350 | def __init__(self, control): 351 | self.items = [] 352 | self.control = control # store xbmcgui.ControlList 353 | self.select_valid = False # True = a non separator line is already selected 354 | 355 | def reset(self): 356 | del self.items[:] 357 | self.control.reset() 358 | 359 | def add_item(self, item): 360 | # add item to local list 361 | self.items.append(item) 362 | # add item to xbmcgui.ListControl 363 | self.control.addItem(item.control) 364 | 365 | def get_selected_position(self): 366 | return self.control.getSelectedPosition() 367 | 368 | def select_item(self, pos): 369 | self.control.selectItem(pos) 370 | 371 | def select_first_item(self): 372 | pos = 0 373 | for item in self.items: 374 | if item.control.getProperty('type') != 'separator': 375 | self.control.selectItem(pos) 376 | return 377 | pos += 1 378 | 379 | def add_separator_line_to_last_item(self): 380 | if self.items: 381 | self.items[-1].set_separator_line(True) 382 | 383 | def onAction(self, action): 384 | # test if action is either up/down/right to set focus correctly 385 | diff = FOCUS_CHANGED_CODES.get(action.getId()) 386 | if diff is not None: 387 | # skip separator lines which can't have the focus 388 | size = self.control.size() 389 | pos = self.control.getSelectedPosition() 390 | if pos >= 0: 391 | while self.items[pos].control.getProperty('type') == 'separator': 392 | pos += diff 393 | if pos < 0: 394 | pos = size - 1 395 | elif pos >= size: 396 | pos = 0 397 | self.control.selectItem(pos) 398 | else: 399 | # any other action 400 | pos = self.get_selected_position() 401 | if pos < 0: 402 | debugPrint(1, 'WidgetList [%d]::onAction [%d], but nothing selected' 403 | % (self.control.getId(), action.getId())) 404 | return 405 | elif pos >= len(self.items): 406 | debugPrint(1, 'WidgetList [%d]::onAction [%d], index out of range (pos=%d, len=%d)' 407 | % (self.control.getId(), action.getId(), pos, len(self.items))) 408 | return 409 | self.items[pos].onAction(action) 410 | 411 | def onClick(self): 412 | pos = self.get_selected_position() 413 | if pos < 0: 414 | debugPrint(1, 'WidgetList [%d]::onClick, but nothing selected' % self.control.getId()) 415 | return 416 | self.items[pos].onClick() 417 | 418 | 419 | class MainWindow(xbmcgui.WindowXMLDialog): 420 | def __new__(cls): 421 | return super(MainWindow, cls).__new__(cls, "menulist.xml", xbmcaddon.Addon().getAddonInfo('path')) 422 | 423 | # init window 424 | def __init__(self): 425 | super(MainWindow, self).__init__() 426 | self.list = None 427 | 428 | def build_menu(self): 429 | pass 430 | 431 | def go_back(self): 432 | """Dummy function for derived classes""" 433 | pass 434 | 435 | # window init callback 436 | def onInit(self): 437 | self.list = WidgetList(self.getControl(CONTROL_ID_LIST)) 438 | self.build_menu() 439 | self.setFocusId(CONTROL_ID_LIST) 440 | 441 | # window action callback 442 | def onAction(self, action): 443 | if action.getId() in WINDOW_EXIT_CODES: 444 | self.go_back() 445 | else: 446 | # forward action to active control 447 | focusId = self.getFocusId() 448 | if focusId == CONTROL_ID_LIST: 449 | self.list.onAction(action) 450 | 451 | # mouse click action 452 | def onClick(self, controlId): 453 | if controlId == CONTROL_ID_LIST: 454 | self.list.onClick() 455 | -------------------------------------------------------------------------------- /resources/lib/openhab1.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import base64 4 | import collections 5 | try: # OrderedDict is new in python 2.7 6 | from collections import OrderedDict as OrderedDict 7 | except: 8 | from ordereddict import OrderedDict 9 | import datetime 10 | import time 11 | import re 12 | import requests 13 | import threading 14 | import weakref 15 | from decimal import Decimal 16 | from decimal import InvalidOperation 17 | from debugout import debugPrint 18 | 19 | 20 | def split_label(label): 21 | """Split a label + value line into label and value, e.g. 22 | "Outside Temperature [9,0 °C]" 23 | -> label = "Outside Temperature" 24 | -> value = "9,0 °C" 25 | """ 26 | if label is None: 27 | return None, None 28 | 29 | m = re.search(r"\s*\[([^\]]*)\]$", label) 30 | if m is not None: 31 | return label[:m.start()], m.groups()[0] 32 | else: 33 | return label, None 34 | 35 | 36 | def as_array(x): 37 | """openHAB doesn't return an array of objects in the JSON rest API if there is only 1 entry in 38 | the array. Therefore this functions converts single entries into an array.""" 39 | if isinstance(x, list): 40 | return x 41 | else: 42 | return [x] 43 | 44 | 45 | def convert_mapping(raw): 46 | """Convert openHAB mapping into a python ordered dictionary""" 47 | if raw is None: 48 | return None 49 | else: 50 | mapping = OrderedDict() 51 | for l in as_array(raw): 52 | try: 53 | mapping[Decimal(l['command'])] = l['label'] 54 | except InvalidOperation: 55 | mapping[l['command']] = l['label'] 56 | return mapping 57 | 58 | 59 | def update_proxy(func): 60 | """Decorator function to update all assigned proxies if any attribute changes""" 61 | def func_wrapper(self, *args, **kwargs): 62 | func(self, *args, **kwargs) 63 | updates = self.attribs.get_changes() 64 | for p in self.proxies: 65 | ref = p() 66 | if ref: 67 | ref.update(*updates) 68 | 69 | return func_wrapper 70 | 71 | 72 | class EmptyResponseError(Exception): 73 | """Exception for empty openHAB responses""" 74 | pass 75 | 76 | 77 | def poll_page_thread(page): 78 | """Thread function to long-poll openHAB pages""" 79 | while True: 80 | try: 81 | page.get_page_blocked() 82 | except EmptyResponseError: 83 | # openHAB server return an empty reponse (typically 5 minutes after long-poll request started) 84 | # ==> try again if server connection is still alive 85 | debugPrint(5, 'poll_page_thread: empty response for page %s' % page.id_) 86 | except requests.exceptions.ReadTimeout as e: 87 | # HTTP request timed out 88 | # ==> try again if server connection is still alive 89 | debugPrint(5, 'poll_page_thread: %s for page %s' % (repr(e), page.id_)) 90 | except requests.exceptions.HTTPError as e: 91 | debugPrint(1, 'poll_page_thread: %s for page %s' % (repr(e), page.id_)) 92 | except requests.exceptions.ConnectTimeout as e: 93 | debugPrint(1, 'poll_page_thread: %s for page %s' % (repr(e), page.id_)) 94 | except requests.exceptions.ConnectionError as e: 95 | # openHAB server terminated the connection 96 | # ==> execute terminate callback and close window 97 | debugPrint(5, 'poll_page_thread: %s for page %s' % (repr(e), page.id_)) 98 | page.oh.terminate() 99 | 100 | if not page.oh.alive: 101 | # exit thread if openHAB server ist not alive any more""" 102 | return 103 | 104 | 105 | class Attributes(collections.MutableMapping): 106 | """A dictionary that tracks changes and stores new/changed key/value + deleted keys""" 107 | 108 | def __init__(self, prefix, *args, **kwargs): 109 | self.prefix = prefix 110 | self.changed = dict() # stores new and changed key/values 111 | self.deleted = set() # stores deleted keys 112 | self.store = dict() 113 | self.update(dict(*args, **kwargs)) # use the free update to set keys 114 | 115 | def __getitem__(self, key): 116 | return self.store[key] 117 | 118 | def __setitem__(self, key, value): 119 | try: 120 | if self.store[key] == value: 121 | return # skip further processing because value unchanged 122 | except KeyError: 123 | pass 124 | 125 | self.store[key] = value 126 | 127 | xkey = self.prefix + key 128 | self.changed[xkey] = value 129 | try: 130 | self.deleted.remove(xkey) 131 | except KeyError: 132 | pass 133 | 134 | def __delitem__(self, key): 135 | del self.store[key] 136 | 137 | xkey = self.prefix + key 138 | self.deleted.add(xkey) 139 | try: 140 | del self.changed[xkey] 141 | except KeyError: 142 | pass 143 | 144 | def __iter__(self): 145 | return iter(self.store) 146 | 147 | def __len__(self): 148 | return len(self.store) 149 | 150 | def get_changes(self): 151 | result = (self.changed, self.deleted) 152 | self.changed = dict() 153 | self.deleted = set() 154 | return result 155 | 156 | def get_all(self): 157 | changed = dict() 158 | for key, value in self.store.iteritems(): 159 | changed[self.prefix + key] = value 160 | return changed 161 | 162 | 163 | class Server(object): 164 | """Python representative of a running openHAB instance.""" 165 | def __init__(self, host='localhost', port='8080'): 166 | self.host = host 167 | self.port = port 168 | path = 'http://%s:%s/' % (host, port) 169 | self.resources = {'items': path + 'rest/items', 170 | 'sitemaps': path + 'rest/sitemaps', 171 | 'images': path + 'images', 172 | 'charts': path + 'chart'} 173 | self.sitemaps = {} 174 | self.pages = {} 175 | self.items = {} 176 | self.widgets = {} 177 | self.http_put_headers = {'content-type': 'text/plain'} 178 | self.http_get_headers = {'accept': 'application/json'} 179 | self.poll_pages = False # True = start thread for every new page to long-poll changes 180 | self.http_proxies = None # proxy for requests library 181 | self.alive = True 182 | self.terminate_callback = [] 183 | 184 | def terminate(self): 185 | self.alive = False 186 | for cb in self.terminate_callback: 187 | cb(self) 188 | 189 | def set_basic_auth(self, username, password): 190 | auth = 'Basic %s' % base64.b64encode('%s:%s' % (username, password)) 191 | self.http_put_headers['authorization'] = auth 192 | self.http_get_headers['authorization'] = auth 193 | 194 | def set_proxy(self, proxy): 195 | if proxy == 'system': 196 | self.http_proxies = None # use system proxies 197 | elif proxy == 'none': 198 | self.http_proxies = {'http': '', 'https': ''} # use no proxy 199 | else: 200 | self.http_proxies = {'http': proxy, 'https': proxy} 201 | 202 | def fetch_abs_json_url(self, url, extra_headers=None): 203 | """Fetch url from openHAB server and convert data from json to Python data structures.""" 204 | headers = self.http_get_headers 205 | if extra_headers is not None: 206 | headers.update(extra_headers) 207 | debugPrint(5, 'fetching json url=%s, headers=%s' % (url, repr(headers))) 208 | resp = requests.get(url, headers=headers, proxies=self.http_proxies) 209 | debugPrint(5, 'response for url=%s, text=%s, headers=%s' % (url, resp.text, resp.headers)) 210 | if resp.status_code != requests.codes.ok: 211 | resp.raise_for_status() 212 | if resp.text == '': 213 | # openHAB returns an empty response after 5 minutes if long-polling is enabled 214 | raise EmptyResponseError 215 | return resp.json(), resp.headers 216 | 217 | def fetch_rel_json_url(self, name, headers=None): 218 | """Fetch url from openHAB server and convert data from json to Python data structures.""" 219 | return self.fetch_abs_json_url('http://%s:%s/rest%s' % (self.host, self.port, name), headers) 220 | 221 | def load_resources(self): 222 | """Fetch resources (http::/rest) from openHAB.""" 223 | self.resources = {} 224 | result = self.fetch_rel_json_url('', self.http_get_headers)[0] 225 | for item in as_array(result['link']): 226 | self.resources[item['@type']] = item['$'] 227 | return self.resources 228 | 229 | def load_sitemaps(self): 230 | """Load sitemaps list from openHAB and create Python instances for every sitemap. 231 | The created sitemaps are without content so far.""" 232 | self.sitemaps = {} 233 | if not self.resources: 234 | self.load_resources() 235 | result = self.fetch_abs_json_url(self.resources['sitemaps'])[0] 236 | for i in as_array(result['sitemap']): 237 | self.sitemaps[i['name']] = Sitemap(self, i) 238 | return self.sitemaps 239 | 240 | def load_items(self): 241 | """Load items from openHAB and create python instances for every openHAB item.""" 242 | self.items = {} 243 | if not self.resources: 244 | self.load_resources() 245 | result = self.fetch_abs_json_url(self.resources['items'])[0] 246 | for itemData in as_array(result['item']): 247 | self.create_item_class(itemData) 248 | return self.items 249 | 250 | def create_page_class(self, sitemap, pageData, prevPage=None): 251 | """Create an openHAB page instance from the given hash properties.""" 252 | if pageData is None: 253 | return None 254 | elif pageData['id'] in self.pages: # test if page already exists 255 | i = self.pages[pageData['id']] 256 | i.init(pageData) 257 | return i 258 | else: 259 | i = Page(sitemap, pageData, prevPage) 260 | 261 | if self.poll_pages: 262 | # start a new thread for every page that polls updates 263 | t = threading.Thread(target=poll_page_thread, args=(i,)) 264 | t.daemon = True 265 | t.start() 266 | 267 | # add new instance to dict of all pages 268 | self.pages[i.id_] = i 269 | return i 270 | 271 | def create_item_class(self, itemData): 272 | """Create an openHAB item instance from the given hash properties.""" 273 | if itemData is None: 274 | return None 275 | elif itemData['name'] in self.items: # test if item already exists 276 | i = self.items[itemData['name']] 277 | i.init(itemData) 278 | return i 279 | else: 280 | if itemData['type'] == 'CallItem': 281 | i = CallItem(self, itemData) 282 | elif itemData['type'] == 'ColorItem': 283 | i = ColorItem(self, itemData) 284 | elif itemData['type'] == 'ContactItem': 285 | i = ContactItem(self, itemData) 286 | elif itemData['type'] == 'DateTimeItem': 287 | i = DateTimeItem(self, itemData) 288 | elif itemData['type'] == 'DimmerItem': 289 | i = DimmerItem(self, itemData) 290 | elif itemData['type'] == 'GroupItem': 291 | i = GroupItem(self, itemData) 292 | elif itemData['type'] == 'LocationItem': 293 | i = LocationItem(self, itemData) 294 | elif itemData['type'] == 'NumberItem': 295 | i = NumberItem(self, itemData) 296 | elif itemData['type'] == 'StringItem': 297 | i = StringItem(self, itemData) 298 | elif itemData['type'] == 'SwitchItem': 299 | i = SwitchItem(self, itemData) 300 | elif itemData['type'] == 'RollershutterItem': 301 | i = RollerShutterItem(self, itemData) 302 | else: 303 | debugPrint(1, 'unknown item type=%s, name=%s' % (itemData['type'], itemData['name'])) 304 | return None 305 | 306 | # add new instance to dict of all items 307 | self.items[i.name] = i 308 | return i 309 | 310 | def create_widget_class(self, page, widgetData): 311 | """ create an openHAB widget instance from the given has properties """ 312 | if widgetData is None: 313 | return None 314 | elif widgetData['widgetId'] in self.widgets: # test if widget already exists 315 | i = self.widgets[widgetData['widgetId']] 316 | i.init(widgetData) 317 | return i 318 | else: 319 | if widgetData['type'] == 'Colorpicker': 320 | i = ColorPickerWidget(page, widgetData) 321 | elif widgetData['type'] == 'Chart': 322 | i = ChartWidget(page, widgetData) 323 | elif widgetData['type'] == 'Frame': 324 | i = FrameWidget(page, widgetData) 325 | elif widgetData['type'] == 'Group': 326 | i = GroupWidget(page, widgetData) 327 | elif widgetData['type'] == 'Image': 328 | i = ImageWidget(page, widgetData) 329 | elif widgetData['type'] == 'Selection': 330 | i = SelectionWidget(page, widgetData) 331 | elif widgetData['type'] == 'Setpoint': 332 | i = SetPointWidget(page, widgetData) 333 | elif widgetData['type'] == 'Slider': 334 | i = SliderWidget(page, widgetData) 335 | elif widgetData['type'] == 'Switch': 336 | i = SwitchWidget(page, widgetData) 337 | elif widgetData['type'] == 'Text': 338 | i = TextWidget(page, widgetData) 339 | elif widgetData['type'] == 'Video': 340 | i = VideoWidget(page, widgetData) 341 | elif widgetData['type'] == 'Mapview': 342 | i = MapViewWidget(page, widgetData) 343 | elif widgetData['type'] == 'Webview': 344 | i = WebViewWidget(page, widgetData) 345 | else: 346 | debugPrint(1, 'unknown widget type=%s, widgetId=%s' % (widgetData['type'], widgetData['widgetId'])) 347 | return None 348 | 349 | # add new instance to dict of all widgets 350 | self.widgets[i.widgetId] = i 351 | return i 352 | 353 | 354 | class Sitemap(object): 355 | """Python representative of an openHAB sitemap.""" 356 | 357 | def __init__(self, oh, sitemapData): 358 | self.oh = oh 359 | self.name = sitemapData['name'] 360 | self.label = sitemapData['label'] 361 | self.link = sitemapData['link'] 362 | self.page = None 363 | 364 | def load_page(self): 365 | """Load sitemap homepage from openHAB and create SitemapPage instance.""" 366 | result = self.oh.fetch_abs_json_url(self.link)[0] 367 | self.page = self.oh.create_page_class(self, result['homepage']) 368 | return self.page 369 | 370 | 371 | class Page(object): 372 | """Python representative of a page of widgets in openHAB. A page can be the homepage of a sitemap 373 | or the linked page of a group or text widget.""" 374 | 375 | def __init__(self, sitemap, pageData, prevPage=None): 376 | self.sitemap = sitemap 377 | self.prevPage = prevPage 378 | self.oh = sitemap.oh 379 | self.id_ = pageData['id'] 380 | self.attribs = Attributes('page_') 381 | self.proxies = [] 382 | self.widgets = [] 383 | self.atmos_id = None 384 | self.init(pageData) 385 | 386 | def set_proxy(self, proxy): 387 | self.proxies.append(weakref.ref(proxy)) 388 | changed = self.attribs.get_all() 389 | proxy.update(changed, set()) 390 | 391 | @update_proxy 392 | def init(self, pageData): 393 | self.attribs['link'] = pageData['link'] 394 | self.attribs['leaf'] = pageData['leaf'].lower() == 'true' 395 | x = split_label(pageData.get('title')) 396 | self.attribs['title'] = x[0] 397 | self.attribs['value'] = x[1] 398 | if 'widget' in pageData: 399 | self.create_all_widgets(as_array(pageData['widget'])) 400 | 401 | def create_all_widgets(self, widgets): 402 | # create list of all existing widget-ids in case of an update 403 | id_list = frozenset([w.widgetId for w in self.widgets]) 404 | for w in widgets: 405 | i = self.oh.create_widget_class(self, w) 406 | if i is not None and i.widgetId not in id_list: 407 | self.widgets.append(i) 408 | 409 | def get_page(self, headers=None): 410 | (pageData, headers) = self.oh.fetch_abs_json_url(self.attribs['link'], headers) 411 | self.init(pageData) 412 | self.atmos_id = headers.get('x-atmosphere-tracking-id') 413 | 414 | def get_page_blocked(self): 415 | headers = {'x-atmosphere-transport': 'long-polling'} 416 | if self.atmos_id is not None: 417 | headers['x-atmosphere-tracking-id'] = self.atmos_id 418 | self.get_page(headers) 419 | 420 | 421 | class WidgetBase(object): 422 | def __init__(self, page, widgetData): 423 | self.page = page 424 | self.oh = page.oh 425 | self.type_ = widgetData['type'] 426 | self.widgetId = widgetData['widgetId'] 427 | self.item = None 428 | self.attribs = Attributes('widget_') 429 | self.proxies = [] 430 | self.init(widgetData) 431 | 432 | @update_proxy 433 | def init(self, widgetData): 434 | x = split_label(widgetData.get('label')) 435 | self.attribs['label'] = x[0] 436 | self.attribs['value'] = x[1] 437 | if 'icon' in widgetData and widgetData['icon'] != 'none': 438 | self.attribs['icon'] = self.oh.resources['images'] + '/' + widgetData['icon'] + '.png' 439 | else: 440 | self.attribs['icon'] = None 441 | self.attribs['label_color'] = widgetData.get('labelcolor') 442 | self.attribs['value_color'] = widgetData.get('valuecolor') 443 | self.item = self.oh.create_item_class(widgetData.get('item')) 444 | 445 | def set_proxy(self, proxy): 446 | self.proxies.append(weakref.ref(proxy)) 447 | proxy.update(self.attribs.get_all(), set()) 448 | 449 | if self.item: 450 | self.item.set_proxy(proxy) 451 | 452 | 453 | class ColorPickerWidget(WidgetBase): 454 | def __init__(self, page, widgetData): 455 | super(ColorPickerWidget, self).__init__(page, widgetData) 456 | 457 | 458 | class ChartWidget(WidgetBase): 459 | def __init__(self, page, widgetData): 460 | super(ChartWidget, self).__init__(page, widgetData) 461 | 462 | @update_proxy 463 | def init(self, widgetData): 464 | super(ChartWidget, self).init(widgetData) 465 | self.attribs['service'] = widgetData.get('service') 466 | self.attribs['period'] = widgetData.get('period') 467 | self.attribs['refresh'] = int(widgetData.get('refresh', 0)) 468 | url = self.oh.resources['charts'] + '?' 469 | if self.item.type_ == 'GroupItem': 470 | url += 'groups=' + self.item.name 471 | if 'period' in self.attribs: 472 | url += '&period=' + self.attribs['period'] 473 | self.attribs['url'] = url 474 | 475 | 476 | class FrameWidget(WidgetBase): 477 | def __init__(self, page, widgetData): 478 | self.widgets = [] # assign widgets before calling super because this in turns calls init 479 | super(FrameWidget, self).__init__(page, widgetData) 480 | 481 | @update_proxy 482 | def init(self, widgetData): 483 | super(FrameWidget, self).init(widgetData) 484 | if 'widget' in widgetData: 485 | self.widgets = [] 486 | for w in as_array(widgetData['widget']): 487 | i = self.oh.create_widget_class(self.page, w) 488 | if i is not None: 489 | self.widgets.append(i) 490 | 491 | 492 | class GroupWidget(WidgetBase): 493 | def __init__(self, page, widgetData): 494 | super(GroupWidget, self).__init__(page, widgetData) 495 | 496 | @update_proxy 497 | def init(self, widgetData): 498 | super(GroupWidget, self).init(widgetData) 499 | if 'linkedPage' in widgetData: 500 | self.page = self.oh.create_page_class(self.page.sitemap, widgetData['linkedPage'], self.page) 501 | else: 502 | self.page = None 503 | 504 | 505 | 506 | class ImageWidget(WidgetBase): 507 | def __init__(self, page, widgetData): 508 | super(ImageWidget, self).__init__(page, widgetData) 509 | 510 | @update_proxy 511 | def init(self, widgetData): 512 | super(ImageWidget, self).init(widgetData) 513 | self.attribs['linkedPage'] = widgetData['linkedPage'] # don't create an extra openhab1.Page because not used so far 514 | self.attribs['url'] = widgetData['url'] 515 | self.attribs['refresh'] = int(widgetData.get('refresh', 0)) 516 | 517 | 518 | class SelectionWidget(WidgetBase): 519 | def __init__(self, page, widgetData): 520 | super(SelectionWidget, self).__init__(page, widgetData) 521 | 522 | @update_proxy 523 | def init(self, widgetData): 524 | super(SelectionWidget, self).init(widgetData) 525 | self.attribs['mapping'] = convert_mapping(widgetData.get('mapping')) 526 | if self.attribs['mapping'] and self.attribs['value'] is None and self.item.attribs['state'] is not None: 527 | self.attribs['value'] = self.attribs['mapping'].get(self.item.attribs['state']) 528 | 529 | 530 | class SetPointWidget(WidgetBase): 531 | def __init__(self, page, widgetData): 532 | super(SetPointWidget, self).__init__(page, widgetData) 533 | 534 | @update_proxy 535 | def init(self, widgetData): 536 | super(SetPointWidget, self).init(widgetData) 537 | self.attribs['min_value'] = Decimal(widgetData['minValue']) 538 | self.attribs['max_value'] = Decimal(widgetData['maxValue']) 539 | self.attribs['step'] = Decimal(widgetData['step']) 540 | 541 | 542 | class SliderWidget(WidgetBase): 543 | def __init__(self, page, widgetData): 544 | super(SliderWidget, self).__init__(page, widgetData) 545 | 546 | @update_proxy 547 | def init(self, widgetData): 548 | super(SliderWidget, self).init(widgetData) 549 | self.attribs['min_value'] = Decimal(0) 550 | self.attribs['max_value'] = Decimal(100) 551 | self.attribs['step'] = Decimal(1) 552 | self.attribs['send_frequency'] = int(widgetData.get('sendFrequency', 0)) 553 | self.attribs['switch_support'] = widgetData.get('switchSupport', '').lower() == 'true' 554 | 555 | 556 | class SwitchWidget(WidgetBase): 557 | def __init__(self, page, widgetData): 558 | super(SwitchWidget, self).__init__(page, widgetData) 559 | 560 | @update_proxy 561 | def init(self, widgetData): 562 | super(SwitchWidget, self).init(widgetData) 563 | self.attribs['mapping'] = convert_mapping(widgetData.get('mapping')) 564 | if self.attribs['mapping'] and self.attribs['value'] is None and self.item.attribs['state'] is not None: 565 | self.attribs['value'] = self.attribs['mapping'].get(self.item.attribs['state']) 566 | 567 | 568 | class TextWidget(WidgetBase): 569 | def __init__(self, page, widgetData): 570 | super(TextWidget, self).__init__(page, widgetData) 571 | 572 | @update_proxy 573 | def init(self, widgetData): 574 | super(TextWidget, self).init(widgetData) 575 | if 'linkedPage' in widgetData: 576 | self.page = self.oh.create_page_class(self.page.sitemap, widgetData['linkedPage'], self.page) 577 | else: 578 | self.page = None 579 | 580 | 581 | class VideoWidget(WidgetBase): 582 | def __init__(self, page, widgetData): 583 | super(VideoWidget, self).__init__(page, widgetData) 584 | 585 | @update_proxy 586 | def init(self, widgetData): 587 | super(VideoWidget, self).init(widgetData) 588 | self.attribs['url'] = widgetData['url'] 589 | self.attribs['encoding'] = widgetData.get('encoding') 590 | 591 | 592 | class MapViewWidget(WidgetBase): 593 | def __init__(self, page, widgetData): 594 | super(MapViewWidget, self).__init__(page, widgetData) 595 | 596 | @update_proxy 597 | def init(self, widgetData): 598 | super(MapViewWidget, self).init(widgetData) 599 | 600 | 601 | class WebViewWidget(WidgetBase): 602 | def __init__(self, page, widgetData): 603 | super(WebViewWidget, self).__init__(page, widgetData) 604 | 605 | @update_proxy 606 | def init(self, widgetData): 607 | super(WebViewWidget, self).init(widgetData) 608 | self.attribs['height'] = int(widgetData.get('height', 1)) 609 | self.attribs['url'] = widgetData['url'] 610 | 611 | 612 | class ItemBase(object): 613 | def __init__(self, oh, itemData): 614 | self.oh = oh 615 | self.name = itemData['name'] 616 | self.type_ = itemData['type'] 617 | self.link = itemData['link'] 618 | self.attribs = Attributes('item_') 619 | self.proxies = [] 620 | self.atmos_id = None # ID used for long polling 621 | self.init(itemData) 622 | 623 | def set_proxy(self, proxy): 624 | self.proxies.append(weakref.ref(proxy)) 625 | proxy.update(self.attribs.get_all(), set()) 626 | 627 | @update_proxy 628 | def init(self, itemData): 629 | self.attribs['state'] = self.state_from_string(itemData['state']) if 'state' in itemData else None 630 | 631 | def state_from_string(self, value): 632 | raise RuntimeError() 633 | 634 | def state_to_string(self, value): 635 | raise RuntimeError() 636 | 637 | def test_state_value(self, value): 638 | raise RuntimeError() 639 | 640 | @update_proxy 641 | def set_state(self, value): 642 | new_state = self.test_state_value(value) 643 | if self.attribs['state'] != new_state: 644 | self.attribs['state'] = new_state 645 | self.post_state() # send update to openHAB 646 | 647 | def send_command(self, value): 648 | """ post command to openHAB, used for actor items """ 649 | resp = requests.post(self.link, data=value, headers=self.oh.http_put_headers, proxies=self.oh.http_proxies) 650 | if resp.status_code != requests.codes.ok: 651 | resp.raise_for_status() 652 | 653 | def post_state(self): 654 | """ post state update to openHAB, used for sensor items """ 655 | resp = requests.put(self.link + '/state', data=self.state_to_string(self.attribs['state']), 656 | headers=self.oh.http_put_headers, proxies=self.oh.http_proxies) 657 | if resp.status_code != requests.codes.ok: 658 | resp.raise_for_status() 659 | 660 | def get_state(self, headers=None): 661 | (result, headers) = self.oh.fetch_abs_json_url(self.link, headers) 662 | if 'state' in result: 663 | self.init(result) 664 | self.atmos_id = headers.get('x-atmosphere-tracking-id') 665 | 666 | def get_state_blocked(self): 667 | headers = {'x-atmosphere-transport': 'long-polling'} 668 | if self.atmos_id is not None: 669 | headers['x-atmosphere-tracking-id'] = self.atmos_id 670 | self.get_state(headers) 671 | 672 | 673 | class CallItem(ItemBase): 674 | def __init__(self, oh, itemData): 675 | super(CallItem, self).__init__(oh, itemData) 676 | 677 | def state_from_string(self, value): 678 | if value is None or value in ('Uninitialized', 'Undefined'): 679 | return None 680 | else: 681 | return value 682 | 683 | def state_to_string(self, value): 684 | if value is None: 685 | return 'Undefined' 686 | else: 687 | return value 688 | 689 | def test_state_value(self, value): 690 | if not isinstance(value, str): 691 | raise TypeError() 692 | return value 693 | 694 | def cmd_call(self, value): 695 | if not isinstance(value, str): 696 | raise TypeError() 697 | self.send_command(value) 698 | 699 | 700 | class ColorItem(ItemBase): 701 | def __init__(self, oh, itemData): 702 | super(ColorItem, self).__init__(oh, itemData) 703 | 704 | def state_from_string(self, value): 705 | if value is None or value in ('Uninitialized', 'Undefined'): 706 | return None 707 | else: 708 | return map(lambda x: float(x), value.split(',', 3)) 709 | 710 | def state_to_string(self, value): 711 | if value is None: 712 | return 'Undefined' 713 | else: 714 | return ','.join(map(lambda x: str(x), value)) 715 | 716 | def test_state_value(self, value): 717 | if not isinstance(value, collections.Sequence): 718 | raise TypeError() 719 | return value 720 | 721 | def cmd_on(self): 722 | self.send_command('ON') 723 | 724 | def cmd_off(self): 725 | self.send_command('OFF') 726 | 727 | def cmd_increase(self): 728 | self.send_command('INCREASE') 729 | 730 | def cmd_decrease(self): 731 | self.send_command('DECREASE') 732 | 733 | @update_proxy 734 | def cmd_set_pct(self, value): 735 | if not isinstance(value, (int, float, Decimal)): 736 | raise TypeError() 737 | self.send_command(str(value)) 738 | #self.attribs['state'] = value 739 | 740 | @update_proxy 741 | def cmd_set_hsv(self, value): 742 | if not isinstance(value, collections.Sequence): 743 | raise TypeError() 744 | self.send_command(','.join(map(lambda x: str(x), value))) 745 | #self.attribs['state'] = value 746 | 747 | 748 | class ContactItem(ItemBase): 749 | def __init__(self, oh, itemData): 750 | super(ContactItem, self).__init__(oh, itemData) 751 | 752 | def state_from_string(self, value): 753 | if value is None or value in ('Uninitialized', 'Undefined'): 754 | return None 755 | elif value == 'OPEN': 756 | return True 757 | elif value == 'CLOSED': 758 | return False 759 | else: 760 | raise ValueError() 761 | 762 | def state_to_string(self, value): 763 | if value is None: 764 | return 'Undefined' 765 | elif value: 766 | return 'OPEN' 767 | else: 768 | return 'CLOSED' 769 | 770 | def test_state_value(self, value): 771 | if not isinstance(value, bool): 772 | raise TypeError() 773 | return value 774 | 775 | 776 | class DateTimeItem(ItemBase): 777 | def __init__(self, oh, itemData): 778 | super(DateTimeItem, self).__init__(oh, itemData) 779 | 780 | def state_from_string(self, value): 781 | if value is None or value in ('Uninitialized', 'Undefined'): 782 | return None 783 | else: 784 | # datetime.strptime is not available on Kodi, therefore this workaround 785 | # see also: http://forum.kodi.tv/showthread.php?tid=112916 786 | t = time.strptime(value, '%Y-%m-%dT%H:%M:%S') 787 | return datetime.datetime.fromtimestamp(time.mktime(t)) 788 | 789 | def state_to_string(self, value): 790 | if value is None: 791 | return 'Undefined' 792 | else: 793 | return value.isoformat() 794 | 795 | def test_state_value(self, value): 796 | if not isinstance(value, datetime.datetime): 797 | raise TypeError() 798 | return value 799 | 800 | 801 | class DimmerItem(ItemBase): 802 | def __init__(self, oh, itemData): 803 | super(DimmerItem, self).__init__(oh, itemData) 804 | 805 | def state_from_string(self, value): 806 | if value is None or value in ('Uninitialized', 'Undefined'): 807 | return None 808 | else: 809 | return Decimal(value) 810 | 811 | def state_to_string(self, value): 812 | if value is None: 813 | return 'Undefined' 814 | else: 815 | return str(value) 816 | 817 | def test_state_value(self, value): 818 | if not isinstance(value, (int, float, Decimal)): 819 | raise TypeError() 820 | return value 821 | 822 | @update_proxy 823 | def cmd_set(self, value): 824 | if not isinstance(value, (int, float, Decimal)): 825 | raise TypeError() 826 | self.send_command(str(value)) 827 | self.attribs['state'] = value 828 | 829 | def cmd_on(self): 830 | self.send_command('ON') 831 | 832 | def cmd_off(self): 833 | self.send_command('OFF') 834 | 835 | def cmd_increase(self): 836 | self.send_command('INCREASE') 837 | 838 | def cmd_decrease(self): 839 | self.send_command('DECREASE') 840 | 841 | def cmd_toggle(self): 842 | self.send_command('TOGGLE') 843 | 844 | 845 | class GroupItem(ItemBase): 846 | def __init__(self, oh, itemData): 847 | super(GroupItem, self).__init__(oh, itemData) 848 | 849 | def state_from_string(self, value): 850 | if value is None or value in ('Uninitialized', 'Undefined'): 851 | return None 852 | else: 853 | return value 854 | 855 | def cmd_set(self, value): 856 | self.send_command(str(value)) 857 | 858 | 859 | class LocationItem(ItemBase): 860 | def __init__(self, oh, itemData): 861 | super(LocationItem, self).__init__(oh, itemData) 862 | 863 | def state_from_string(self, value): 864 | if value is None or value in ('Uninitialized', 'Undefined'): 865 | return None 866 | else: 867 | return value 868 | 869 | def state_to_string(self, value): 870 | if value is None: 871 | return 'Undefined' 872 | else: 873 | return value 874 | 875 | def test_state_value(self, value): 876 | if not isinstance(value, str): 877 | raise TypeError() 878 | return value 879 | 880 | 881 | class NumberItem(ItemBase): 882 | def __init__(self, oh, itemData): 883 | super(NumberItem, self).__init__(oh, itemData) 884 | 885 | def state_from_string(self, value): 886 | if value is None or value in ('Uninitialized', 'Undefined'): 887 | return None 888 | else: 889 | return Decimal(value) 890 | 891 | def state_to_string(self, value): 892 | if value is None: 893 | return 'Undefined' 894 | else: 895 | return str(value) 896 | 897 | def test_state_value(self, value): 898 | if not isinstance(value, (int, float, Decimal)): 899 | raise TypeError() 900 | return value 901 | 902 | @update_proxy 903 | def cmd_set(self, value): 904 | if not isinstance(value, (int, float, Decimal)): 905 | raise TypeError() 906 | self.send_command(str(value)) 907 | self.attribs['state'] = value 908 | 909 | 910 | class RollerShutterItem(ItemBase): 911 | def __init__(self, oh, itemData): 912 | super(RollerShutterItem, self).__init__(oh, itemData) 913 | 914 | def state_from_string(self, value): 915 | if value is None or value in ('Uninitialized', 'Undefined'): 916 | return None 917 | else: 918 | return Decimal(value) 919 | 920 | def state_to_string(self, value): 921 | if value is None: 922 | return 'Undefined' 923 | # elif isinstance(value, str): 924 | # return value 925 | else: 926 | return str(value) 927 | 928 | def test_state_value(self, value): 929 | # if value not in ('UP', 'DOWN') and not isinstance(value, (int, float)): 930 | if not isinstance(value, (int, float, Decimal)): 931 | raise TypeError() 932 | return value 933 | 934 | @update_proxy 935 | def cmd_set(self, value): 936 | if not isinstance(value, (int, float, Decimal)): 937 | raise TypeError() 938 | self.send_command(str(value)) 939 | self.attribs['state'] = value 940 | 941 | def cmd_stop(self): 942 | self.send_command('STOP') 943 | 944 | def cmd_move(self): 945 | self.send_command('MOVE') 946 | 947 | def cmd_up(self): 948 | self.send_command('UP') 949 | 950 | def cmd_down(self): 951 | self.send_command('DOWN') 952 | 953 | def cmd_toggle(self): 954 | self.send_command('TOGGLE') 955 | 956 | 957 | class StringItem(ItemBase): 958 | def __init__(self, oh, itemData): 959 | super(StringItem, self).__init__(oh, itemData) 960 | 961 | def state_from_string(self, value): 962 | if value is None or value in ('Uninitialized', 'Undefined'): # REVISIT: what is the string for an uninitialized string item? 963 | return None 964 | else: 965 | return value 966 | 967 | def state_to_string(self, value): 968 | if value is None: 969 | return 'Undefined' # REVISIT: what is the string for an undefined string item? 970 | else: 971 | return value 972 | 973 | def test_state_value(self, value): 974 | if not isinstance(value, str): 975 | raise TypeError() 976 | return value 977 | 978 | @update_proxy 979 | def cmd_set(self, value): 980 | if not isinstance(value, str): 981 | raise TypeError() 982 | self.send_command(value) 983 | self.attribs['state'] = value 984 | 985 | 986 | class SwitchItem(ItemBase): 987 | def __init__(self, oh, itemData): 988 | super(SwitchItem, self).__init__(oh, itemData) 989 | 990 | def state_from_string(self, value): 991 | if value is None or value in ('Uninitialized', 'Undefined'): 992 | return None 993 | elif value == 'ON': 994 | return True 995 | elif value == 'OFF': 996 | return False 997 | else: 998 | raise ValueError 999 | 1000 | def state_to_string(self, value): 1001 | if value is None: 1002 | return 'Undefined' 1003 | elif value: 1004 | return 'ON' 1005 | else: 1006 | return 'OFF' 1007 | 1008 | def test_state_value(self, value): 1009 | if not isinstance(value, bool): 1010 | raise TypeError() 1011 | return value 1012 | 1013 | @update_proxy 1014 | def cmd_set(self, value): 1015 | if not isinstance(value, bool): 1016 | raise TypeError() 1017 | self.send_command(self.state_to_string(value)) 1018 | self.attribs['state'] = value 1019 | 1020 | @update_proxy 1021 | def cmd_on(self): 1022 | self.send_command('ON') 1023 | self.attribs['state'] = True 1024 | 1025 | @update_proxy 1026 | def cmd_off(self): 1027 | self.send_command('OFF') 1028 | self.attribs['state'] = False 1029 | 1030 | @update_proxy 1031 | def cmd_toggle(self): 1032 | self.send_command('TOGGLE') 1033 | self.attribs['state'] = not self.attribs['state'] 1034 | 1035 | 1036 | if __name__ == '__main__': 1037 | oh = Server('localhost') 1038 | oh.poll_pages = True 1039 | oh.load_sitemaps() 1040 | homepage = oh.sitemaps['demo'].load_page() 1041 | -------------------------------------------------------------------------------- /resources/lib/openhab2.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import base64 4 | import collections 5 | try: # OrderedDict is new in python 2.7 6 | from collections import OrderedDict as OrderedDict 7 | except: 8 | from ordereddict import OrderedDict 9 | import datetime 10 | import time 11 | import re 12 | import requests 13 | import threading 14 | import weakref 15 | from decimal import Decimal 16 | from decimal import InvalidOperation 17 | from debugout import debugPrint 18 | 19 | 20 | def split_label(label): 21 | """Split a label + value line into label and value, e.g. 22 | "Outside Temperature [9,0 °C]" 23 | -> label = "Outside Temperature" 24 | -> value = "9,0 °C" 25 | """ 26 | if label is None: 27 | return None, None 28 | 29 | m = re.search(r"\s*\[([^\]]*)\]$", label) 30 | if m is not None: 31 | return label[:m.start()], m.groups()[0] 32 | else: 33 | return label, None 34 | 35 | 36 | def as_array(x): 37 | """openHAB doesn't return an array of objects in the JSON rest API if there is only 1 entry in 38 | the array. Therefore this functions converts single entries into an array.""" 39 | if isinstance(x, list): 40 | return x 41 | else: 42 | return [x] 43 | 44 | 45 | def convert_mapping(raw): 46 | """Convert openHAB mapping into a python ordered dictionary""" 47 | if raw is None: 48 | return None 49 | else: 50 | mapping = OrderedDict() 51 | for l in as_array(raw): 52 | try: 53 | mapping[Decimal(l['command'])] = l['label'] 54 | except InvalidOperation: 55 | mapping[l['command']] = l['label'] 56 | return mapping 57 | 58 | 59 | def update_proxy(func): 60 | """Decorator function to update all assigned proxies if any attribute changes""" 61 | def func_wrapper(self, *args, **kwargs): 62 | func(self, *args, **kwargs) 63 | updates = self.attribs.get_changes() 64 | for p in self.proxies: 65 | ref = p() 66 | if ref: 67 | ref.update(*updates) 68 | 69 | return func_wrapper 70 | 71 | 72 | class EmptyResponseError(Exception): 73 | """Exception for empty openHAB responses""" 74 | pass 75 | 76 | 77 | def poll_page_thread(page): 78 | """Thread function to long-poll openHAB pages""" 79 | while True: 80 | try: 81 | page.get_page_blocked() 82 | except EmptyResponseError: 83 | # openHAB server return an empty reponse (typically 5 minutes after long-poll request started) 84 | # ==> try again if server connection is still alive 85 | debugPrint(5, 'poll_page_thread: empty response for page %s' % page.id_) 86 | except requests.exceptions.ReadTimeout as e: 87 | # HTTP request timed out 88 | # ==> try again if server connection is still alive 89 | debugPrint(5, 'poll_page_thread: %s for page %s' % (repr(e), page.id_)) 90 | except requests.exceptions.HTTPError as e: 91 | debugPrint(1, 'poll_page_thread: %s for page %s' % (repr(e), page.id_)) 92 | except requests.exceptions.ConnectTimeout as e: 93 | debugPrint(1, 'poll_page_thread: %s for page %s' % (repr(e), page.id_)) 94 | except requests.exceptions.ConnectionError as e: 95 | # openHAB server terminated the connection 96 | # ==> execute terminate callback and close window 97 | debugPrint(5, 'poll_page_thread: %s for page %s' % (repr(e), page.id_)) 98 | page.oh.terminate() 99 | 100 | if not page.oh.alive: 101 | # exit thread if openHAB server ist not alive any more""" 102 | return 103 | 104 | 105 | class Attributes(collections.MutableMapping): 106 | """A dictionary that tracks changes and stores new/changed key/value + deleted keys""" 107 | 108 | def __init__(self, prefix, *args, **kwargs): 109 | self.prefix = prefix 110 | self.changed = dict() # stores new and changed key/values 111 | self.deleted = set() # stores deleted keys 112 | self.store = dict() 113 | self.update(dict(*args, **kwargs)) # use the free update to set keys 114 | 115 | def __getitem__(self, key): 116 | return self.store[key] 117 | 118 | def __setitem__(self, key, value): 119 | try: 120 | if self.store[key] == value: 121 | return # skip further processing because value unchanged 122 | except KeyError: 123 | pass 124 | 125 | self.store[key] = value 126 | 127 | xkey = self.prefix + key 128 | self.changed[xkey] = value 129 | try: 130 | self.deleted.remove(xkey) 131 | except KeyError: 132 | pass 133 | 134 | def __delitem__(self, key): 135 | del self.store[key] 136 | 137 | xkey = self.prefix + key 138 | self.deleted.add(xkey) 139 | try: 140 | del self.changed[xkey] 141 | except KeyError: 142 | pass 143 | 144 | def __iter__(self): 145 | return iter(self.store) 146 | 147 | def __len__(self): 148 | return len(self.store) 149 | 150 | def get_changes(self): 151 | result = (self.changed, self.deleted) 152 | self.changed = dict() 153 | self.deleted = set() 154 | return result 155 | 156 | def get_all(self): 157 | changed = dict() 158 | for key, value in self.store.iteritems(): 159 | changed[self.prefix + key] = value 160 | return changed 161 | 162 | 163 | class Server(object): 164 | """Python representative of a running openHAB instance.""" 165 | def __init__(self, host='localhost', port='8080'): 166 | self.host = host 167 | self.port = port 168 | path = 'http://%s:%s/' % (host, port) 169 | self.resources = {'items': path + 'rest/items', 170 | 'sitemaps': path + 'rest/sitemaps', 171 | 'images': path + 'images', 172 | 'charts': path + 'chart'} 173 | self.sitemaps = {} 174 | self.pages = {} 175 | self.items = {} 176 | self.widgets = {} 177 | self.http_put_headers = {'content-type': 'text/plain'} 178 | self.http_get_headers = {'accept': 'application/json'} 179 | self.poll_pages = False # True = start thread for every new page to long-poll changes 180 | self.http_proxies = None # proxy for requests library 181 | self.alive = True 182 | self.terminate_callback = [] 183 | 184 | def terminate(self): 185 | self.alive = False 186 | for cb in self.terminate_callback: 187 | cb(self) 188 | 189 | def set_basic_auth(self, username, password): 190 | auth = 'Basic %s' % base64.b64encode('%s:%s' % (username, password)) 191 | self.http_put_headers['authorization'] = auth 192 | self.http_get_headers['authorization'] = auth 193 | 194 | def set_proxy(self, proxy): 195 | if proxy == 'system': 196 | self.http_proxies = None # use system proxies 197 | elif proxy == 'none': 198 | self.http_proxies = {'http': '', 'https': ''} # use no proxy 199 | else: 200 | self.http_proxies = {'http': proxy, 'https': proxy} 201 | 202 | def fetch_abs_json_url(self, url, extra_headers=None): 203 | """Fetch url from openHAB server and convert data from json to Python data structures.""" 204 | headers = self.http_get_headers 205 | if extra_headers is not None: 206 | headers.update(extra_headers) 207 | debugPrint(5, 'fetching json url=%s, headers=%s' % (url, repr(headers))) 208 | resp = requests.get(url, headers=headers, proxies=self.http_proxies) 209 | debugPrint(5, 'response for url=%s, text=%s, headers=%s' % (url, resp.text, resp.headers)) 210 | if resp.status_code != requests.codes.ok: 211 | resp.raise_for_status() 212 | if resp.text == '': 213 | # openHAB returns an empty response after 5 minutes if long-polling is enabled 214 | raise EmptyResponseError 215 | return resp.json(), resp.headers 216 | 217 | def fetch_rel_json_url(self, name, headers=None): 218 | """Fetch url from openHAB server and convert data from json to Python data structures.""" 219 | return self.fetch_abs_json_url('http://%s:%s/rest%s' % (self.host, self.port, name), headers) 220 | 221 | def load_resources(self): 222 | """Fetch resources (http::/rest) from openHAB.""" 223 | self.resources = {} 224 | result = self.fetch_rel_json_url('', self.http_get_headers)[0] 225 | for item in as_array(result['link']): 226 | self.resources[item['@type']] = item['$'] 227 | return self.resources 228 | 229 | def load_sitemaps(self): 230 | """Load sitemaps list from openHAB and create Python instances for every sitemap. 231 | The created sitemaps are without content so far.""" 232 | self.sitemaps = {} 233 | if not self.resources: 234 | self.load_resources() 235 | result = self.fetch_abs_json_url(self.resources['sitemaps'])[0] 236 | for i in as_array(result): 237 | self.sitemaps[i['name']] = Sitemap(self, i) 238 | return self.sitemaps 239 | 240 | def load_items(self): 241 | """Load items from openHAB and create python instances for every openHAB item.""" 242 | self.items = {} 243 | if not self.resources: 244 | self.load_resources() 245 | result = self.fetch_abs_json_url(self.resources['items'])[0] 246 | for itemData in as_array(result['item']): 247 | self.create_item_class(itemData) 248 | return self.items 249 | 250 | def create_page_class(self, sitemap, pageData, prevPage=None): 251 | """Create an openHAB page instance from the given hash properties.""" 252 | if pageData is None: 253 | return None 254 | elif pageData['id'] in self.pages: # test if page already exists 255 | i = self.pages[pageData['id']] 256 | i.init(pageData) 257 | return i 258 | else: 259 | i = Page(sitemap, pageData, prevPage) 260 | 261 | if self.poll_pages: 262 | # start a new thread for every page that polls updates 263 | t = threading.Thread(target=poll_page_thread, args=(i,)) 264 | t.daemon = True 265 | t.start() 266 | 267 | # add new instance to dict of all pages 268 | self.pages[i.id_] = i 269 | return i 270 | 271 | def create_item_class(self, itemData): 272 | """Create an openHAB item instance from the given hash properties.""" 273 | if itemData is None: 274 | return None 275 | elif itemData['name'] in self.items: # test if item already exists 276 | i = self.items[itemData['name']] 277 | i.init(itemData) 278 | return i 279 | else: 280 | if itemData['type'] == 'Call': 281 | i = CallItem(self, itemData) 282 | elif itemData['type'] == 'Color': 283 | i = ColorItem(self, itemData) 284 | elif itemData['type'] == 'Contact': 285 | i = ContactItem(self, itemData) 286 | elif itemData['type'] == 'DateTime': 287 | i = DateTimeItem(self, itemData) 288 | elif itemData['type'] == 'Dimmer': 289 | i = DimmerItem(self, itemData) 290 | elif itemData['type'] == 'Group': 291 | i = GroupItem(self, itemData) 292 | elif itemData['type'] == 'Location': 293 | i = LocationItem(self, itemData) 294 | elif itemData['type'] == 'Number': 295 | i = NumberItem(self, itemData) 296 | elif itemData['type'] == 'String': 297 | i = StringItem(self, itemData) 298 | elif itemData['type'] == 'Switch': 299 | i = SwitchItem(self, itemData) 300 | elif itemData['type'] == 'Rollershutter': 301 | i = RollerShutterItem(self, itemData) 302 | else: 303 | debugPrint(1, 'unknown item type=%s, name=%s' % (itemData['type'], itemData['name'])) 304 | return None 305 | 306 | # add new instance to dict of all items 307 | self.items[i.name] = i 308 | return i 309 | 310 | def create_widget_class(self, page, widgetData): 311 | """ create an openHAB widget instance from the given has properties """ 312 | if widgetData is None: 313 | return None 314 | elif widgetData['widgetId'] in self.widgets: # test if widget already exists 315 | i = self.widgets[widgetData['widgetId']] 316 | i.init(widgetData) 317 | return i 318 | else: 319 | if widgetData['type'] == 'Colorpicker': 320 | i = ColorPickerWidget(page, widgetData) 321 | elif widgetData['type'] == 'Chart': 322 | i = ChartWidget(page, widgetData) 323 | elif widgetData['type'] == 'Frame': 324 | i = FrameWidget(page, widgetData) 325 | elif widgetData['type'] == 'Group': 326 | i = GroupWidget(page, widgetData) 327 | elif widgetData['type'] == 'Image': 328 | i = ImageWidget(page, widgetData) 329 | elif widgetData['type'] == 'Selection': 330 | i = SelectionWidget(page, widgetData) 331 | elif widgetData['type'] == 'Setpoint': 332 | i = SetPointWidget(page, widgetData) 333 | elif widgetData['type'] == 'Slider': 334 | i = SliderWidget(page, widgetData) 335 | elif widgetData['type'] == 'Switch': 336 | i = SwitchWidget(page, widgetData) 337 | elif widgetData['type'] == 'Text': 338 | i = TextWidget(page, widgetData) 339 | elif widgetData['type'] == 'Video': 340 | i = VideoWidget(page, widgetData) 341 | elif widgetData['type'] == 'Mapview': 342 | i = MapViewWidget(page, widgetData) 343 | elif widgetData['type'] == 'Webview': 344 | i = WebViewWidget(page, widgetData) 345 | else: 346 | debugPrint(1, 'unknown widget type=%s, widgetId=%s' % (widgetData['type'], widgetData['widgetId'])) 347 | return None 348 | 349 | # add new instance to dict of all widgets 350 | self.widgets[i.widgetId] = i 351 | return i 352 | 353 | 354 | class Sitemap(object): 355 | """Python representative of an openHAB sitemap.""" 356 | 357 | def __init__(self, oh, sitemapData): 358 | self.oh = oh 359 | self.name = sitemapData['name'] 360 | self.label = sitemapData['label'] 361 | self.link = sitemapData['link'] 362 | self.page = None 363 | 364 | def load_page(self): 365 | """Load sitemap homepage from openHAB and create SitemapPage instance.""" 366 | result = self.oh.fetch_abs_json_url(self.link)[0] 367 | self.page = self.oh.create_page_class(self, result['homepage']) 368 | return self.page 369 | 370 | 371 | class Page(object): 372 | """Python representative of a page of widgets in openHAB. A page can be the homepage of a sitemap 373 | or the linked page of a group or text widget.""" 374 | 375 | def __init__(self, sitemap, pageData, prevPage=None): 376 | self.sitemap = sitemap 377 | self.prevPage = prevPage 378 | self.oh = sitemap.oh 379 | self.id_ = pageData['id'] 380 | self.attribs = Attributes('page_') 381 | self.proxies = [] 382 | self.widgets = [] 383 | self.atmos_id = None 384 | self.init(pageData) 385 | 386 | def set_proxy(self, proxy): 387 | self.proxies.append(weakref.ref(proxy)) 388 | changed = self.attribs.get_all() 389 | proxy.update(changed, set()) 390 | 391 | @update_proxy 392 | def init(self, pageData): 393 | self.attribs['link'] = pageData['link'] 394 | self.attribs['leaf'] = pageData['leaf'] 395 | x = split_label(pageData.get('title')) 396 | self.attribs['title'] = x[0] 397 | self.attribs['value'] = x[1] 398 | if 'widgets' in pageData: 399 | self.create_all_widgets(as_array(pageData['widgets'])) 400 | 401 | def create_all_widgets(self, widgets): 402 | # create list of all existing widget-ids in case of an update 403 | id_list = frozenset([w.widgetId for w in self.widgets]) 404 | for w in widgets: 405 | i = self.oh.create_widget_class(self, w) 406 | if i is not None and i.widgetId not in id_list: 407 | self.widgets.append(i) 408 | 409 | def get_page(self, headers=None): 410 | (pageData, headers) = self.oh.fetch_abs_json_url(self.attribs['link'], headers) 411 | self.init(pageData) 412 | self.atmos_id = headers.get('x-atmosphere-tracking-id') 413 | 414 | def get_page_blocked(self): 415 | headers = {'x-atmosphere-transport': 'long-polling'} 416 | if self.atmos_id is not None: 417 | headers['x-atmosphere-tracking-id'] = self.atmos_id 418 | self.get_page(headers) 419 | 420 | 421 | class WidgetBase(object): 422 | def __init__(self, page, widgetData): 423 | self.page = page 424 | self.oh = page.oh 425 | self.type_ = widgetData['type'] 426 | self.widgetId = widgetData['widgetId'] 427 | self.item = None 428 | self.attribs = Attributes('widget_') 429 | self.proxies = [] 430 | self.init(widgetData) 431 | 432 | @update_proxy 433 | def init(self, widgetData): 434 | x = split_label(widgetData.get('label')) 435 | self.attribs['label'] = x[0] 436 | self.attribs['value'] = x[1] 437 | if 'icon' in widgetData and widgetData['icon'] != 'none': 438 | self.attribs['icon'] = self.oh.resources['images'] + '/' + widgetData['icon'] + '.png' 439 | else: 440 | self.attribs['icon'] = None 441 | self.attribs['label_color'] = widgetData.get('labelcolor') 442 | self.attribs['value_color'] = widgetData.get('valuecolor') 443 | self.item = self.oh.create_item_class(widgetData.get('item')) 444 | 445 | def set_proxy(self, proxy): 446 | self.proxies.append(weakref.ref(proxy)) 447 | proxy.update(self.attribs.get_all(), set()) 448 | 449 | if self.item: 450 | self.item.set_proxy(proxy) 451 | 452 | 453 | class ColorPickerWidget(WidgetBase): 454 | def __init__(self, page, widgetData): 455 | super(ColorPickerWidget, self).__init__(page, widgetData) 456 | 457 | 458 | class ChartWidget(WidgetBase): 459 | def __init__(self, page, widgetData): 460 | super(ChartWidget, self).__init__(page, widgetData) 461 | 462 | @update_proxy 463 | def init(self, widgetData): 464 | super(ChartWidget, self).init(widgetData) 465 | self.attribs['service'] = widgetData.get('service') 466 | self.attribs['period'] = widgetData.get('period') 467 | self.attribs['refresh'] = int(widgetData.get('refresh', 0)) 468 | url = self.oh.resources['charts'] + '?' 469 | if self.item.type_ == 'GroupItem': 470 | url += 'groups=' + self.item.name 471 | if 'period' in self.attribs: 472 | url += '&period=' + self.attribs['period'] 473 | self.attribs['url'] = url 474 | 475 | 476 | class FrameWidget(WidgetBase): 477 | def __init__(self, page, widgetData): 478 | self.widgets = [] # assign widgets before calling super because this in turns calls init 479 | super(FrameWidget, self).__init__(page, widgetData) 480 | 481 | @update_proxy 482 | def init(self, widgetData): 483 | super(FrameWidget, self).init(widgetData) 484 | if 'widgets' in widgetData: 485 | self.widgets = [] 486 | for w in as_array(widgetData['widgets']): 487 | i = self.oh.create_widget_class(self.page, w) 488 | if i is not None: 489 | self.widgets.append(i) 490 | 491 | 492 | class GroupWidget(WidgetBase): 493 | def __init__(self, page, widgetData): 494 | super(GroupWidget, self).__init__(page, widgetData) 495 | 496 | @update_proxy 497 | def init(self, widgetData): 498 | super(GroupWidget, self).init(widgetData) 499 | if 'linkedPage' in widgetData: 500 | self.page = self.oh.create_page_class(self.page.sitemap, widgetData['linkedPage'], self.page) 501 | else: 502 | self.page = None 503 | 504 | 505 | 506 | class ImageWidget(WidgetBase): 507 | def __init__(self, page, widgetData): 508 | super(ImageWidget, self).__init__(page, widgetData) 509 | 510 | @update_proxy 511 | def init(self, widgetData): 512 | super(ImageWidget, self).init(widgetData) 513 | if 'linkedPage' in widgetData: 514 | self.attribs['linkedPage'] = widgetData['linkedPage'] # don't create an extra Page because not used so far 515 | else: 516 | self.attribs['linkedPage'] = None 517 | self.attribs['url'] = widgetData['url'] 518 | self.attribs['refresh'] = int(widgetData.get('refresh', 0)) 519 | 520 | 521 | class SelectionWidget(WidgetBase): 522 | def __init__(self, page, widgetData): 523 | super(SelectionWidget, self).__init__(page, widgetData) 524 | 525 | @update_proxy 526 | def init(self, widgetData): 527 | super(SelectionWidget, self).init(widgetData) 528 | self.attribs['mapping'] = convert_mapping(widgetData.get('mappings')) 529 | if self.attribs['mapping'] and self.attribs['value'] is None and self.item.attribs['state'] is not None: 530 | self.attribs['value'] = self.attribs['mapping'].get(self.item.attribs['state']) 531 | 532 | 533 | class SetPointWidget(WidgetBase): 534 | def __init__(self, page, widgetData): 535 | super(SetPointWidget, self).__init__(page, widgetData) 536 | 537 | @update_proxy 538 | def init(self, widgetData): 539 | super(SetPointWidget, self).init(widgetData) 540 | self.attribs['min_value'] = Decimal(widgetData['minValue']) 541 | self.attribs['max_value'] = Decimal(widgetData['maxValue']) 542 | self.attribs['step'] = Decimal(widgetData['step']) 543 | 544 | 545 | class SliderWidget(WidgetBase): 546 | def __init__(self, page, widgetData): 547 | super(SliderWidget, self).__init__(page, widgetData) 548 | 549 | @update_proxy 550 | def init(self, widgetData): 551 | super(SliderWidget, self).init(widgetData) 552 | self.attribs['min_value'] = Decimal(0) 553 | self.attribs['max_value'] = Decimal(100) 554 | self.attribs['step'] = Decimal(1) 555 | self.attribs['send_frequency'] = int(widgetData.get('sendFrequency', 0)) 556 | self.attribs['switch_support'] = widgetData.get('switchSupport', False) 557 | 558 | 559 | class SwitchWidget(WidgetBase): 560 | def __init__(self, page, widgetData): 561 | super(SwitchWidget, self).__init__(page, widgetData) 562 | 563 | @update_proxy 564 | def init(self, widgetData): 565 | super(SwitchWidget, self).init(widgetData) 566 | self.attribs['mapping'] = convert_mapping(widgetData.get('mappings')) 567 | if self.attribs['mapping'] and self.attribs['value'] is None and self.item.attribs['state'] is not None: 568 | self.attribs['value'] = self.attribs['mapping'].get(self.item.attribs['state']) 569 | 570 | 571 | class TextWidget(WidgetBase): 572 | def __init__(self, page, widgetData): 573 | super(TextWidget, self).__init__(page, widgetData) 574 | 575 | @update_proxy 576 | def init(self, widgetData): 577 | super(TextWidget, self).init(widgetData) 578 | if 'linkedPage' in widgetData: 579 | self.page = self.oh.create_page_class(self.page.sitemap, widgetData['linkedPage'], self.page) 580 | else: 581 | self.page = None 582 | 583 | 584 | class VideoWidget(WidgetBase): 585 | def __init__(self, page, widgetData): 586 | super(VideoWidget, self).__init__(page, widgetData) 587 | 588 | @update_proxy 589 | def init(self, widgetData): 590 | super(VideoWidget, self).init(widgetData) 591 | self.attribs['url'] = widgetData['url'] 592 | self.attribs['encoding'] = widgetData.get('encoding') 593 | 594 | 595 | class MapViewWidget(WidgetBase): 596 | def __init__(self, page, widgetData): 597 | super(MapViewWidget, self).__init__(page, widgetData) 598 | 599 | @update_proxy 600 | def init(self, widgetData): 601 | super(MapViewWidget, self).init(widgetData) 602 | 603 | 604 | class WebViewWidget(WidgetBase): 605 | def __init__(self, page, widgetData): 606 | super(WebViewWidget, self).__init__(page, widgetData) 607 | 608 | @update_proxy 609 | def init(self, widgetData): 610 | super(WebViewWidget, self).init(widgetData) 611 | self.attribs['height'] = int(widgetData.get('height', 1)) 612 | self.attribs['url'] = widgetData['url'] 613 | 614 | 615 | class ItemBase(object): 616 | def __init__(self, oh, itemData): 617 | self.oh = oh 618 | self.name = itemData['name'] 619 | self.type_ = itemData['type'] 620 | self.link = itemData['link'] 621 | self.attribs = Attributes('item_') 622 | self.proxies = [] 623 | self.atmos_id = None # ID used for long polling 624 | self.init(itemData) 625 | 626 | def set_proxy(self, proxy): 627 | self.proxies.append(weakref.ref(proxy)) 628 | proxy.update(self.attribs.get_all(), set()) 629 | 630 | @update_proxy 631 | def init(self, itemData): 632 | self.attribs['state'] = self.state_from_string(itemData['state']) if 'state' in itemData else None 633 | 634 | def state_from_string(self, value): 635 | raise RuntimeError() 636 | 637 | def state_to_string(self, value): 638 | raise RuntimeError() 639 | 640 | def test_state_value(self, value): 641 | raise RuntimeError() 642 | 643 | @update_proxy 644 | def set_state(self, value): 645 | new_state = self.test_state_value(value) 646 | if self.attribs['state'] != new_state: 647 | self.attribs['state'] = new_state 648 | self.post_state() # send update to openHAB 649 | 650 | def send_command(self, value): 651 | """ post command to openHAB, used for actor items """ 652 | resp = requests.post(self.link, data=value, headers=self.oh.http_put_headers, proxies=self.oh.http_proxies) 653 | if resp.status_code != requests.codes.ok: 654 | resp.raise_for_status() 655 | 656 | def post_state(self): 657 | """ post state update to openHAB, used for sensor items """ 658 | resp = requests.put(self.link + '/state', data=self.state_to_string(self.attribs['state']), 659 | headers=self.oh.http_put_headers, proxies=self.oh.http_proxies) 660 | if resp.status_code != requests.codes.ok: 661 | resp.raise_for_status() 662 | 663 | def get_state(self, headers=None): 664 | (result, headers) = self.oh.fetch_abs_json_url(self.link, headers) 665 | if 'state' in result: 666 | self.init(result) 667 | self.atmos_id = headers.get('x-atmosphere-tracking-id') 668 | 669 | def get_state_blocked(self): 670 | headers = {'x-atmosphere-transport': 'long-polling'} 671 | if self.atmos_id is not None: 672 | headers['x-atmosphere-tracking-id'] = self.atmos_id 673 | self.get_state(headers) 674 | 675 | 676 | class CallItem(ItemBase): 677 | def __init__(self, oh, itemData): 678 | super(CallItem, self).__init__(oh, itemData) 679 | 680 | def state_from_string(self, value): 681 | if value is None or value in ('NULL', 'UNDEF'): 682 | return None 683 | else: 684 | return value 685 | 686 | def state_to_string(self, value): 687 | if value is None: 688 | return 'UNDEF' 689 | else: 690 | return value 691 | 692 | def test_state_value(self, value): 693 | if not isinstance(value, str): 694 | raise TypeError() 695 | return value 696 | 697 | def cmd_call(self, value): 698 | if not isinstance(value, str): 699 | raise TypeError() 700 | self.send_command(value) 701 | 702 | 703 | class ColorItem(ItemBase): 704 | def __init__(self, oh, itemData): 705 | super(ColorItem, self).__init__(oh, itemData) 706 | 707 | def state_from_string(self, value): 708 | if value is None or value in ('NULL', 'UNDEF'): 709 | return None 710 | else: 711 | return map(lambda x: float(x), value.split(',', 3)) 712 | 713 | def state_to_string(self, value): 714 | if value is None: 715 | return 'UNDEF' 716 | else: 717 | return ','.join(map(lambda x: str(x), value)) 718 | 719 | def test_state_value(self, value): 720 | if not isinstance(value, collections.Sequence): 721 | raise TypeError() 722 | return value 723 | 724 | def cmd_on(self): 725 | self.send_command('ON') 726 | 727 | def cmd_off(self): 728 | self.send_command('OFF') 729 | 730 | def cmd_increase(self): 731 | self.send_command('INCREASE') 732 | 733 | def cmd_decrease(self): 734 | self.send_command('DECREASE') 735 | 736 | @update_proxy 737 | def cmd_set_pct(self, value): 738 | if not isinstance(value, (int, float, Decimal)): 739 | raise TypeError() 740 | self.send_command(str(value)) 741 | #self.attribs['state'] = value 742 | 743 | @update_proxy 744 | def cmd_set_hsv(self, value): 745 | if not isinstance(value, collections.Sequence): 746 | raise TypeError() 747 | self.send_command(','.join(map(lambda x: str(x), value))) 748 | #self.attribs['state'] = value 749 | 750 | 751 | class ContactItem(ItemBase): 752 | def __init__(self, oh, itemData): 753 | super(ContactItem, self).__init__(oh, itemData) 754 | 755 | def state_from_string(self, value): 756 | if value is None or value in ('NULL', 'UNDEF'): 757 | return None 758 | elif value == 'open': 759 | return True 760 | elif value == 'closed': 761 | return False 762 | else: 763 | raise ValueError() 764 | 765 | def state_to_string(self, value): 766 | if value is None: 767 | return 'UNDEF' 768 | elif value: 769 | return 'open' 770 | else: 771 | return 'closed' 772 | 773 | def test_state_value(self, value): 774 | if not isinstance(value, bool): 775 | raise TypeError() 776 | return value 777 | 778 | 779 | class DateTimeItem(ItemBase): 780 | def __init__(self, oh, itemData): 781 | super(DateTimeItem, self).__init__(oh, itemData) 782 | 783 | def state_from_string(self, value): 784 | if value is None or value in ('NULL', 'UNDEF'): 785 | return None 786 | else: 787 | # datetime.strptime is not available on Kodi, therefore this workaround 788 | # see also: http://forum.kodi.tv/showthread.php?tid=112916 789 | value = value.partition('.')[0] # remove ms and UTC offset because also not supported 790 | t = time.strptime(value, '%Y-%m-%dT%H:%M:%S') 791 | return datetime.datetime.fromtimestamp(time.mktime(t)) 792 | 793 | def state_to_string(self, value): 794 | if value is None: 795 | return 'UNDEF' 796 | else: 797 | return value.isoformat() 798 | 799 | def test_state_value(self, value): 800 | if not isinstance(value, datetime.datetime): 801 | raise TypeError() 802 | return value 803 | 804 | 805 | class DimmerItem(ItemBase): 806 | def __init__(self, oh, itemData): 807 | super(DimmerItem, self).__init__(oh, itemData) 808 | 809 | def state_from_string(self, value): 810 | if value is None or value in ('NULL', 'UNDEF'): 811 | return None 812 | else: 813 | return Decimal(value) 814 | 815 | def state_to_string(self, value): 816 | if value is None: 817 | return 'UNDEF' 818 | else: 819 | return str(value) 820 | 821 | def test_state_value(self, value): 822 | if not isinstance(value, (int, float, Decimal)): 823 | raise TypeError() 824 | return value 825 | 826 | @update_proxy 827 | def cmd_set(self, value): 828 | if not isinstance(value, (int, float, Decimal)): 829 | raise TypeError() 830 | self.send_command(str(value)) 831 | self.attribs['state'] = value 832 | 833 | def cmd_on(self): 834 | self.send_command('ON') 835 | 836 | def cmd_off(self): 837 | self.send_command('OFF') 838 | 839 | def cmd_increase(self): 840 | self.send_command('INCREASE') 841 | 842 | def cmd_decrease(self): 843 | self.send_command('DECREASE') 844 | 845 | def cmd_toggle(self): 846 | self.send_command('TOGGLE') 847 | 848 | 849 | class GroupItem(ItemBase): 850 | def __init__(self, oh, itemData): 851 | super(GroupItem, self).__init__(oh, itemData) 852 | 853 | def state_from_string(self, value): 854 | if value is None or value in ('NULL', 'UNDEF'): 855 | return None 856 | else: 857 | return value 858 | 859 | def cmd_set(self, value): 860 | self.send_command(str(value)) 861 | 862 | 863 | class LocationItem(ItemBase): 864 | def __init__(self, oh, itemData): 865 | super(LocationItem, self).__init__(oh, itemData) 866 | 867 | def state_from_string(self, value): 868 | if value is None or value in ('NULL', 'UNDEF'): 869 | return None 870 | else: 871 | return value 872 | 873 | def state_to_string(self, value): 874 | if value is None: 875 | return 'UNDEF' 876 | else: 877 | return value 878 | 879 | def test_state_value(self, value): 880 | if not isinstance(value, str): 881 | raise TypeError() 882 | return value 883 | 884 | 885 | class NumberItem(ItemBase): 886 | def __init__(self, oh, itemData): 887 | super(NumberItem, self).__init__(oh, itemData) 888 | 889 | def state_from_string(self, value): 890 | if value is None or value in ('NULL', 'UNDEF'): 891 | return None 892 | else: 893 | return Decimal(value) 894 | 895 | def state_to_string(self, value): 896 | if value is None: 897 | return 'UNDEF' 898 | else: 899 | return str(value) 900 | 901 | def test_state_value(self, value): 902 | if not isinstance(value, (int, float, Decimal)): 903 | raise TypeError() 904 | return value 905 | 906 | @update_proxy 907 | def cmd_set(self, value): 908 | if not isinstance(value, (int, float, Decimal)): 909 | raise TypeError() 910 | self.send_command(str(value)) 911 | self.attribs['state'] = value 912 | 913 | 914 | class RollerShutterItem(ItemBase): 915 | def __init__(self, oh, itemData): 916 | super(RollerShutterItem, self).__init__(oh, itemData) 917 | 918 | def state_from_string(self, value): 919 | if value is None or value in ('NULL', 'UNDEF'): 920 | return None 921 | else: 922 | return Decimal(value) 923 | 924 | def state_to_string(self, value): 925 | if value is None: 926 | return 'UNDEF' 927 | # elif isinstance(value, str): 928 | # return value 929 | else: 930 | return str(value) 931 | 932 | def test_state_value(self, value): 933 | # if value not in ('UP', 'DOWN') and not isinstance(value, (int, float)): 934 | if not isinstance(value, (int, float, Decimal)): 935 | raise TypeError() 936 | return value 937 | 938 | @update_proxy 939 | def cmd_set(self, value): 940 | if not isinstance(value, (int, float, Decimal)): 941 | raise TypeError() 942 | self.send_command(str(value)) 943 | self.attribs['state'] = value 944 | 945 | def cmd_stop(self): 946 | self.send_command('STOP') 947 | 948 | def cmd_move(self): 949 | self.send_command('MOVE') 950 | 951 | def cmd_up(self): 952 | self.send_command('UP') 953 | 954 | def cmd_down(self): 955 | self.send_command('DOWN') 956 | 957 | def cmd_toggle(self): 958 | self.send_command('TOGGLE') 959 | 960 | 961 | class StringItem(ItemBase): 962 | def __init__(self, oh, itemData): 963 | super(StringItem, self).__init__(oh, itemData) 964 | 965 | def state_from_string(self, value): 966 | if value is None or value in ('NULL', 'UNDEF'): # REVISIT: what is the string for an uninitialized string item? 967 | return None 968 | else: 969 | return value 970 | 971 | def state_to_string(self, value): 972 | if value is None: 973 | return 'UNDEF' # REVISIT: what is the string for an undefined string item? 974 | else: 975 | return value 976 | 977 | def test_state_value(self, value): 978 | if not isinstance(value, str): 979 | raise TypeError() 980 | return value 981 | 982 | @update_proxy 983 | def cmd_set(self, value): 984 | if not isinstance(value, str): 985 | raise TypeError() 986 | self.send_command(value) 987 | self.attribs['state'] = value 988 | 989 | 990 | class SwitchItem(ItemBase): 991 | def __init__(self, oh, itemData): 992 | super(SwitchItem, self).__init__(oh, itemData) 993 | 994 | def state_from_string(self, value): 995 | if value is None or value in ('NULL', 'UNDEF'): 996 | return None 997 | elif value == 'ON': 998 | return True 999 | elif value == 'OFF': 1000 | return False 1001 | else: 1002 | return None #raise ValueError 1003 | 1004 | def state_to_string(self, value): 1005 | if value is None: 1006 | return 'UNDEF' 1007 | elif value: 1008 | return 'ON' 1009 | else: 1010 | return 'OFF' 1011 | 1012 | def test_state_value(self, value): 1013 | if not isinstance(value, bool): 1014 | raise TypeError() 1015 | return value 1016 | 1017 | @update_proxy 1018 | def cmd_set(self, value): 1019 | if not isinstance(value, bool): 1020 | raise TypeError() 1021 | self.send_command(self.state_to_string(value)) 1022 | self.attribs['state'] = value 1023 | 1024 | @update_proxy 1025 | def cmd_on(self): 1026 | self.send_command('ON') 1027 | self.attribs['state'] = True 1028 | 1029 | @update_proxy 1030 | def cmd_off(self): 1031 | self.send_command('OFF') 1032 | self.attribs['state'] = False 1033 | 1034 | @update_proxy 1035 | def cmd_toggle(self): 1036 | self.send_command('TOGGLE') 1037 | self.attribs['state'] = not self.attribs['state'] 1038 | 1039 | 1040 | if __name__ == '__main__': 1041 | oh = Server('localhost') 1042 | oh.poll_pages = True 1043 | oh.load_sitemaps() 1044 | homepage = oh.sitemaps['demo'].load_page() -------------------------------------------------------------------------------- /resources/lib/ordereddict.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009 Raymond Hettinger 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation files 5 | # (the "Software"), to deal in the Software without restriction, 6 | # including without limitation the rights to use, copy, modify, merge, 7 | # publish, distribute, sublicense, and/or sell copies of the Software, 8 | # and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 18 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 19 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 21 | # OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | from UserDict import DictMixin 24 | 25 | class OrderedDict(dict, DictMixin): 26 | 27 | def __init__(self, *args, **kwds): 28 | if len(args) > 1: 29 | raise TypeError('expected at most 1 arguments, got %d' % len(args)) 30 | try: 31 | self.__end 32 | except AttributeError: 33 | self.clear() 34 | self.update(*args, **kwds) 35 | 36 | def clear(self): 37 | self.__end = end = [] 38 | end += [None, end, end] # sentinel node for doubly linked list 39 | self.__map = {} # key --> [key, prev, next] 40 | dict.clear(self) 41 | 42 | def __setitem__(self, key, value): 43 | if key not in self: 44 | end = self.__end 45 | curr = end[1] 46 | curr[2] = end[1] = self.__map[key] = [key, curr, end] 47 | dict.__setitem__(self, key, value) 48 | 49 | def __delitem__(self, key): 50 | dict.__delitem__(self, key) 51 | key, prev, next = self.__map.pop(key) 52 | prev[2] = next 53 | next[1] = prev 54 | 55 | def __iter__(self): 56 | end = self.__end 57 | curr = end[2] 58 | while curr is not end: 59 | yield curr[0] 60 | curr = curr[2] 61 | 62 | def __reversed__(self): 63 | end = self.__end 64 | curr = end[1] 65 | while curr is not end: 66 | yield curr[0] 67 | curr = curr[1] 68 | 69 | def popitem(self, last=True): 70 | if not self: 71 | raise KeyError('dictionary is empty') 72 | if last: 73 | key = reversed(self).next() 74 | else: 75 | key = iter(self).next() 76 | value = self.pop(key) 77 | return key, value 78 | 79 | def __reduce__(self): 80 | items = [[k, self[k]] for k in self] 81 | tmp = self.__map, self.__end 82 | del self.__map, self.__end 83 | inst_dict = vars(self).copy() 84 | self.__map, self.__end = tmp 85 | if inst_dict: 86 | return (self.__class__, (items,), inst_dict) 87 | return self.__class__, (items,) 88 | 89 | def keys(self): 90 | return list(self) 91 | 92 | setdefault = DictMixin.setdefault 93 | update = DictMixin.update 94 | pop = DictMixin.pop 95 | values = DictMixin.values 96 | items = DictMixin.items 97 | iterkeys = DictMixin.iterkeys 98 | itervalues = DictMixin.itervalues 99 | iteritems = DictMixin.iteritems 100 | 101 | def __repr__(self): 102 | if not self: 103 | return '%s()' % (self.__class__.__name__,) 104 | return '%s(%r)' % (self.__class__.__name__, self.items()) 105 | 106 | def copy(self): 107 | return self.__class__(self) 108 | 109 | @classmethod 110 | def fromkeys(cls, iterable, value=None): 111 | d = cls() 112 | for key in iterable: 113 | d[key] = value 114 | return d 115 | 116 | def __eq__(self, other): 117 | if isinstance(other, OrderedDict): 118 | if len(self) != len(other): 119 | return False 120 | for p, q in zip(self.items(), other.items()): 121 | if p != q: 122 | return False 123 | return True 124 | return dict.__eq__(self, other) 125 | 126 | def __ne__(self, other): 127 | return not self == other 128 | -------------------------------------------------------------------------------- /resources/lib/selectdialog.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import xbmcaddon 4 | import xbmcgui 5 | 6 | # selectdialog.xml control ID's 7 | CONTROL_ID_LIST = 1100 8 | 9 | # xbmcgui action codes to go up/back in a window 10 | WINDOW_EXIT_CODES = frozenset([xbmcgui.ACTION_PARENT_DIR, 11 | xbmcgui.ACTION_PREVIOUS_MENU, 12 | xbmcgui.ACTION_NAV_BACK, 13 | xbmcgui.KEY_BUTTON_BACK]) 14 | 15 | 16 | class SelectDialog(xbmcgui.WindowXMLDialog): 17 | def __new__(cls, title, items, index=0): 18 | return super(SelectDialog, cls).__new__(cls, "selectdialog.xml", xbmcaddon.Addon().getAddonInfo('path')) 19 | 20 | # init window 21 | def __init__(self, title, items, index=None): 22 | super(SelectDialog, self).__init__() 23 | self.control = None 24 | self.title = title 25 | self.items = items 26 | self.index = index 27 | self.result = None 28 | self.listitems = [] 29 | 30 | def show(self): 31 | self.doModal() 32 | return self.result 33 | 34 | def set_title(self, title): 35 | self.title = title 36 | if self.control: 37 | self.setProperty('title', self.title) 38 | 39 | def set_items(self, items): 40 | self.items = items 41 | # rebuild whole menu if item list has changed 42 | self.build_list() 43 | 44 | def set_index(self, index): 45 | if self.index is not None: 46 | self.listitems[self.index].select(False) 47 | self.index = index 48 | if self.index is not None: 49 | self.listitems[self.index].select(True) 50 | 51 | def build_list(self): 52 | self.control.reset() 53 | self.listitems = [] 54 | for x in self.items: 55 | li = xbmcgui.ListItem(label=x) 56 | self.listitems.append(li) 57 | self.control.addItem(li) 58 | 59 | if self.index is not None: 60 | self.listitems[self.index].select(True) # set selected flag 61 | self.control.selectItem(self.index) # move focus to selected item 62 | 63 | # window init callback 64 | def onInit(self): 65 | self.control = self.getControl(CONTROL_ID_LIST) 66 | self.build_list() 67 | self.setProperty('title', self.title) 68 | self.setFocusId(CONTROL_ID_LIST) 69 | 70 | # window action callback 71 | def onAction(self, action): 72 | if action.getId() in WINDOW_EXIT_CODES: 73 | self.close() 74 | 75 | # mouse click action 76 | def onClick(self, controlId): 77 | if controlId == CONTROL_ID_LIST: 78 | self.result = self.control.getSelectedPosition() 79 | self.close() 80 | -------------------------------------------------------------------------------- /resources/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/skins/Default/720p/colorpicker.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 100 4 | 5 | 1 6 | 100 7 | 60 8 | 9 | yes 10 | dialogeffect 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 30 24 | 82 25 | 200 26 | 110 27 | 100 28 | 100 29 | 100 30 | 100 31 | horizontal 32 | 200 33 | 34 | 35 | 2 36 | 2 37 | 38 | 20 39 | 20 40 | $INFO[ListItem.Art(thumb)] 41 | 42 | 43 | 20 44 | 20 45 | center 46 | center 47 | font13_title 48 | AA000000 49 | 50 | ListItem.IsSelected + !StringCompare(ListItem.Label, FF000000) 51 | 52 | 53 | 20 54 | 20 55 | center 56 | center 57 | font13_title 58 | AAffffff 59 | 60 | ListItem.IsSelected + StringCompare(ListItem.Label, FF000000) 61 | 62 | 63 | 64 | 65 | 66 | 0 67 | 0 68 | 24 69 | 24 70 | colors/FFFFFFFF.png 71 | 72 | 73 | 2 74 | 2 75 | 76 | 20 77 | 20 78 | $INFO[ListItem.Art(thumb)] 79 | 80 | 81 | 20 82 | 20 83 | center 84 | center 85 | font13_title 86 | AA000000 87 | 88 | ListItem.IsSelected + !StringCompare(ListItem.Label, FF000000) 89 | 90 | 91 | 20 92 | 20 93 | center 94 | center 95 | font13_title 96 | AAffffff 97 | 98 | ListItem.IsSelected + StringCompare(ListItem.Label, FF000000) 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /resources/skins/Default/720p/menulist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1100 4 | no 5 | 6 | CommonBackground 7 | 8 | Section header image 9 | 20 10 | 3 11 | 35 12 | 35 13 | keep 14 | icon_system.png 15 | 16 | 17 | Section header text behind section header image 18 | 65 19 | 5 20 | 1000 21 | 30 22 | horizontal 23 | left 24 | 5 25 | 26 | WindowTitleCommons 27 | 28 | 29 | 30 | 31 | Dialog window 32 | 90 33 | 30 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Window background 44 | 5 45 | 5 46 | 1090 47 | 630 48 | default/ContentPanel.png 49 | 50 | 51 | 52 | Close window button 53 | 980 54 | 11 55 | 64 56 | 32 57 | 67 | 68 | 119 | 120 | Top left logo 121 | 30 122 | 15 123 | 220 124 | 80 125 | keep 126 | default/openhab-logo.png 127 | 128 | 129 | 130 | Right menu header background 131 | 268 132 | 10 133 | 794 134 | 50 135 | stretch 136 | default/dialogheader.png 137 | 138 | 139 | Right menu header label 140 | 300 141 | 22 142 | 740 143 | 30 144 | font16caps 145 | 146 | left 147 | center 148 | white 149 | black 150 | 151 | 152 | Right menu background 153 | 268 154 | 10 155 | 795 156 | 618 157 | default/black-back2.png 158 | 159 | 169 | 179 | 180 | Right menu scroll bar 181 | 1062 182 | 62 183 | 24 184 | 541 185 | 1100 186 | 1100 187 | ScrollBarV.png 188 | ScrollBarV_bar.png 189 | ScrollBarV_bar_focus.png 190 | ScrollBarNib.png 191 | ScrollBarNib.png 192 | vertical 193 | false 194 | 195 | 196 | 197 | 272 198 | 62 199 | 790 200 | 541 201 | 202 | 203 | 0 204 | 0 205 | 790 206 | 520 207 | 1097 208 | 1097 209 | 300 210 | 1097 211 | 212 | 213 | !StringCompare(ListItem.Property(type), separator) 214 | 215 | 0 216 | 0 217 | 790 218 | 40 219 | default/MenuItemNF.png 220 | 221 | 222 | 0 223 | 2 224 | 36 225 | 36 226 | keep 227 | ListItem.Property(iconurl) 228 | 229 | 230 | 40 231 | 6 232 | 500 233 | 20 234 | FF999999 235 | white 236 | ListItem.Label 237 | 238 | 239 | right 240 | 363 241 | 6 242 | 400 243 | 20 244 | FF999999 245 | white 246 | ListItem.Property(value) 247 | !StringCompare(ListItem.Property(type), bool) + !StringCompare(ListItem.Property(type), button) 248 | 249 | 250 | 251 | 5 252 | 6 253 | 400 254 | 35 255 | font13_title 256 | FFEE6E73 257 | ListItem.Label 258 | StringCompare(ListItem.Property(type), separator) 259 | 260 | 261 | Separator line 262 | 0 263 | 38 264 | 790 265 | 3 266 | separator2.png 267 | StringCompare(ListItem.Property(separator_line), 1) 268 | 269 | 270 | StringCompare(ListItem.Property(type), bool) 271 | 272 | 738 273 | 5 274 | 30 275 | 30 276 | keep 277 | default/radiobutton-nofocus.png 278 | StringCompare(ListItem.Property(value), 0) | IsEmpty(ListItem.Property(value)) 279 | 280 | 281 | 738 282 | 5 283 | 30 284 | 30 285 | keep 286 | default/radiobutton-focus.png 287 | StringCompare(ListItem.Property(value), 1) 288 | 289 | 290 | 291 | 745 292 | 9 293 | 22 294 | 22 295 | keep 296 | icons/do.png 297 | StringCompare(ListItem.Property(type), button) 298 | 299 | 300 | 775 301 | 13 302 | 9 303 | 15 304 | icons/next.png 305 | StringCompare(ListItem.Property(show_next), 1) 306 | 307 | 308 | 309 | 310 | !StringCompare(ListItem.Property(type), separator) 311 | 312 | 0 313 | 0 314 | 790 315 | 40 316 | Control.HasFocus(1100) 317 | default/MenuItemFO.png 318 | 319 | 320 | 0 321 | 2 322 | 36 323 | 36 324 | keep 325 | ListItem.Property(iconurl) 326 | 327 | 328 | 40 329 | 6 330 | 500 331 | 20 332 | white 333 | ListItem.Label 334 | Control.HasFocus(1100) 335 | 336 | 337 | 40 338 | 6 339 | 500 340 | 20 341 | FF999999 342 | ListItem.Label 343 | !Control.HasFocus(1100) 344 | 345 | 346 | right 347 | 363 348 | 6 349 | 400 350 | 20 351 | white 352 | ListItem.Property(value) 353 | !StringCompare(ListItem.Property(type), bool) + !StringCompare(ListItem.Property(type), button) + Control.HasFocus(1100) 354 | 355 | 356 | right 357 | 363 358 | 6 359 | 400 360 | 20 361 | FF999999 362 | ListItem.Property(value) 363 | !StringCompare(ListItem.Property(type), bool) + !StringCompare(ListItem.Property(type), button) + !Control.HasFocus(1100) 364 | 365 | 366 | 367 | 5 368 | 6 369 | 400 370 | 35 371 | font13_title 372 | FFEE6E73 373 | ListItem.Label 374 | StringCompare(ListItem.Property(type), separator) 375 | 376 | 377 | Separator line 378 | 0 379 | 38 380 | 790 381 | 3 382 | separator2.png 383 | StringCompare(ListItem.Property(separator_line), 1) 384 | 385 | 386 | StringCompare(ListItem.Property(type), bool) 387 | 388 | 738 389 | 5 390 | 30 391 | 30 392 | keep 393 | default/radiobutton-nofocus.png 394 | StringCompare(ListItem.Property(value), 0) | IsEmpty(ListItem.Property(value)) 395 | 396 | 397 | 738 398 | 5 399 | 30 400 | 30 401 | keep 402 | default/radiobutton-focus.png 403 | StringCompare(ListItem.Property(value), 1) 404 | 405 | 406 | 407 | 745 408 | 9 409 | 22 410 | 22 411 | keep 412 | icons/do.png 413 | StringCompare(ListItem.Property(type), button) 414 | 415 | 416 | 775 417 | 13 418 | 9 419 | 15 420 | icons/next.png 421 | StringCompare(ListItem.Property(show_next), 1) 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | -------------------------------------------------------------------------------- /resources/skins/Default/720p/selectdialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 100 5 | 60 6 | 7 | dialogeffect 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Window_OpenClose_Animation 20 | 21 | 20 22 | 70 23 | 200 24 | 495 25 | 1100 26 | 1100 27 | 1101 28 | 1101 29 | 1101 30 | 200 31 | 32 | 33 | 0 34 | 0 35 | 200 36 | 40 37 | default/button-nofocus.png 38 | 39 | 40 | 20 41 | 0 42 | 160 43 | 40 44 | font13 45 | left 46 | center 47 | FF999999 48 | FFEE6E73 49 | ListItem.Label 50 | 51 | 52 | 53 | 54 | 0 55 | 0 56 | 200 57 | 40 58 | default/button-focus.png 59 | 60 | 61 | 20 62 | 0 63 | 160 64 | 40 65 | font13 66 | left 67 | center 68 | white 69 | FFEE6E73 70 | ListItem.Label 71 | 72 | 73 | 74 | 75 | 240 76 | 70 77 | 25 78 | 495 79 | ScrollBarV.png 80 | ScrollBarV_bar.png 81 | ScrollBarV_bar_focus.png 82 | ScrollBarNib.png 83 | ScrollBarNib.png 84 | 1100 85 | 1100 86 | true 87 | vertical 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF000000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF000000.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF222222.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF222222.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF326492.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF326492.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF3989D3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF3989D3.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF444444.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF444444.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF48885D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF48885D.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF488B8E.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF488B8E.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF5E3F7A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF5E3F7A.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF5FC27D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF5FC27D.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF61CACE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF61CACE.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF63A2DC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF63A2DC.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF659247.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF659247.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF666666.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF666666.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF7F50AB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF7F50AB.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF80CE98.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF80CE98.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF83D4D7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF83D4D7.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF883C77.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF883C77.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF888888.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF888888.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF8DD45D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF8DD45D.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FF9975BD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FF9975BD.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFA5DC7E.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFA5DC7E.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFAACBEC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFAACBEC.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFAC3832.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFAC3832.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFB3AA3F.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFB3AA3F.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFB78538.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFB78538.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFB9E4C8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFB9E4C8.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFB9E7E9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFB9E7E9.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFC24BA9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFC24BA9.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFC7B2DA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFC7B2DA.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFCBEAB8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFCBEAB8.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFCE72BA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFCE72BA.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFE3B0D9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFE3B0D9.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFECE351.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFECE351.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFF86B64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFF86B64.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFF9ADAA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFF9ADAA.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFFA443B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFFA443B.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFFF9845.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFFF9845.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFFFAf51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFFFAf51.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFFFD39F.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFFFD39F.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFFFF65F.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFFFF65F.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFFFF9A5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFFFF9A5.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFaaaaaa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFaaaaaa.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFcccccc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFcccccc.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFeeeeee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFeeeeee.png -------------------------------------------------------------------------------- /resources/skins/Default/media/colors/FFffffff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/colors/FFffffff.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/ContentPanel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/ContentPanel.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/ContentPanelMirror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/ContentPanelMirror.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/DialogBack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/DialogBack.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/DialogCloseButton-focus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/DialogCloseButton-focus.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/DialogCloseButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/DialogCloseButton.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/HomeIcon-Focus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/HomeIcon-Focus.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/HomeIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/HomeIcon.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/MenuItemFO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/MenuItemFO.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/MenuItemNF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/MenuItemNF.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/about.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/black-back2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/black-back2.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/blue_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/blue_line.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/button-focus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/button-focus.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/button-focus2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/button-focus2.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/button-nofocus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/button-nofocus.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/dialogheader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/dialogheader.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/floor.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/header_oe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/header_oe.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/openhab-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/openhab-logo.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/osd_slider_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/osd_slider_bg.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/osd_slider_bg_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/osd_slider_bg_2.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/osd_slider_nib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/osd_slider_nib.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/osd_slider_nibNF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/osd_slider_nibNF.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/radiobutton-focus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/radiobutton-focus.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/radiobutton-nofocus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/radiobutton-nofocus.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/scroll-down-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/scroll-down-2.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/scroll-down-focus-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/scroll-down-focus-2.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/scroll-stop-focus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/scroll-stop-focus.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/scroll-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/scroll-stop.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/scroll-up-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/scroll-up-2.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/scroll-up-focus-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/scroll-up-focus-2.png -------------------------------------------------------------------------------- /resources/skins/Default/media/default/separator2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/default/separator2.png -------------------------------------------------------------------------------- /resources/skins/Default/media/icons/do.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/icons/do.png -------------------------------------------------------------------------------- /resources/skins/Default/media/icons/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/script.module.openhab/5ba4cb98b6cb15c95b0ecd2e86e379f610744718/resources/skins/Default/media/icons/next.png --------------------------------------------------------------------------------