Raspberry Pi controlling AC Infinity Booster Fans (UIS)

We recently switched to geothermal for our heating/cooling which look like it’s lowering our bill by around 40%. We had two systems before feeding the house and there was a huge variability in heating and cooling, especially in our upstairs bedrooms, which have vaulted ceilings.

I was using AC infinity register fans to try boost those rooms when cooling or heating was in use, and they did fine, but were rather dumb.

So, in order to lower noise and control the flow rate of air better, I have installed three AC Infinity cloud line fans inline with ducts feeding each of three bedrooms.

We have a Venstar Colortouch thermostat controlling our ClimateMaster geothermal unit, and the reason I chose that particular brand was that it supported multiple stages AND multiple remote sensors. I have a sensor in each of the major rooms and the thermostat uses the average of those to determine heat/cooling. The color touch also has a local web api where I can pull the current temperature in each room, as well as the status of the thermostat:

/query/info

{"name":"MAIN","mode":1,"state":1,"fan":1,"fanstate":1,"tempunits":0,"schedule":0,"schedulepart":255,"away":0,"spacetemp":68.0,"heattemp":71.0,"cooltemp":70.0,"cooltempmin":35.0,"cooltempmax":99.0,"heattempmin":35.0,"heattempmax":99.0,"activestage":1,"hum_active":0,"hum":26,"hum_setpoint":0,"dehum_setpoint":99,"setpointdelta":2.0,"availablemodes":0}

/query/sensors
{"sensors":[{"name":"Thermostat","temp":72.0,"hum":26},{"name":"Space Temp","temp":68.0},{"id":4,"name":"Livingroom ","temp":70.0,"battery":98,"type":"Remote"},{"id":3,"name":"Ella","temp":70.0,"battery":98,"type":"Remote"},{"id":2,"name":"Boys","temp":68.0,"battery":98,"type":"Remote"},{"id":1,"name":"Master","temp":70.0,"battery":98,"type":"Remote"},{"id":5,"name":"Outdoor","temp":32.0,"battery":100,"type":"Outdoor"},{"id":6,"name":"Dining Room","temp":70.0,"battery":98,"type":"Remote"},{"id":7,"name":"Gallery","temp":65.0,"battery":100,"type":"Remote"}]}

So now, what. wanted to do was have a Raspberry Pi query the sensors, and determine which of them was under or over temperature and adjust the AC Infinity fans for those rooms to compensate, based on how bad the offset was.

I used an oscilloscope to check which of the AC infinity lines was controlling the fan speed (its the yellow one) and gather data on the method used. Its a PWM control, where by default the yellow line is pulled high (10v) and in the case there is no attached controller, and the line is left to be high, we get 100% fan speed.

Below are the outputs at levels 1,3 and 10 using the AC Infinity stock controller:

So, the yellow like floats high and in order to control the fan speed we must pull it low for whatever amount of time we need to create the duty cycle we need.

