Controlling a portable AC

Discuss garden automation systems and software here, including commercial products or Raspberry Pi and Arduino DIY setups.
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

I have a portable AC unit, the kind that's on casters. I'm looking to integrate it into my grow. I've taken the plastic shroud off and I can visualize a simple way of piping the grow air thru the evaporator coil. The goal is to have it sitting outside the lung room, in the attic where it can vent hot exhaust, and with the proper combination of ducts & plumbing divert grow air thru the evaporator and back into the grow.

The tricky part is how to control it from the environmental control software that's running the grow (Mycodo).
I know there are many who simply program an ESP to act as a remote control and send IR signals to the AC to control it but I don't like that method, too unreliable without feedback from the AC about what state it's in.

So I am looking to replace the ACs' built-in circuit board with a relay board controlled by an ESP8266. I've searched and can't seem to find anyone else who has done similar so it seems to be uncharted territory...

Here is the circuit board of the AC unit:
IMG_20211113_104643.jpg
And the included schematic for this particular AC:
IMG_20211113_104615.jpg
At the very least, it seems I need 4 relays:
- One relay for the compressor
- One for the "UP FAN" which is the fan that draws air through the evaporator
- One for the "DOWN FAN" which is the fan that draws air through the condenser.
- One for the motor involved with the condensate. I suspect when the circuit detects condensate is accumulating at the bottom, it turns on the flicker motor which I believe throws condensate onto the condenser coil, which is normally hot, and evaporates the condensate to remove it through the exhaust. I might consider removing this complexity and have the condensate drain to waste instead, this would prevent exhausting humidity into the attic.

On the input side, there are two temperature sensors in this AC unit. One monitors the ambient air temperature. This data will be replaced with the canopy temperature instead. The other temperature sensor is attached to the evaporator coil, which I believe is to check to make sure that the coils aren't frozen over in a block of ice from running the compressor too long. I will want to keep that one functional.

Uncharted territory, there be dragons...
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Found this:

https://www.openhardware.io/view/8280/V ... Controller
Someone's already put together something that does far more than what I need
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Another similar project. https://hackaday.io/project/182589/
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Something like this.

Code: Select all

...
I think it needs some code to monitor the duty cycle of the pump, but considering I'm using this to cool a 240W LED I seriously doubt the pump will run anywhere near its' limits.

Waiting for some new relays to get here to pieces this together.
I'm using a Heltec WIFI KIT 32, which is an esp32 with a small LED screen.
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Cleaned up a bit

Code: Select all

// espAC
// control a heat pump

//this device
#define espName "espAC"
int interval = 1000; //loop delay ms
unsigned long lastLoop=0;

//For display of Wifi Kit 32
#include "heltec.h"
char line[64];

//WIFI Stuff
#include <WiFi.h>
const char* ssid = "*";
const char* password = "*";
IPAddress   staticIP(192,168,2,54);
IPAddress   gateway(192,168,0,1);
IPAddress   subnet(255,255,0,0);

//MQTT Client stuff
#include <PubSubClient.h>
WiFiClient    espClient;
PubSubClient  mqtt(espClient);
const char*   mqtt_server = "192.168.2.1";
char          tempString[8];
char          topic[32];
unsigned char payload[64];

//MAX31855 Thermocouple stuff
#include <SPI.h>
#include "Adafruit_MAX31855.h"
SPIClass hspi;
Adafruit_MAX31855 probe(5, &hspi); //pin 5 = V_SPI_CS0 to CS on 31855, pin 18=SCK to SCKon 31855, pin 19=MISO->SO on 31855

//HeatPump stuff
class HeatPump {
private:
  
// GPIOs
  #define coldFanGPIO 10
  #define hotFanGPIO  11
  #define compressorGPIO  12
  //MOSI 23
  //MISO 19
  //SCK 18
  //SS 5

// Internal
  double coilTemp = 0;
  double coilTargetTemp = 0;
  double airTemp = 0;
  double airHumi = 0;
  double h, airDew = 0;
  bool compressor = false;
  bool hotFan = false;
  bool coldFan = false;
  bool running = false;
  int runtime = 0;
  int dutytime = 0;
   
  typedef enum { // HP_State_t
    none = 'n',
    idle_State = 'i',   //Idle, off...
    fan_State = 'f',    //just the fan
    cold_State = 'c',     //pump the cool
    //pumpHold_State = 'p', //cool the pump
    //duty_State = 'd',  //when we exceed duty cycle
    
  } HP_State_t;

  typedef enum { // HP_Event_t
    noneEvent,
    StartFan,
    StopFan,
    StartCold,
    StopCold,
    StartDehum,
    StopDehum,
  } HP_Event_t;
  
  HP_State_t myState=none, nextState=idle_State;
  HP_Event_t myEvent=noneEvent;

  inline void getcoilTemp() {
      //K-type probe?
      //MAX31855

    coilTemp = probe.readCelsius();
    if (isnan(coilTemp))
      Serial.println("Something wrong with thermocouple!");
  }

