-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathTStatGcal.py
307 lines (273 loc) · 10.5 KB
/
TStatGcal.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
#!/usr/bin/env python
#Copyright (c) 2011, Paul Jennings <[email protected]>
#All rights reserved.
#Redistribution and use in source and binary forms, with or without
#modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * The names of its contributors may not be used to endorse or promote
# products derived from this software without specific prior written
# permission.
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
#LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
#SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
#INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
#CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
#ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
#THE POSSIBILITY OF SUCH DAMAGE.
VERSION = 1.0
# TStatGcal.py
# Script to pull commands from Google Calendar and update thermostat.
#
# Requirements:
# * gdata (http://code.google.com/p/gdata-python-client/)
# * ElementTree (http://effbot.org/zone/element-index.htm)
# * Python-TStat (same place you got this script)
#
# Usage:
# 1. Create a Google/GMail account (or Google Apps for domains).
# 2. Go to http://calendar.google.com
# 3. Create a calendar (called "Thermostat" for example).
# 4. Add events with titles of the form:
# "Heat 70" -- sets heat to 70 degrees
# "Cool 70" -- sets cool to 70 degrees
# "Fan On" -- forces fan on
# "Mode Off" -- forces system off
# 5. Run the following commands (assuming Unix/Linux system):
# echo "[email protected]" >> ~/.google
# echo "yourpassword" >> ~/.google
# chmod 400 ~/.google
# (where "[email protected]" is the account that you created in
# step 1 and "yourpassword" is your password)
# 6. Add the following to your crontab to run every 5 minutes or so:
# TStatGcal.py <thermostat_address> <calendar_name>
# Where <thermostat_address> is the IP address of your thermostat
# and <calendar_name> is the name of the calendar you created in
# step 3.
#
# Notes:
# In order to limit the chance that this script sets your
# thermostat to dangerous settings (e.g. too low or off during
# the winter, there are some override variables below:
# HEAT_MIN, HEAT_MAX: Minimum/maximum setting for heat
# COOL_MIN, COOL_MAX: Minimum/maximum setting for cool
# COMMANDS: What parts of the thermostat the script is
# allowed to control
#
# Set the HEAT/COOL variables to appropriate values for your
# situation. By default, this script will not set the
# thermostat mode (on/off/auto). You probably want to leave
# it on auto. This is to prevent a hacker (or a typo) from
# turning your furnace off during the winter.
#
# By default, this script does not disable cloud updates.
# That way, if this script does not run for some reason (e.g.
# if your computer crashes), you can still have a reasonable
# backup program running. When the cloud updates your thermostat,
# there may be a short period where the setting does not match
# what is on your calendar. If this behavior is undesirable, you
# can disable cloud updates.
#
# At the start time of your event, the script will set the
# the thermostat to the requested setting. The duration of the
# events on your calendar is ignored. For example, a simple
# program might look like this:
# 6:30 -- Heat 70
# 8:00 -- Heat 60
# 16:00 -- Heat 70
# 22:00 -- Heat 60
# In order to create this program in your calendar, you would need
# four events. If you create a "Heat 70" event that lasts from
# 6:30-22:00 and an overlapping "Heat 60" event that lasts from
# 8:00-16:00, you will effectively miss the "Heat 70" command at
# 16:00. Only the start time of the event is used.
# Minimum and maximum values for heat and cool
# The script will never set values outside of this range
HEAT_MIN = 55
HEAT_MAX = 80
COOL_MIN = 70
COOL_MAX = 100
# Valid commands
# Remove commands that you don't want the script to execute here
# mode in particular can be dangerous, because someone could create
# a 'mode off' command and turn your heat off in the winter.
#COMMANDS = ['Heat', 'Cool', 'Mode', 'Fan']
COMMANDS = ['Heat', 'Cool', 'Fan']
PERIODS = ['Wake', 'Leave', 'Home', 'Sleep']
try:
from xml.etree import ElementTree # for Python 2.5 users
except ImportError:
from elementtree import ElementTree
import gdata.calendar.service
import gdata.service
import atom.service
import gdata.calendar
import atom
import datetime
import getopt
import os
import sys
import string
import time
import TStat
def getCalendarService(username, password):
# Log in to Google
calendar_service = gdata.calendar.service.CalendarService()
calendar_service.email = username
calendar_service.password = password
calendar_service.source = "TStatGCal-%s" % VERSION
calendar_service.ProgrammaticLogin()
return calendar_service
def main(tstatAddr, commandMap=None, username=None, password=None, calName="Thermostat"):
# Connect to thermostat
tstat = TStat.TStat(tstatAddr)
# Command map is used to translate things like "Wake" into "Heat 70"
if commandMap is None:
commandMap = {}
calendar_service = getCalendarService(username, password)
# Create date range for event search
today = datetime.datetime.today()
gmt = time.gmtime()
gmtDiff = datetime.datetime(gmt[0], gmt[1], gmt[2], gmt[3], gmt[4]) - today
tomorrow = datetime.datetime.today()+datetime.timedelta(days=8)
query = gdata.calendar.service.CalendarEventQuery()
query.start_min = "%04i-%02i-%02i" % (today.year, today.month, today.day)
query.start_max = "%04i-%02i-%02i" % (tomorrow.year, tomorrow.month, tomorrow.day)
print "start_min:", query.start_min
print "start_max:", query.start_max
# Look for a calendar called calName
feed = calendar_service.GetOwnCalendarsFeed()
for i, a_calendar in enumerate(feed.entry):
if a_calendar.title.text == calName:
query.feed = a_calendar.content.src
if query.feed is None:
print "No calendar with name '%s' found" % calName
return
# Search for the event that has passed but is closest to the current time
# There is probably a better way to do this...
closest = None
closestDT = None
closestWhen = None
closestEvent = None
closestCommand = None
closestValue = None
periods = {}
feed = calendar_service.CalendarQuery(query)
for i, an_event in enumerate(feed.entry):
#print '\t%s. %s' % (i, an_event.title.text,)
# Try to map named time period into actual command
text = an_event.title.text.strip()
if not text in PERIODS:
if commandMap.has_key(text):
text = commandMap[text]
print "Translated %s into %s" % (an_event.title.text.strip(), text)
# Skip events that are not valid commands
try:
(command, value) = text.splitlines()[0].split()
except:
command = text
if command not in COMMANDS:
print "Warning: '%s' is not a valid command" % text
continue
try:
float(value)
except:
if value not in ['Off', 'On', 'Auto']:
print "Warning: '%s' is not a valid command" % an_event.title.text
continue
for a_when in an_event.when:
d = a_when.start_time.split("T")[0]
t = a_when.start_time.split("T")[1].split(".")[0]
(year, month, day) = [int(p) for p in d.split("-")]
(hour, min, sec) = [int(p) for p in t.split(":")]
dt = datetime.datetime(year, month, day, hour, min, sec)-gmtDiff
#print "DT:", dt
d = dt-datetime.datetime.today()
#print "d.days:", d.days
if text in PERIODS:
if not periods.has_key(dt.day):
periods[dt.day] = {}
periods[dt.day][text] = dt
else:
# Skip events that are in the future
if d.days >= 0:
continue
if closest is None:
closest = d
closestDT = dt
closestWhen = a_when
closestEvent = an_event
closestCommand = command
closestValue = value
else:
if d.days < closest.days:
continue
if d.seconds > closest.seconds:
closest = d
closestDT = dt
closestWhen = a_when
closestEvent = an_event
closestCommand = command
closestValue = value
print "Found periods:", periods
# Handle programmed periods
periodCommands = {}
for day in range(0,7):
if not periodCommands.has_key(day):
periodCommands[day] = []
for p in PERIODS:
if periods.has_key(p):
periodCommands.append(int(periods[p].hour*60+periods[p].minute))
periodCommands.append(int(commandMap[p].split()[-1]))
else:
periodCommands.append(periodCommands[-2])
periodCommands.append(periodCommands[-2])
print "Commands:", periodCommands
if closestEvent is None:
print "No events found"
return
text = closestEvent.title.text
print "Closest event: %s at %s" % (text, closestDT)
#(command, value) = text.splitlines()[0].split()
command, value = (closestCommand, closestValue)
if command == 'Heat':
value = int(value)
if value >= HEAT_MIN and value <= HEAT_MAX:
print "Setting heat to %s" % int(value)
#tstat.setHeatPoint(value)
else:
print "Value out of acceptable heat range:", value
elif command == 'Cool':
value = int(value)
if value >= COOL_MIN and value <= COOL_MAX:
print "Setting cool to %s" % value
tstat.setCoolPoint(int(value))
else:
print "Value out of acceptable cool range:", value
elif command == 'Fan':
print "Setting fan to %s" % value
tstat.setFanMode(value)
elif command == 'Mode':
print "Setting mode to %s" % value
tstat.setTstatMode(value)
if __name__ == '__main__':
f = open(os.path.expanduser("~/.google"))
username = f.readline().splitlines()[0]
password = f.readline().splitlines()[0]
f.close()
commandMap = {}
if os.path.isfile(os.path.expanduser("~/.tstat_commands")):
f = open(os.path.expanduser("~/.tstat_commands"))
for line in f.readlines():
key, value = line.split(":")
commandMap[key] = value
f.close()
main(sys.argv[1], username=username, password=password, calName=sys.argv[2], commandMap=commandMap)