-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Basic functionality in place and working
- Loading branch information
Ben Franske
committed
Jan 17, 2011
0 parents
commit 2e095f7
Showing
4 changed files
with
401 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
#! /usr/bin/python | ||
|
||
import pystat | ||
import sys | ||
import string | ||
from optparse import OptionParser | ||
|
||
def main(): | ||
usage = "usage: %prog [options]" | ||
parser = OptionParser(usage=usage, version="%prog 0.1") | ||
parser.add_option("-t", "--temp", | ||
action="store_true", dest="temp", | ||
help="Get the current temperature") | ||
parser.add_option("-s", "--setpoint", | ||
action="store_true", dest="sp", | ||
help="Get the current setpoint") | ||
parser.add_option("-b", "--basic", | ||
action="store_true", dest="basicinfo", | ||
help="Get the basic statistics") | ||
(options, args) = parser.parse_args() | ||
t = pystat.thermostat('/dev/ttyUSB0') | ||
if options.temp: | ||
print (t.currentsettings()['temperature']) | ||
if options.sp: | ||
print (t.currentsettings()['setpoint']) | ||
if options.basicinfo: | ||
info = t.currentsettings() | ||
print ("Temperature: " + info['temperature']) | ||
print ("Set Point: " + info['setpoint']) | ||
print ("Mode: " + info['mode']) | ||
print ("Fan Mode: " + info['fanMode']) | ||
t.close() | ||
|
||
if __name__ == "__main__": | ||
main() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
# pyStat Python module | ||
|
||
#Copyright 2010-2011 Ben Franske ([email protected] http://www.benfranske.com) | ||
|
||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
|
||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
|
||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
import serial | ||
from time import localtime, strftime, strptime | ||
global tstat_address | ||
global local_address | ||
|
||
# Thermostat address is 1 by default | ||
tstat_address=1 | ||
|
||
# Local address is 0 by default | ||
local_address=0 | ||
|
||
# pyStat is a Python module for sending and receiving information from | ||
# the Residential Control Systems (RCS) TR-60 (and possibly other RCS) | ||
# thermostats. | ||
# | ||
# Example usage: | ||
# | ||
# Create a thermostat object that can send and receive information from | ||
# the thermostat | ||
# t = pystat.thermostat('/dev/ttyUSB0') | ||
# Adjust the thermostat setpoint to 72 degrees) | ||
# t.setpoint(72) | ||
# Close the thermostat object and release the serial port | ||
# t.close() | ||
# | ||
|
||
class thermostat: | ||
|
||
commands = { | ||
'setpoint' : 'SP', | ||
'setpointheat' : 'SPH', | ||
'setpointcool' : 'SPC', | ||
'setmode' : 'M', | ||
'setfan' : 'F', | ||
'tempformat' : 'CFM', | ||
'textmsg' : 'TM', | ||
'settime' : 'TIME', | ||
'setdate' : 'DATE', | ||
'setdayofweek' : 'DOW', | ||
'schedulecontrol' : 'SC', | ||
'scheduleentry' : 'SE', | ||
'returnstatus' : 'R'} | ||
|
||
def sendBasicSet(self,tstat_address,local_address,cmd,value): | ||
# Sends a standard command to the thermostat | ||
if tstat_address is None: return "Invalid thermostat address" | ||
if local_address is None: return "Invalid local address" | ||
tstat_address = str(tstat_address) | ||
local_address = str(local_address) | ||
value = str(value) | ||
msg = "A=" + tstat_address + " O=" + local_address + " " + cmd + "=" + value + "\r" | ||
self.send_msg(msg,0) | ||
return | ||
|
||
def getLine(self,tstat_address,local_address,cmd,value): | ||
# Send a status request message and return one line | ||
if tstat_address is None: return "Invalid thermostat address" | ||
if local_address is None: return "Invalid local address" | ||
tstat_address = str(tstat_address) | ||
local_address = str(local_address) | ||
value = str(value) | ||
msg = "A=" + tstat_address + " O=" + local_address + " " + cmd + "=" + value + "\r" | ||
line = '' | ||
line = self.send_msg(msg,1) | ||
return line | ||
|
||
def send_msg(self,message,expectdata=0): | ||
# This method is responsible for handling all outgoing serial messages. | ||
try: | ||
if not self.ser.isOpen(): | ||
self.ser.open() | ||
except serial.SerialException: | ||
raise | ||
return "Could not open serial port" | ||
self.ser.write(message) | ||
line = '' | ||
# Only some of the commands return data | ||
if expectdata is 1: | ||
while True: | ||
char = self.ser.read(1) | ||
line += char | ||
if char == '\r': | ||
break | ||
return line | ||
|
||
def __init__(self, serPort=None, verbose=False): | ||
|
||
# Serial port initialization | ||
try: | ||
if serPort is None: | ||
raise Exception('Serial port not selected') | ||
else: | ||
if type(serPort) is str: | ||
port = serPort | ||
else: | ||
raise Exception('User specified serial port is not a string.') | ||
self.ser = serial.Serial(port,9600,8,'N',1,timeout=1) | ||
except serial.SerialException: | ||
raise | ||
except NameError: | ||
self.ser = None | ||
raise | ||
|
||
# --- Low-level/internal methods --- | ||
|
||
def close(self): | ||
# Use close to release the serial port and socket interface. | ||
try: | ||
self.ser.close() | ||
except: | ||
pass | ||
|
||
# --- High-level methods --- | ||
|
||
def setpoint(self, setpoint): | ||
# Adjust the thermostat setpoint for the currently selected mode (heating or cooling) | ||
status = self.sendBasicSet(tstat_address,local_address,self.commands['setpoint'],setpoint) | ||
return | ||
def setfan(self, setfan): | ||
# Put the fan in auto mode (FALSE) or manual on mode (TRUE) | ||
if setfan: | ||
status = self.sendBasicSet(tstat_address,local_address,self.commands['setfan'],1) | ||
elif not setfan: | ||
status = self.sendBasicSet(tstat_address,local_address,self.commands['setfan'],0) | ||
return | ||
def setclocktopc(self): | ||
# Set the time, date and day of week on the thermostat to match the local system time on the computer | ||
status = self.sendBasicSet(tstat_address,local_address,self.commands['settime'],strftime("%H:%M:%S",localtime())) | ||
status = self.sendBasicSet(tstat_address,local_address,self.commands['setdate'],strftime("%m/%d/%y",localtime())) | ||
status = self.sendBasicSet(tstat_address,local_address,self.commands['setdayofweek'],str(int(strftime("%w",localtime())) + 1)) | ||
return | ||
def currentsettings(self): | ||
# Return an array with the current settings of the thermostat | ||
settings = self.getLine(tstat_address,local_address,self.commands['returnstatus'],1) | ||
data = settings.split() | ||
parsedData = {} | ||
for i in data: | ||
subdata = i.split("=") | ||
if subdata[0] == "A": | ||
parsedData['address']=subdata[1] | ||
elif subdata[0] == "O": | ||
parsedData['originator']=subdata[1] | ||
elif subdata[0] == "Z": | ||
parsedData['zone']=subdata[1] | ||
elif subdata[0] == "T": | ||
parsedData['temperature']=subdata[1] | ||
elif subdata[0] == "SP": | ||
parsedData['setpoint']=subdata[1] | ||
elif subdata[0] == "SPH": | ||
parsedData['setpointHeating']=subdata[1] | ||
elif subdata[0] == "SPC": | ||
parsedData['setpointCooling']=subdata[1] | ||
elif subdata[0] == "M": | ||
parsedData['mode']=subdata[1] | ||
elif subdata[0] == "FM": | ||
parsedData['fanMode']=subdata[1] | ||
elif subdata[0] == "SC": | ||
parsedData['scheduleControl']=subdata[1] | ||
return parsedData | ||
def getperiodsched(self, day, period): | ||
# Return an array with the schedule entries for a given day and period (day and period given numerically in RCS format) | ||
# Sun=1, Sat=7 | ||
# Note that schedTime is returned in the python struct_time format (with only hour, minute and weekday set) | ||
settings = self.getLine(tstat_address,local_address,self.commands['scheduleentry'] + str(day) + '/' + str(period),'?') | ||
data = settings.split() | ||
parsedData = {} | ||
pythonDOW = str(day-1) | ||
for i in data: | ||
subdata = i.split("=") | ||
# if subdata[0] == "A": | ||
# parsedData['address']=subdata[1] | ||
# elif subdata[0] == "O": | ||
# parsedData['originator']=subdata[1] | ||
if subdata[0] == self.commands['scheduleentry'] + str(day) + '/' + str(period): | ||
parsedData['schedTime']=strptime(pythonDOW + subdata[1][0:4],"%w%H%M") | ||
parsedData['setpointHeating']=subdata[1][4:6] | ||
parsedData['setpointCooling']=subdata[1][6:8] | ||
parsedData['period']=period | ||
return parsedData | ||
def getdaysched(self, day): | ||
# Return an array with the daily schedule for the given day (given numerically where Sun=1, Sat=7) | ||
daysched = {} | ||
for i in range(1,5): | ||
daysched[i]=self.getperiodsched(day, i) | ||
return daysched | ||
def getweeksched(self): | ||
# Return an array with the complete weekly schedule (where Sun=1, Sat=7) | ||
weeksched = {} | ||
for i in range(1,8): | ||
weeksched[i]=self.getdaysched(i) | ||
return weeksched | ||
def setperiodsched(self, schedule): | ||
# Accepts an array with the entries for a given day and period in the same format as getperiodsched returns | ||
# Note that schedTime is in the python struct_time format (with only hour, minute and weekday used) | ||
day=int(strftime("%w",schedule['schedTime']))+1 | ||
time=strftime("%H%M",schedule['schedTime']) | ||
status = self.sendBasicSet(tstat_address,local_address,self.commands['scheduleentry'] + str (day) + '/' + str(schedule['period']),time + schedule['setpointHeating'] + schedule['setpointCooling']) | ||
return | ||
def setdaysched(self, schedule): | ||
# Accepts a multi-dimensional array with the entries for all periods of a given day in the same format as getdaysched returns | ||
# Note that schedTime is in the python struct_time format (with only hour, minute and weekday used) | ||
daysched = {} | ||
for i in range(1,5): | ||
self.setperiodsched(schedule[i]) | ||
return | ||
def setweeksched(self, schedule): | ||
# Accepts a multi-dimensional array with the entries for all periods of all days in the same format as getweeksched returns | ||
# Note that schedTime is in the python struct_time format (with only hour, minute and weekday used) | ||
daysched = {} | ||
for i in range(1,5): | ||
self.setdaysched(schedule[i]) | ||
return |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
#! /usr/bin/python | ||
|
||
import pystat | ||
import sys | ||
import string | ||
from optparse import OptionParser | ||
from lxml import etree | ||
from time import localtime, strftime, strptime | ||
|
||
def convertWeekToXML(weekSched): | ||
# Returns an XML etree version of the weekSched array | ||
root = etree.Element("schedule") | ||
day = {} | ||
day_number = {} | ||
day_name = {} | ||
|
||
for i in weekSched: | ||
day[i] = etree.SubElement(root, "day") | ||
# Get the current day number from the time_struct of the first period | ||
day_number[i] = etree.SubElement(day[i], "day_number").text = str(int(strftime("%w",weekSched[i][1]['schedTime']))+1) | ||
# Save the current day name to make the XML more human-readable but this is NOT used anythere, only the day_number is! | ||
day_name[i] = etree.SubElement(day[i], "day_name").text = strftime("%A",weekSched[i][1]['schedTime']) | ||
|
||
period = {} | ||
period_number = {} | ||
period_setpointHeating = {} | ||
period_setpointCooling = {} | ||
period_schedTime = {} | ||
|
||
for k in weekSched[i]: | ||
period[k] = etree.SubElement(day[i], "period") | ||
period_number[k] = etree.SubElement(period[k], "period_number").text = str(weekSched[i][k]['period']) | ||
period_schedTime[k] = etree.SubElement(period[k], "schedTime").text = strftime("%H%M",weekSched[i][k]['schedTime']) | ||
period_setpointHeating[k] = etree.SubElement(period[k], "setpointHeating").text = str(weekSched[i][k]['setpointHeating']) | ||
period_setpointCooling[k] = etree.SubElement(period[k], "setpointCooling").text = str(weekSched[i][k]['setpointCooling']) | ||
|
||
return root | ||
|
||
def convertXMLToWeek(weekXML): | ||
# Returns a week-style array of the XML data | ||
weekSched = {} | ||
i = 1 | ||
for day in weekXML: | ||
k = 1 | ||
daySched = {} | ||
for period in day: | ||
periodSched = {} | ||
# Day number is here | ||
if period.tag == 'day_number': | ||
day_number = int(period.text) | ||
for element in period: | ||
# Period number, time and setpoints are here | ||
if element.tag == 'period_number': | ||
periodSched['period'] = int(element.text) | ||
if element.tag == 'setpointCooling': | ||
periodSched['setpointCooling'] = element.text | ||
if element.tag == 'setpointHeating': | ||
periodSched['setpointHeating'] = element.text | ||
if element.tag == 'schedTime': | ||
schedTime = element.text | ||
pythonDOW = str(day_number-1) | ||
periodSched['schedTime'] = strptime(pythonDOW + schedTime,"%w%H%M") | ||
if period.tag == 'period': | ||
daySched[k] = periodSched | ||
k += 1 | ||
weekSched[i] = daySched | ||
i += 1 | ||
return weekSched | ||
|
||
def saveSched(fileName): | ||
t = pystat.thermostat('/dev/ttyUSB0') | ||
weeksched=t.getweeksched() | ||
t.close() | ||
etree.ElementTree(convertWeekToXML(weeksched)).write(fileName, pretty_print=True) | ||
return | ||
|
||
def loadSched(fileName): | ||
weekXML = etree.parse(fileName).getroot() | ||
t = pystat.thermostat('/dev/ttyUSB0') | ||
t.setweeksched(convertXMLToWeek(weekXML)) | ||
t.close() | ||
return | ||
|
||
def main(): | ||
usage = "usage: %prog [options]" | ||
parser = OptionParser(usage=usage, version="%prog 0.1") | ||
parser.add_option("-s", "--save", | ||
action="store", type="string", dest="saveFile", | ||
metavar="FILE", help="Save the current weekly schedule to FILE") | ||
parser.add_option("-l", "--load", | ||
action="store", type="string", dest="loadFile", | ||
metavar="FILE", help="Load the weekly schedule from FILE") | ||
(options, args) = parser.parse_args() | ||
if options.saveFile: | ||
saveSched(options.saveFile) | ||
elif options.loadFile: | ||
loadSched(options.loadFile) | ||
|
||
if __name__ == "__main__": | ||
main() | ||
|
Oops, something went wrong.