|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +#Copyright (c) 2011, Paul Jennings <pjennings-tstat@pjennings.net> |
| 4 | +#All rights reserved. |
| 5 | + |
| 6 | +#Redistribution and use in source and binary forms, with or without |
| 7 | +#modification, are permitted provided that the following conditions are met: |
| 8 | +# |
| 9 | +# * Redistributions of source code must retain the above copyright notice, |
| 10 | +# this list of conditions and the following disclaimer. |
| 11 | +# * Redistributions in binary form must reproduce the above copyright |
| 12 | +# notice, this list of conditions and the following disclaimer in the |
| 13 | +# documentation and/or other materials provided with the distribution. |
| 14 | +# * The names of its contributors may not be used to endorse or promote |
| 15 | +# products derived from this software without specific prior written |
| 16 | +# permission. |
| 17 | + |
| 18 | +#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| 19 | +#AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| 20 | +#IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| 21 | +#ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
| 22 | +#LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| 23 | +#CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| 24 | +#SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| 25 | +#INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| 26 | +#CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| 27 | +#ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| 28 | +#THE POSSIBILITY OF SUCH DAMAGE. |
| 29 | + |
| 30 | +VERSION = 1.0 |
| 31 | + |
| 32 | +# Minimum and maximum values for heat and cool |
| 33 | +# The script will never set values outside of this range |
| 34 | +HEAT_MIN = 55 |
| 35 | +HEAT_MAX = 80 |
| 36 | +COOL_MIN = 70 |
| 37 | +COOL_MAX = 100 |
| 38 | + |
| 39 | +# Valid commands |
| 40 | +# Remove commands that you don't want the script to execute here |
| 41 | +# mode in particular can be dangerous, because someone could create |
| 42 | +# a 'mode off' command and turn your heat off in the winter. |
| 43 | +COMMANDS = ['Heat', 'Cool', 'Mode', 'Fan'] |
| 44 | + |
| 45 | +try: |
| 46 | + from xml.etree import ElementTree # for Python 2.5 users |
| 47 | +except ImportError: |
| 48 | + from elementtree import ElementTree |
| 49 | +import gdata.calendar.service |
| 50 | +import gdata.service |
| 51 | +import atom.service |
| 52 | +import gdata.calendar |
| 53 | + |
| 54 | +import atom |
| 55 | +import datetime |
| 56 | +import getopt |
| 57 | +import os |
| 58 | +import sys |
| 59 | +import string |
| 60 | +import time |
| 61 | + |
| 62 | +import TStat |
| 63 | + |
| 64 | +def main(tstatAddr, username=None, password=None, calName="Thermostat"): |
| 65 | + # Connect to thermostat |
| 66 | + tstat = TStat.TStat(tstatAddr) |
| 67 | + |
| 68 | + # Log in to Google |
| 69 | + calendar_service = gdata.calendar.service.CalendarService() |
| 70 | + calendar_service.email = username |
| 71 | + calendar_service.password = password |
| 72 | + calendar_service.source = "TStatGCal-%s" % VERSION |
| 73 | + calendar_service.ProgrammaticLogin() |
| 74 | + |
| 75 | + # Create date range for event search |
| 76 | + today = datetime.datetime.today() |
| 77 | + gmt = time.gmtime() |
| 78 | + gmtDiff = datetime.datetime(gmt[0], gmt[1], gmt[2], gmt[3], gmt[4]) - today |
| 79 | + tomorrow = datetime.datetime.today()+datetime.timedelta(days=2) |
| 80 | + |
| 81 | + query = gdata.calendar.service.CalendarEventQuery() |
| 82 | + query.start_min = "%04i-%02i-%02i" % (today.year, today.month, today.day) |
| 83 | + query.start_max = "%04i-%02i-%02i" % (tomorrow.year, tomorrow.month, tomorrow.day) |
| 84 | + |
| 85 | + # Look for a calendar called calName |
| 86 | + feed = calendar_service.GetOwnCalendarsFeed() |
| 87 | + for i, a_calendar in enumerate(feed.entry): |
| 88 | + if a_calendar.title.text == calName: |
| 89 | + query.feed = a_calendar.content.src |
| 90 | + |
| 91 | + if query.feed is None: |
| 92 | + print "No calendar with name '%s' found" % calName |
| 93 | + return |
| 94 | + |
| 95 | + # Search for the event that has passed but is closest to the current time |
| 96 | + closest = None |
| 97 | + closestDT = None |
| 98 | + closestWhen = None |
| 99 | + closestEvent = None |
| 100 | + feed = calendar_service.CalendarQuery(query) |
| 101 | + for i, an_event in enumerate(feed.entry): |
| 102 | + #print '\t%s. %s' % (i, an_event.title.text,) |
| 103 | + # Skip events that are not valid commands |
| 104 | + (command, value) = an_event.title.text.splitlines()[0].split() |
| 105 | + if command not in COMMANDS: |
| 106 | + print "Warning: '%s' is not a valid command" % an_event.title.text |
| 107 | + continue |
| 108 | + try: |
| 109 | + float(value) |
| 110 | + except: |
| 111 | + if value not in ['Off', 'On', 'Auto']: |
| 112 | + print "Warning: '%s' is not a valid command" % an_event.title.text |
| 113 | + continue |
| 114 | + for a_when in an_event.when: |
| 115 | + d = a_when.start_time.split("T")[0] |
| 116 | + t = a_when.start_time.split("T")[1].split(".")[0] |
| 117 | + (year, month, day) = [int(p) for p in d.split("-")] |
| 118 | + (hour, min, sec) = [int(p) for p in t.split(":")] |
| 119 | + dt = datetime.datetime(year, month, day, hour, min, sec)-gmtDiff |
| 120 | + #print "DT:", dt |
| 121 | + d = dt-datetime.datetime.today() |
| 122 | + #print "d.days:", d.days |
| 123 | + |
| 124 | + # Skip events that are in the future |
| 125 | + if d.days >= 0: |
| 126 | + continue |
| 127 | + |
| 128 | + if closest is None: |
| 129 | + closest = d |
| 130 | + closestDT = dt |
| 131 | + closestWhen = a_when |
| 132 | + closestEvent = an_event |
| 133 | + else: |
| 134 | + if d.days < closest.days: |
| 135 | + continue |
| 136 | + if d.seconds > closest.seconds: |
| 137 | + closest = d |
| 138 | + closestDT = dt |
| 139 | + closestWhen = a_when |
| 140 | + closestEvent = an_event |
| 141 | + |
| 142 | + if closestEvent is None: |
| 143 | + print "No events found" |
| 144 | + return |
| 145 | + |
| 146 | + text = closestEvent.title.text |
| 147 | + print "Closest event: %s at %s" % (text, closestDT) |
| 148 | + (command, value) = text.splitlines()[0].split() |
| 149 | + if command == 'Heat': |
| 150 | + print "Setting heat to %s" % int(value) |
| 151 | + tstat.setHeatPoint(int(value)) |
| 152 | + elif command == 'Cool': |
| 153 | + print "Setting cool to %s" % value |
| 154 | + tstat.setCoolPoint(int(value)) |
| 155 | + elif command == 'Fan': |
| 156 | + print "Setting fan to %s" % value |
| 157 | + tstat.setFanMode(value) |
| 158 | + elif command == 'Mode': |
| 159 | + print "Setting mode to %s" % value |
| 160 | + tstat.setTstatMode(value) |
| 161 | + |
| 162 | +if __name__ == '__main__': |
| 163 | + f = open(os.path.expanduser("~/.google")) |
| 164 | + username = f.readline().splitlines()[0] |
| 165 | + password = f.readline().splitlines()[0] |
| 166 | + main(sys.argv[1], username=username, password=password, calName=sys.argv[2]) |
0 commit comments