  inline void calcDew() {
    h = (log10(airHumi)-2)/0.4343 + (17.62 * airTemp)/(243.12+airTemp);

    airDew = 243.12*h/(17.62-h);
    dtostrf(airDew, 4, 1, tempString);
    mqtt.publish("espAC/airDew", tempString);
  }

  inline void calcTargetTemp() {
    coilTargetTemp = airDew + 0;
  }
  
  inline void setAirHumi(double n) {
    airHumi = n;
    calcDew();
  }

  inline void setAirTemp(double n) {
    airTemp = n;
    calcDew();
  }
  
  inline void startHotFan(){
    digitalWrite(hotFanGPIO, HIGH);
    hotFan = true;
  }
  
  inline void stopHotFan(){
    digitalWrite(hotFanGPIO, LOW);
    hotFan = false;
  }
  
  inline void startFan(){
    digitalWrite(coldFanGPIO, HIGH);
    coldFan = true;
  }

  inline void stopFan(){
    digitalWrite(coldFanGPIO, LOW);
    coldFan = false;
  }
  
  inline void startPump() {
    digitalWrite(compressorGPIO, HIGH);
    compressor = true;
}

  inline void stopPump() {
    digitalWrite(compressorGPIO, LOW);
    compressor = false;
  }

  inline void startCold() {
    //Full on AC
    //set target coil temp to 0C
    coilTargetTemp = 0;
    nextState = cold_State;
  }

  inline void startDehum() {
    //Dehumidifier 
    //set target coil temp to dewpoint+calibration
    coilTargetTemp = airDew + 0;
    nextState = cold_State;  
  }

  inline void screenPreLoop() {
    Heltec.display->clear();
    Heltec.display->drawString(0,0, String("myState:")+ (char)myState);
    
    sprintf(line, "T:%2.f H:%2.f D:%2.f C:%2.f", airTemp, airHumi, airDew, coilTemp);
    Heltec.display->drawString(0,10, line);
    
    Heltec.display->drawString(0,20, String("coilTargT: ") + coilTargetTemp );
    
    Heltec.display->drawString(0,30, String("FAN:") + coldFan + String(" HFAN:")+hotFan + String(" PUMP:")+compressor);
  }

  inline void screenPostLoop() {
    Heltec.display->display();
  }

public:

  void callback(char* topic, byte* payload, unsigned int length) {
    
    //I want to use strcat(espName, "/fan") but something's fucky
    if (0 == strcmp(topic, "espAC/fan"))
      if(payload[0] == '1') 
        myEvent = StartFan;
      else if (payload[0]=='0')
        myEvent = StopFan;
      else
        Serial.println("unhandled payload");


    else if (0 == strcmp(topic, "espAC/cold"))
      if(payload[0] == '1')
        myEvent = StartCold;
      else if (payload[0]=='0')
        myEvent = StopCold;
      else
        Serial.println("unhandled payload");

    else if (0 == strcmp(topic, "espAC/dehum"))
      if(payload[0] == '1')
        myEvent = StartDehum;
      else if (payload[0]=='0')
        myEvent= StopDehum;
      else
        Serial.println("unhandled payload");

    else if (0 == strcmp(topic, "canopy/humidity"))      
      setAirHumi(atof((char *)payload)); //no sanity checking
   
    else if (0 == strcmp(topic, "canopy/temperature"))
      setAirTemp(atof((char *)payload)); //no sanity checking

    else {
      Serial.print("Unhandled callback topic: ");
      Serial.println(topic);
    }
  
    memset(payload, 0 , length); //wtf buggy mqtt library not clearing previous payload?
}
  
  HeatPump() {
    pinMode(coldFanGPIO, OUTPUT);
    pinMode(hotFanGPIO, OUTPUT);
    pinMode(compressorGPIO, OUTPUT);

    digitalWrite(coldFanGPIO, LOW);
    digitalWrite(hotFanGPIO, LOW);
    digitalWrite(compressorGPIO, LOW);

    SPI.begin(18, 19, 23, 5); //no checking for error
    probe.begin(); //ditto
        
    //todo NVRAM to resume state after unexpected poweroff? 

    }
 
