├── .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 |
58 | PreviousMenu
59 | default/DialogCloseButton-focus.png
60 | default/DialogCloseButton.png
61 | 1
62 | 1
63 | 1
64 | 1
65 | system.getbool(input.enablemouse)
66 |
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
--------------------------------------------------------------------------------