Skip to content

Commit a4bb1be

Browse files
committed
Initial Pelican thermostat integration.
1 parent 12a8868 commit a4bb1be

1 file changed

Lines changed: 280 additions & 0 deletions

File tree

Calendar/IntegrationPelican.py

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
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 = "&nbsp;" * (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

Comments
 (0)