  void loop() {
    screenPreLoop();
 
    myState = nextState;
    
    switch(myState) {
      case none: {
        break;
      }
      case idle_State: {
        switch(myEvent) {
          case StartFan: {
            startFan();
            nextState = fan_State;
            break;
          }
          case StartCold: {
            startCold();
            break;
          }
          case StartDehum: {
            startDehum();
            break;
          }
          case noneEvent: {
            //Serial.println("Idle, no event");
            break;
          }
          default:
            Serial.println("Unhandled event");
        }
        break;
      }
      case fan_State: {
        
        switch(myEvent) {
          case StopFan: {
            stopFan();
            nextState = idle_State;
            break;
          }
          case StartCold: {
            startCold();
            break;
          }
          case StartDehum: {
            startDehum();
            break;
          }
          default:
            Serial.println("Unhandled event");
        }
        //fan goes brrrr
        break;
      }
      case cold_State: {
        switch(myEvent) {
          case StopCold: {
            stopPump();
            stopFan();
            stopHotFan();//might need a delayed turnoff?
            nextState = idle_State;
            goto stopper;
          }
          case StartCold: {
            coilTargetTemp = 0;
            break;
          }
          case StartDehum: {
            calcTargetTemp();
            break;
          }
          default:
            Serial.println("Unhandled event");
        }

        //no new events to process, pump goes brrrrr?

        if(coilTargetTemp) calcTargetTemp();
        
        getcoilTemp();

        startFan();
        
        startHotFan();
        
        if(coilTargetTemp < coilTemp + 0) startPump();
        else stopPump();

        break;
       }
  
      default:
        Serial.println("unhandled state");
    }
    
    stopper:
    
    myEvent = noneEvent;

    screenPostLoop();
  }
  
} *myFirstAC; //class HeatPump

//////////////////////////////////////// SETUP CODE
void led_setup() {
  Heltec.begin(true /*DisplayEnable Enable*/, false /*LoRa Enable*/, true /*Serial Enable*/);
  Heltec.display->clear();
  Heltec.display->drawString(0,0, "Hello World!");
  Heltec.display->display();
}

void wifi_setup() {
  Heltec.display->drawString(0,10,"wifi...");
  Heltec.display->display();
  char i =0;
  WiFi.persistent(false);
  WiFi.begin(ssid, password);
  WiFi.config(staticIP, gateway, gateway, subnet);

  while (WiFi.status() != WL_CONNECTED & i < 25) {
    i++;
    delay(100);
  }

  Serial.print("i:");
  Serial.println(i, DEC);
 
  Heltec.display->drawString(16,10,"...done");
  Heltec.display->display();
}

void mqtt_setup() {
  Heltec.display->drawString(60,10,"mqtt..");
  Heltec.display->display();
  
  mqtt.setServer(mqtt_server, 1883);

  mqtt.setCallback(callback);

  mqtt_connect(); //might not return?

  mqtt.subscribe("espAC/setInterval");

  mqtt.subscribe("espAC/fan");

  mqtt.subscribe("espAC/cold");
  mqtt.subscribe("espAC/dehum");

  mqtt.subscribe("canopy/temperature");
  mqtt.subscribe("canopy/humidity");

  Heltec.display->drawString(90,10,"..done");
  Heltec.display->display();
}

void mqtt_connect() {
  while (!mqtt.connected())
    if (mqtt.connect(espName)) {}
    else delay(1000); //potential infinit loop here
}

void callback(char* topic, byte* payload, unsigned int length) {

  if(0) {
    Serial.print("callback topic: ");
    Serial.print(topic);
    Serial.print(" payload: ");
    Serial.println((char *) payload);
    //Serial.print(sizeof( (char *)payload));
  }

  if (0 == strcmp(topic, "espAC/setInterval"))
    interval = atoi((char *)payload); //no sanity checking here
  else
    myFirstAC->callback(topic, payload, length);
}

void setup() {
  Serial.begin(115200);
  Serial.println("\n\n\n");

  led_setup();

  wifi_setup();
  
  mqtt_setup();
  
  myFirstAC = new HeatPump;
}
 
void loop() {
  digitalWrite(25, HIGH); //LED heartbeat

  if (WiFi.status() != WL_CONNECTED ) 
      wifi_setup();

  if (!mqtt.connected()) 
    mqtt_connect();

  mqtt.loop();

  myFirstAC->loop();

  digitalWrite(25, LOW);

  while(millis()-lastLoop < interval)
    if(!(millis() % 100)) mqtt.loop();

  lastLoop = millis();
}
I need to sit by the AC unit and make it run through some cycles to observe it and make some tweaks to the sequences.
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Testing things with an mqtt app, I've also got Mycodo mqtt outputs working.
IMG_20211207_223213.jpg
Screenshot_20211207-233031_MQTT_Dash.png
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

I could see a niche for an esp8266-based HVAC controller with an mqtt interface like this one:
https://github.com/nassir-malik/Smart_T ... oogle_Nest

This project uses MQTT but sends IR signals:
https://www.electronics-lab.com/project ... e-control/
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

This is the naked AC unit, without the plastic exterior.
On the upper left side is the inlet, with the evaporator. On the upper right is the outlet. The bottom half is the compressor.
Ac
Ac
I love it when you go "hmm I wonder if this works..." And it works great.
Outlet
Outlet
I think I'll order a custom made ceiling box that better matches the dimensions of the evaporator for the inlet.
Inlet
Inlet
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Power relays arrived. Went with something beefier than the usual DIY relay boards, and these had spade blades so I don't have to cut into the original wires on the AC.
IMG_20211223_151310.jpg
Post Reply