1+ global model , Data , q
2+
3+ from urllib import urlencode
4+ import xml .etree .ElementTree as ET
5+ import json
6+
7+
8+
9+ site = ""
10+ username = ""
11+ password = ""
12+ stats = None
13+
14+ # encode URL parameters
15+ auth = urlencode ({'username' : username , 'password' : password })
16+
17+ base_url = "https://{}/api.cgi?{}" .format (site , auth )
18+
19+ def xml_to_dict (element ):
20+ # Convert an XML element and its children into a dictionary
21+ # if node has only text, make that the value alone.
22+ if element .text and element .text .strip () and not element .attrib and len (list (element )) == 0 :
23+ return element .text .strip ()
24+
25+ node = {}
26+ # Add element attributes if present
27+ if element .attrib :
28+ node .update (element .attrib )
29+ # Add element text if present
30+ if element .text and element .text .strip ():
31+ node ['_text' ] = element .text .strip ()
32+ # Recursively process child elements
33+ for child in element :
34+ child_dict = xml_to_dict (child )
35+ if child .tag not in node :
36+ node [child .tag ] = child_dict
37+ else :
38+ # Handle multiple children with the same tag
39+ if not isinstance (node [child .tag ], list ):
40+ node [child .tag ] = [node [child .tag ]]
41+ node [child .tag ].append (child_dict )
42+
43+ if node == {}:
44+ return None
45+ return node
46+
47+ def xml_str_to_dict (xml_string ):
48+ # Parse the XML string
49+ root = ET .fromstring (xml_string )
50+ # Convert XML to dictionary
51+ return {root .tag : xml_to_dict (root )}
52+
53+ def get_thermostats ():
54+ global stats
55+ if stats is None :
56+ atts = {
57+ 'username' : username ,
58+ 'password' : password ,
59+ 'request' : 'get' ,
60+ 'object' : 'Thermostat' ,
61+ 'value' : 'name;groupName;serialNo;description;schedule'
62+ }
63+ url = "https://{}/api.cgi?{}" .format (site , urlencode (atts ))
64+ stats = xml_str_to_dict (model .RestGet (url , {}))
65+ return stats
66+
67+ def get_reservables (typ ):
68+ return q .QuerySql ("""
69+ SELECT ReservableId,
70+ ParentId,
71+ COALESCE(BadgeColor, '#153aa8') as Color,
72+ Name,
73+ Description,
74+ IsReservable,
75+ IsEnabled,
76+ IsCountable,
77+ Quantity
78+ FROM Reservable
79+ WHERE ReservableTypeId = {}
80+ AND IsDeleted = 0
81+ AND IsEnabled = 1
82+
83+ ORDER BY ReservableTypeId, Name
84+ ;
85+ """ .format (typ ))
86+
87+ # Gets thermostat events for the thermostat with the given serial or for all thermostats if no serial is given.
88+ def get_thermostat_events (serial = None ):
89+
90+ selection = ['origin:External' ]
91+
92+ if serial :
93+ selection .append ('serialNo:{}' .format (serial ))
94+
95+ atts = {
96+ 'username' : username ,
97+ 'password' : password ,
98+ 'request' : 'get' ,
99+ 'object' : 'ThermostatEvent' ,
100+ 'selection' : ';' .join (selection ),
101+ 'value' : 'eventId;title;startDate;startTime;endTime;heatSetting;coolSetting;fan;keypad;outsideVentilation;serialNo'
102+ }
103+ url = "https://{}/api.cgi?{}" .format (site , urlencode (atts ))
104+ return xml_str_to_dict (model .RestGet (url , {}))
105+
106+ # url = base_url + "&request=get&object=Thermostat&value=name;groupName;serialNo;description;schedule"
107+
108+ def has_valid_credentials ():
109+ global username , password , site
110+
111+ username = model .Setting ("PelicanHvacEmail" , "" )
112+ password = model .Setting ("PelicanHvacPassword" , "" )
113+ site = model .Setting ("PelicanHvacSite" , "" )
114+
115+ if not username or not password or not site :
116+ return False
117+
118+ r = get_thermostats ()
119+ return r ['result' ]['success' ] == "1"
120+
121+ def has_configuration ():
122+ return False
123+
124+
125+
126+ if model .HttpMethod == "post" and Data .v == "credsave" and Data .username != "" and Data .password != "" and Data .site != "" :
127+ model .SetSetting ("PelicanHvacEmail" , Data .username .strip ())
128+ model .SetSetting ("PelicanHvacPassword" , Data .password .strip ())
129+ model .SetSetting ("PelicanHvacSite" , Data .site .strip ())
130+
131+ print ("REDIRECT=/PyScript/IntegrationPelican?v=config" )
132+
133+ elif not has_valid_credentials ():
134+
135+ model .Title = "Configure Pelican HVAC Integration"
136+ model .Header = "Configure Pelican HVAC Integration"
137+
138+ print ("<p>Set up Pelican HVAC integration with Pelican account credentials. We <b>strongly</b> recommend that you create a user within Pelican just for this purpose, separate from any human users.</p>" )
139+
140+ # language=html
141+ form = """
142+ <form action="/PyScriptForm/IntegrationPelican?v=credsave" method="POST">
143+ <table id="settings" class="table table-striped">
144+ <thead>
145+ <tr>
146+ <th>Setting</th>
147+ <th style="width:50%">Value</th>
148+ </tr>
149+ </thead>
150+ <tbody>
151+ <tr>
152+ <td><label for="site">Site Address (e.g. mychurch.officeclimatecontrol.net)</label></td>
153+ <td><input name="site" value="{0}" type="text" /></td>
154+ </tr>
155+ <tr>
156+ <td><label for="username">Email Address</label></td>
157+ <td><input name="username" value="{1}" type="email" /></td>
158+ </tr>
159+ <tr>
160+ <td><label for="password">Password</label></td>
161+ <td><input name="password" value="{2}" type="text" /></td>
162+ </tr>
163+ </tbody>
164+ </table>
165+
166+ <p>If you need to revisit these settings, they will be in the main TouchPoint settings in the "General" section.</p>
167+
168+ <input type="submit" value="Save" class="btn btn-primary" />
169+
170+ </form>
171+ """
172+
173+ print (form .format (site , username , password ))
174+
175+ elif Data .v == "config" :
176+ model .Title = "Configure Pelican HVAC Integration"
177+ model .Header = "Configure Pelican HVAC Integration"
178+
179+ stats = get_thermostats ()
180+ stats = stats ['result' ]['Thermostat' ]
181+
182+ # sort by name alphabetically
183+ stats .sort (key = lambda x : x .get ('name' , '' ))
184+ stats_count = len (stats )
185+
186+ # group list of stats by groupName
187+ grouped_stats = {}
188+ for stat in stats :
189+ group_name = stat .get ('groupName' , 'Ungrouped' )
190+ if group_name not in grouped_stats :
191+ grouped_stats [group_name ] = []
192+ grouped_stats [group_name ].append (stat )
193+ stats = grouped_stats
194+
195+
196+ existingConfig = model .TextContent ("IntegrationPelicanConfig.json" ) or "[]"
197+
198+ header_row = ""
199+ option_row = ""
200+
201+ for group in stats .keys ():
202+ group_stats = stats [group ]
203+ for stat in group_stats :
204+ header_row += "<th class=\" device\" >{}<br /><span class=\" small\" >{}</span></th>\n " .format (stat .get ('name' , 'Unnamed' ), stat .get ('serialNo' , 'Unnamed' ))
205+ option_row += "<td>\n <input type=\" checkbox\" value=\" [1]\" name=\" {0}-[0]\" />\n </td>\n " .format (stat .get ('serialNo' , '' ))
206+
207+
208+ reservables = get_reservables (1 ) # 1 = rooms
209+
210+ reservable_dict = {r .ReservableId : r for r in reservables }
211+ children = {r .ReservableId : [] for r in reservables }
212+ for r in reservables :
213+ if r .ParentId in reservable_dict :
214+ children [r .ParentId ].append (r )
215+
216+ def generate_row (r , indents = 0 ):
217+ indent_str = " " * (4 * indents )
218+ row = "<td>{}{}</td>\n " .format (indent_str , r .Name )
219+ if r .IsReservable :
220+ row += option_row .replace ('[0]' , str (r .ReservableId ))
221+ else :
222+ row += "<td colspan=\" {}\" ></td>\n " .format (stats_count )
223+ mtx = "<tr>\n {}\n </tr>\n " .format (row )
224+ for child in children [r .ReservableId ]:
225+ mtx += generate_row (child , indents + 1 )
226+ return mtx
227+
228+ matrix = ""
229+ for r in reservables :
230+ if r .ParentId is None :
231+ matrix += generate_row (r )
232+
233+ # language=html
234+ matrix = """
235+ <table class="table table-striped">
236+ <thead>
237+ <tr>
238+ <th>Room</th>
239+ {}
240+ </tr>
241+ </thead>
242+ <tbody>
243+ {}
244+ </tbody>
245+ </table>
246+ """ .format (header_row , matrix )
247+
248+ print (matrix )
249+
250+ # language=html
251+ print ("""
252+ <style>
253+ th.device {
254+ writing-mode: vertical-rl;
255+ transform: rotate(180deg);
256+ }
257+ </style>
258+ """ )
259+
260+
261+ elif not has_configuration ():
262+ print ("REDIRECT=/PyScript/IntegrationPelican?v=config" )
263+
264+ else :
265+
266+
267+ # response = get_thermostats()
268+ # response = get_thermostat_events("7S3-GV44")
269+
270+ response = get_thermostat_events ()
271+
272+
273+ # response = model.RestGet(url, {})
274+
275+
276+ # response = xml_str_to_dict(response)
277+
278+ print ("<pre>" )
279+ print (json .dumps (response , indent = 4 ))
280+ print ("</pre>" )
0 commit comments