Now that I have a passing understanding of queues and threading, I have redesigned the script to use a thread for the serial communications. In earlier version, I had several functions that did handoffs via semaphores for various actions. For the most part, user input, background updates and error correction are all stacked in a priority queue and dealt with by the serialTasks function.
I also had some complex code that determined which system / zone are being worked on -- I have basically eliminated this check and assume the commands being issued are correct for the current zone.
Now that the serialTasks function is running without delays, it can work through the HVAC's entire command set of one system and eight zones in a little over two minutes. If you had all of the options (humidifier, UV light, etc. and a second System (16 zones)), it looks like it would take about 4.5 minutes to go through all of the functions.
I removed almost all of my regular expression code and replaced them with str functions. This wound up being a lot easier for me to debug.
- Code: Select all
#! /usr/bin/python
# Bryant Evolution Connex / Carrier Infinity script for Indigo
#
#
#
# Requirements:
#
# Bryant Evolution Connex or Carrier Infinity Communicating HVAC System
# System Access Module (SAM) SYSTXCCRCT01 or SYSTXCCSAM01 (PERHAPS OTHERS)
# StarTech NETRS2321P IP to serial module
#
# Remote Access Protocol Documentation: http://dms.hvacpartners.com/docs/1009/Public/02/APSAM01-01.pdf
import socket
import re
import time
import datetime
import indigo
import sys
import threading
import Queue
loopTime = datetime.datetime.now()
# Connex / Infinity can support two systems. Change to ("S1", "S2") if this applies. Comma MUST be present even if only one system
arrSys = (
"S1",
# "S2"
)
# Global system numeric variables. Comment out unavailable features. Remember NO COMMA after last variable.
arrGloNum = (
"Z1RH", # Relative Humidity. Zone 1 is only zone that reports humidity
"OAT", # Outside Air Temperature
# "Z1RHTG", # Relative Humidity Target
"FILTRLVL", # Filter life used
"VACDAYS", # Number of Vacation Days
"VACMINT", # Vacation Minimum Temp
"VACMAXT", # Vacation Maximum Temp
"VACMINH", # Vacation Minimum Humidity
"VACMAXH", # Vacation Maximum Humidity
"CFGDEAD", # Thermostat deadband
# "CFGCPH", # Maximum thermostat cycles per hour (Not useful on Evolution Connex)
# "HUMLVL", # Humidifier pad life
# "UVLVL" # UV lamp life
"ZONE" # Zone displayed on touchscreen
)
# Global system text variables
arrGloTxt = (
# "ZONE",
"MODE", # System Mode (HEAT, COOL, AUTO, OFF, EHEAT)
"HUMID", # Humidifier state
"BLIGHT", # System LCD Backlight
"FILTRRMD", # Filter reminder
"VACFAN", # Vacation fan setting (AUTO, LOW, MED, HIGH)
"CFGEM", # English / Metric setting (Send E or M, returns F or C)
"CFGAUTO", # System Automatic mode
"CFGTYPE", # System type (COOL, HEAT, HEATCOOL)
"CFGPGM", # Programing enabled?
"DEALER", # Dealer name
"DEALERPH", # Dealer phone number
"DAY", # Day of Week
"TIME", # Time of Day
# "CFGFAN", # Programmable Fan Setting (Not useful on Evolution Connex)
# "UVRMD", # UV Reminder setting
# "HUMRMD" # Humidity pad reminder setting
"VACAT" # Vacation state
)
arrZoneNumPriority = (
"RT", # Displayed zone room temperature
)
# Zone numeric variables
arrZoneNum = (
"HTSP", # Zone heat setpoint
"CLSP" # Zone cool setpoint
)
# Zone text variables
arrZoneTxtPriority = (
"HOLD", # HOLD ON sets zone into HOLD permanent. OFF turns HOLD off.
"UNOCC", # UNOCC ON sets Zone into AWAY state HOLD permanent. OFF sets into HOME state HOLD permanent. (Use HOLD OFF to turn off UNOCC)
"OVR", # State of local (On the Thermostat) override timer
"FAN" # Set fan speed (AUTO, LOW, MED, HIGH). AUTO turns continuous fan off.
)
arrZoneTxt = (
"OTMR", # Local override time remaining in hours and minutes.
"NAME", # Zone's name
)
arrSetupVars = (
"hvacStop",
"hvacBusyBackground",
"hvacBusyForeground",
"hvacForegroundTask",
"hvacForegroundState",
"hvacForegroundZone",
"hvacDefaultOverrideTime",
"hvacLastError",
"hvacLastCommand",
"hvacLastCommandTime",
"hvacLastCommandSent",
"hvacCleanExit",
"hvacZoneUpdatePriority",
"hvacForegroundSystem",
"hvacLastErrorTime",
"hvacLastLoopTime",
"hvacLastErrorMessage",
"hvacQueueSize"
)
hvacManufacturer = "Bryant" # Change to Carrier if you have OCD.
# Script works the same.
try:
indigo.variables.folder.create(hvacManufacturer+" HVAC") # Create Indigo variable folder if necessary
except ValueError:
pass
try:
indigo.variables.folder.create(hvacManufacturer+" HVAC Control") # Create Indigo control variable folder if necessary
except ValueError:
pass
#
# Create Indigo Variables if necessary.
#
for i in arrSetupVars:
try:
indigo.variable.create(i, value="", folder=str(hvacManufacturer+" HVAC Control")) # Create control variables if necessary
except ValueError:
indigo.variable.updateValue(i, value="")
indigo.variable.updateValue("hvacStop", str(False))
indigo.variable.updateValue("hvacBusyForeground", str(False))
indigo.variable.updateValue("hvacBusyBackground", str(False))
indigo.variable.updateValue("hvacDefaultOverrideTime", value="01:00")
indigo.variable.updateValue("hvacForegroundSystem", value="S1")
indigo.variable.updateValue("hvacLastLoopTime", value="")
End = "\r\n" # Each message from HVAC terminates in CRLF. Used to ensure data packet is completely received.
HOST2 = 'hvac.bollar.com'
PORT2 = 23
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(7.0)
s.connect((HOST2, PORT2))
#
# Define Queue
#
# q = Queue.Queue(maxsize=0)
pq = Queue.PriorityQueue(maxsize=0)
class job(object):
def __init__(self, priority, description):
self.priority = priority
self.description = description
return
def __cmp__(self, other):
return cmp(self.priority, other.priority)
#
# Get Foreground Tasks raised by Indigo. Note Queue is passed as (q).
#
def foregroundTasks(pq):
indigo.server.log("HVAC foregroundTasks : Starting...")
foreCommand = ""
zone = ""
system = ""
state = ""
while string.capitalize(indigo.variables["hvacStop"].value) == str(False):
try:
if string.capitalize(indigo.variables["hvacBusyForeground"].value) == str(True):
task = (indigo.variables["hvacForegroundTask"].value)
zone = (indigo.variables["hvacForegroundZone"].value)
system = (indigo.variables["hvacForegroundSystem"].value)
state = (indigo.variables["hvacForegroundState"].value)
indigo.variable.updateValue("hvacForegroundTask", value="")
indigo.variable.updateValue("hvacForegroundZone", value="")
indigo.variable.updateValue("hvacForegroundState", value="")
indigo.variable.updateValue("hvacBusyForeground", str(False))
if int(zone) > 0:
foreCommand = system + "Z" + zone + "OVR" + "?"
pq.put(job(10, foreCommand))
if indigo.variables["hvac"+system+"Z"+str(zone)+"OVR"].value == "OFF":
foreCommand = system + "Z" + zone + task + "!" + state
pq.put(job(11, foreCommand))
foreCommand = system + "Z" + zone + "HTSP" + "?"
pq.put(job(12, foreCommand))
foreCommand = system + "Z" + zone + "CLSP" + "?"
pq.put(job(12, foreCommand))
foreCommand = system + "Z" + zone + "RT" + "?"
pq.put(job(12, foreCommand))
foreCommand = system + "Z" + zone + "HOLD" + "?"
pq.put(job(14, foreCommand))
foreCommand = system + "Z" + zone + "UNOCC" + "?"
pq.put(job(14, foreCommand))
foreCommand = system + "Z" + zone + "FAN" + "?"
pq.put(job(14, foreCommand))
indigo.server.log("HVAC foregroundTasks Queue:" + foreCommand + " " + str(pq.qsize()))
else:
indigo.server.log("Zone Override - Ignoring Command :" + foreCommand)
else:
foreCommand = system + task + "!" + state
pq.put(job(10, foreCommand))
indigo.variable.updateValue("hvacQueueSize", value=str(pq.qsize()))
indigo.variable.updateValue("hvacZoneUpdatePriority", str(True))
else:
time.sleep(0.1)
except ValueError:
pass
except:
e = sys.exc_info()[0]
time.sleep(10.0)
indigo.server.log("HVAC foregroundTasks: " + str(e))
indigo.variable.updateValue("hvacLastError", value=str(response))
indigo.variable.updateValue("hvacLastErrorMessage", value=str(e))
indigo.variable.updateValue("hvacLastErrorTime", value=str(datetime.datetime.now()))
pass
finally:
pass
indigo.server.log("HVAC foregroundTasks : Stopping...")
#
# Send Serial Tasks raised by Indigo. Note Queue is passed as (pq).
#
def serialTasks(pq):
indigo.server.log("HVAC serialTasks : Starting...")
while string.capitalize(indigo.variables["hvacStop"].value) == str(False):
if pq.qsize() > 0:
try:
queueCommand = pq.get()
foreCommand = queueCommand.description
indigo.variable.updateValue("hvacLastCommandSent", value=foreCommand)
indigo.variable.updateValue("hvacQueueSize", value=str(pq.qsize()))
s.sendall(foreCommand + End)
response = receiveHVACValue(s)
p = re.split(":", response, maxsplit=1) # Breaks response into two groups with : as a delimiter
# indigo.server.log("HVAC serialTasks: p = " + str(p[0]) + " " + str(p[1]))
cmd = str(p[0])
rtn = str(p[1])
r = filter(str.isdigit, rtn)
if rtn == "NAK CMD":
pq.put(job(8, foreCommand))
indigo.server.log("HVAC serialTasks: Returned NAK CMD: " + str(foreCommand) + " " + str(response))
if rtn == "NAK VAL":
# pq.put(job(8, foreCommand))
indigo.server.log("HVAC serialTasks: Returned NAK VAL: " + str(foreCommand) + " " + str(response))
if rtn == "NAK":
pq.put(job(8, foreCommand))
indigo.server.log("HVAC serialTasks: Returned NAK: " + str(foreCommand) + " " + str(response))
else:
if r != "":
indigo.variable.updateValue("hvac"+cmd, value=r)
else:
indigo.variable.updateValue("hvac"+cmd, value=rtn)
except ValueError:
pq.put(job(8, foreCommand))
# indigo.server.log("HVAC serialTasks: Returned ValueError: " + str(foreCommand) + " " + str(response))
# indigo.variable.create("hvac"+str(m.group(0)), value=str(m.group(1)), folder=str(hvacManufacturer+" HVAC"))
except IndexError:
pq.put(job(8, foreCommand))
# indigo.server.log("HVAC serialTasks: Returned IndexError: " + str(foreCommand) + " " + str(response))
except socket.timeout:
pq.put(job(8, foreCommand))
# indigo.server.log("HVAC serialTasks: Returned socket.timeout: " + str(foreCommand) + " " + str(response))
except:
e = sys.exc_info()[0]
time.sleep(0.1)
indigo.server.log("HVAC serialTasks: " + str(e))
indigo.server.log("HVAC serialTasks: Returned Other Errors: " + str(foreCommand) + " " + str(response))
indigo.variable.updateValue("hvacLastError", value=str(response))
indigo.variable.updateValue("hvacLastErrorMessage", value=str(e))
indigo.variable.updateValue("hvacLastErrorTime", value=str(datetime.datetime.now()))
pass
finally:
foreCommand = ""
indigo.variable.updateValue("hvacLastCommand", value=response)
indigo.variable.updateValue("hvacLastCommandTime", value=str(datetime.datetime.now()))
# indigo.server.log("HVAC serialTasks Response: " + response)
indigo.variable.updateValue("hvacBusyForeground", str(False))
else:
time.sleep(0.1)
# indigo.server.log("HVAC serialTasks waiting...")
indigo.server.log("HVAC serialTasks : Stopping...")
def getHVACValue(system, zone, command, commandText):
# indigo.server.log("HVAC getHVACValueLocal foreCommand: " + foreCommand + " " + str(q.qsize()))
backCommand = ""
while pq.qsize() > 0:
if string.capitalize(indigo.variables["hvacStop"].value) == str(True):
return()
time.sleep(0.1)
indigo.variable.updateValue("hvacBusyBackground", str(True))
#
# Zone Specific Commands
#
if zone > 0:
backCommand = system+"Z"+str(zone)+command+"?"
else:
backCommand = system+command+"?"
pq.put(job(20, backCommand))
indigo.variable.updateValue("hvacLastCommandTime", value=str(datetime.datetime.now()))
backCommand = ""
indigo.variable.updateValue("hvacLastCommandTime", value=str(datetime.datetime.now()))
indigo.variable.updateValue("hvacBusyForeground", str(False))
return()
#
# Receives and parses data returned from HVAC
# http://code.activestate.com/recipes/408859-socketrecv-three-ways-to-turn-it-into-recvall/
#
def receiveHVACValue(s):
total_data = []
response = ""
while True:
data = unicode(s.recv(32), errors="ignore")
if End in data:
total_data.append(data[:data.find(End)])
response = "".join(total_data)
break
total_data.append(data)
if len(total_data)>1:
#check if end_of_data was split
last_pair=total_data[-2]+total_data[-1]
if End in last_pair:
total_data[-2]=last_pair[:last_pair.find(End)]
total_data.pop()
break
return(response)
#
# Set up thread to get foreground tasks from Indigo. Note it calls the Queue (q) as an arg.
#
t1 = threading.Thread(name='t1', target=foregroundTasks, args=(pq,))
t1.daemon = True
t1.start()
t2 = threading.Thread(name='t2', target=serialTasks, args=(pq,))
t2.daemon = True
t2.start()
#
# Main loop. Cycles through each zone and global command in sequence. Each command takes about five seconds to execute.
#
# g = System
# i = Command
# x = Zone
while string.capitalize(indigo.variables["hvacStop"].value) == str(False):
indigo.variable.updateValue("hvacZoneUpdatePriority", str(False))
tDelta = datetime.datetime.now() - loopTime
indigo.variable.updateValue("hvacLastLoopTime", value=str(tDelta))
loopTime = datetime.datetime.now()
for g in arrSys:
for i in arrZoneNumPriority:
for x in range (1, 9):
getHVACValue(g, x, i, False)
for i in arrZoneNum:
for x in range (1, 9):
if string.capitalize(indigo.variables["hvacZoneUpdatePriority"].value) == str(False):
getHVACValue(g, x, i, False)
else:
pass
for i in arrZoneTxtPriority:
for x in range (1, 9):
getHVACValue(g, x, i, True)
for i in arrZoneTxt:
for x in range (1, 9):
if string.capitalize(indigo.variables["hvacZoneUpdatePriority"].value) == str(False):
getHVACValue(g, x, i, True)
else:
pass
for i in arrGloNum:
for x in range (1, 2):
if string.capitalize(indigo.variables["hvacZoneUpdatePriority"].value) == str(False):
getHVACValue(g, 0, i, False)
else:
pass
for i in arrGloTxt:
if string.capitalize(indigo.variables["hvacZoneUpdatePriority"].value) == str(False):
getHVACValue(g, 0, i, True)
else:
pass
s.close()
indigo.variable.updateValue("hvacStop", str(False))
indigo.variable.updateValue("hvacBusyBackground", str(False))
indigo.variable.updateValue("hvacBusyForeground", str(False))
indigo.variable.updateValue("hvacCleanExit", str(True))
indigo.server.log(hvacManufacturer + " HVAC Stopped")