The Raspberry Pi only has a single PWM line and coding multiple controls using direct GPIO would be a losing battle, so I selected and I2C based PWM controller board (https://www.amazon.com/gp/product/B082QT9D5F) in order to be able to simply communicate every so often and let the board do its thing.

I used the USB C breakout boards from amazon: https://www.amazon.com/dp/B09KC1SMGD to break out the “USB C’ connector that AC infinity uses on their fans to get access to the lines themselves:

Now, since the line was going to be coming in at 10v from the fan, when I tried to pull down the voltage directly using these PWM controllers the best I could do was a 5v delta, not the 10v that the fans needed. The result was that at best I could drive them, to 30% power. As a result, I used some MOSFET boards to essentially power up the delta I needed: https://www.amazon.com/gp/product/B07NWD8W26/

The result is a Raspberry Pi with 4 sets of hardware driving currently three AC Infinity fans, one feeding each bedroom:

Code for Raspberry Pi below:

import datetime
import requests
import time
from board import SCL, SDA
import busio
import RPi.GPIO as GPIO
from pathlib import Path
import pigpio
from adafruit_pca9685 import PCA9685
import urllib.request

thermostatIP="10.0.1.40"
monitoredRooms = [{'Name':"Ella",'minSpeed':0,'maxSpeed':4, 'channel':1 , 'tachPin':6 },{'Name':"Boys",'minSpeed':0,'maxSpeed':5, 'channel':0, 'tachPin':13 },{'Name':"Gallery",'minSpeed':0,'maxSpeed':3, 'channel':3, 'tachPin':14 }]
maxDifference = 3


# Create the I2C bus interface.
i2c_bus = busio.I2C(SCL, SDA)

# Create a simple PCA9685 class instance.
pca = PCA9685(i2c_bus)

# Set the PWM frequency to 50hz.
pca.frequency = 50
GPIO.setmode(GPIO.BCM)
LED_PIN = 5
GPIO.setup(LED_PIN, GPIO.OUT)

def isMonitored(submittedRoom):
    global monitoredRooms
    for room in monitoredRooms:
       if(room['Name']==submittedRoom):
            return True
    return False
def minSpeed(submittedRoom):
    global monitoredRooms
    for room in monitoredRooms:
       if(room['Name']==submittedRoom):
            return room['minSpeed']
    return 0
def maxSpeed(submittedRoom):
    global monitoredRooms
    for room in monitoredRooms:
       if(room['Name']==submittedRoom):
            return room['maxSpeed']
    return 0

def fanChannel(submittedRoom):
    global monitoredRooms
    for room in monitoredRooms:
       if(room['Name']==submittedRoom):
            return room['channel']
    return 9


def tachPin(submittedRoom):
    global monitoredRooms
    for room in monitoredRooms:
       if(room['Name']==submittedRoom):
            return room['tachPin']
    return 99

GPIO.output(LED_PIN, GPIO.HIGH)
print("Checking thermostats and adjusting AC Infinity fans...")

# Turn on LED so if someone is watching they can tell something is happening....
GPIO.output(LED_PIN, GPIO.HIGH)

#Default Mode
mode="heat"

#In case this needs to be a loop, I have this rest indented
if (1==1):
    # Go get the data from the Venstar Thermostat - this gives us basics
    thermodata = requests.get('http://' + thermostatIP + '/query/info')
    thermo_dict= thermodata.json()

    # Lets save heat, cool set points, the mode and the heat/cool stage
    targetheat=thermo_dict['heattemp']
    targetcool=thermo_dict['cooltemp']
    mode=thermo_dict['mode']
    stage=thermo_dict['activestage']
    if (mode==1):
        mode="heat"
    if(mode==2):
        mode="cool"

    # Go get the data for all the sensors
    sensordata = requests.get('http://' + thermostatIP + '/query/sensors')
    sensor_dict= sensordata.json()
    sensors=sensor_dict['sensors']

    # Save status to a local file so it can be viewed
    f = open("acinfinitystatus.txt", "w")
    f.write("Thermostat is in " + mode + " mode at stage " + str(stage) + " and set to heat to "+str(targetheat)+" and cool to "+ str(targetcool) + "\n")

    # Search through all rooms to find the "Space Temp" which is the thermostat's average it calculated
    average=0
    for key in sensor_dict["sensors"]:
        roomName=key["name"]
        roomTemp=key["temp"]
        if(roomName=="Space Temp"):
            average=roomTemp
 
    # Now lets look for the highest and lowest temperature readings
    maxTemp=0
    minTemp=100
    for key in sensor_dict["sensors"]:
        roomName=key["name"]
        roomTemp=key["temp"]
        now = datetime.datetime.now()
        if((now.hour>16) | (now.hour<8)):
            # Ignore my outdoor sensor, the average temperature and in my case the thermostat itself.
            if((roomName!="Outside") & (roomName!="Space Temp") & (roomName!="Thermostat")):
                if (roomTemp>maxTemp):
                    maxTemp=roomTemp
                if (roomTemp<minTemp):
                    minTemp=roomTemp
    # 
    f.write("Thermostat average is set to " + str(average) + ". Max temp is " + str(maxTemp)+ " and Min temp is " + str(minTemp)+ "\n")
    
    # If we are heading into the evening, or its still morning, lets weight our average towards the best
    # recorded temperature to keep them cosy (or cool)
    if((now.hour>16) | (now.hour<8)):
        f.write("Its past 4PM or before 8am, so weighting the average...\n")
        if(mode=="heat"):
            # Weight towards the warmest room
            average=(maxTemp+average)/2
        if(mode=="cool"):
            # Weight towards the coolest room
            average=(minTemp+average)/2

        f.write("Weighted average is set to " + str(average) +"\n")
    else:
        f.write("Its daytime, so no weighting...\n")


    # Now, lets parse all rooms, check for ones with AC Infinity fans connected, and set those.
    for key in sensor_dict["sensors"]:
        roomName=key["name"]
        roomTemp=key["temp"]
        if(mode=="cool"):
            delta=roomTemp-average
        if(mode=="heat"):
            delta=average-roomTemp
        if(delta<0):
            delta=0

        # We calculate how much speed we need based on how far off from the average this room is
        # up to a max of 3 degrees

        percentSpeed=delta/maxDifference
        if(percentSpeed>1):
            # Cant set speed to more than 100%
            percentSpeed=1

        # Now calculate that percentage in terms of the range defined for this room
        optimalFanSpeed=(maxSpeed(roomName)-minSpeed(roomName))*percentSpeed+minSpeed(roomName)
        # And since we're pulling down the voltage own the yellow wire, higher means more pulldown
        # so we are reversing the speed and putting it in terms the PWM board understands 0-65535
        # We also buffer because AC infinity fans also assume a dead controller if the value is 
        # 0 OR 65535 - so Im assuming 11 steps and ignoring 0

        optimalFanPWM=65535-round((optimalFanSpeed+1)*11*65535/100)
        roomChannel=fanChannel(roomName)
        # Some code here for me to save this stuff to a database to be able to look at performance
        if(roomName=="Ella"):
            fan_ella=str(100-int(optimalFanPWM/65535*100))
        if(roomName=="Boys"):
            fan_boys=str(100-int(optimalFanPWM/65535*100))
        if(roomName=="Gallery"):
            fan_gallery=str(100-int(optimalFanPWM/65535*100))


        f.write("\n" + roomName.rjust(12) + ": "+ str(roomTemp)+ " ("+ format(delta, '.1f').zfill(4)+ ")  Fan: ["+ str(minSpeed(roomName))+ "-" + str(maxSpeed(roomName))+ "] Optimal: "+ format(optimalFanSpeed, '.0f') +" PWM "+str(optimalFanPWM)+ " = " + str(100-int(optimalFanPWM/65535*100)) + "%")
        if(isMonitored(roomName)):
            f.write(" ****")
            f2 = open("acinfinitylog.txt", "a")
            f2.write(roomName.rjust(12) + ": "+ str(roomTemp)+ " ("+ format(delta, '.1f').zfill(4)+ ")  Fan: ["+ str(minSpeed(roomName))+ "-" + str(maxSpeed(roomName))+ "] Optimal: "+ format(optimalFanSpeed, '.0f') +" PWM "+str(optimalFanPWM)+ " = " + str(100-int(optimalFanPWM/65535*100)) + "%\n")
            f2.close()
            pca.channels[roomChannel].duty_cycle = optimalFanPWM



    postDbURL="http://10.0.1.10/environmentals/acinfinity.php?ella=" + fan_ella + "&boys=" + fan_boys + "&gallery=" + fan_gallery
    if(datetime.datetime.now().minute % 5 == 0):
        # Post data to a database so I can view graphs
        contents = urllib.request.urlopen(postDbURL)
    f.write("\n\n")
    f.close()
    GPIO.output(LED_PIN, GPIO.LOW)

The result of all of this, is a fantastic dashboard for keeping an eye on my heating/cooling system:

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s