Controlling a portable AC
I just drove myself crazy for an hour trying to figure out why I couldn't get temperature values from the probe with a MAX31855 over the SPI bus. Turns out this AC unit doesn't use Type K thermocouples, they're thermistors. I need a voltage divider and the analog input pin instead.
Previously pasted source code outdated, since then have added OTA support, flood flapper detection, more diagnostic mqtt commands, etc. Both fans and the compressor are functional, coil temperature readings are coming in. When the compressor runs, one coil warms and the other cools. I'm psyched it's functional. It's not very stealthy and clearly oversized for my needs, at least now I'm not too worried about monitoring the compressor's duty cycle. And the duct tape has already started coming off so I'll do the obvious solution and add more duct tape.
Another similar project: https://allelectricproject.com/arduino- ... ontroller/
So it's now wired up and plumbed into my grow. It's not really what I was expecting. In order to dehumidify, the evaporator coil needs to be so cold that it's affecting the grow air temperature and the heater has to run more to compensate.
Obviously the AC unit I'm using is an AC first and dehumidifier second. It makes great cold air, but it's a mediocre dehumidifier due to the heat from the condenser not being recirculated back into the grow.
Naturally, I ordered a proper dehumidifier (GE ADEW20LY) and will move my circuitry into that unit instead.
Obviously the AC unit I'm using is an AC first and dehumidifier second. It makes great cold air, but it's a mediocre dehumidifier due to the heat from the condenser not being recirculated back into the grow.
Naturally, I ordered a proper dehumidifier (GE ADEW20LY) and will move my circuitry into that unit instead.
Chapter two! Or what should have been chapter 1 in the first place but I was making do with what I had on hand.
New dehumidifier arrived today: Right away, I notice you can only set the desired humidity in 5% increments, not very precise.
Let's strip it naked: So this is less obvious on how to get it plumbed into the air handling ducts. The white body plastic is actually part of the squirrel cage fan enclosure, I will need to keep that fan enclosed to make it work.
A proper dehumidifier with the evaporator and condenser coils sandwiched together: The circuit is different: On the AC unit, the circuit board within the guts had the microcontroller on it, while the display & buttons panel was more of a remote control with no electronics on it. On this dehum, the display board has the microcontroller while the main board is just relays and power. There's a 12V AC power transformer once again, relays are 12VDC. This dehum has two thermistors, one is attached to the evaporator coil like on the AC unit, but another is attached to the outlet of the compressor, probably to make sure nothing overheats.
This time around, I'm thinking that I will leave the main board alone, and instead figure out the pinout for the cable connecting the main board to the display board in order to replace the display board with the usual esp32...
New dehumidifier arrived today: Right away, I notice you can only set the desired humidity in 5% increments, not very precise.
Let's strip it naked: So this is less obvious on how to get it plumbed into the air handling ducts. The white body plastic is actually part of the squirrel cage fan enclosure, I will need to keep that fan enclosed to make it work.
A proper dehumidifier with the evaporator and condenser coils sandwiched together: The circuit is different: On the AC unit, the circuit board within the guts had the microcontroller on it, while the display & buttons panel was more of a remote control with no electronics on it. On this dehum, the display board has the microcontroller while the main board is just relays and power. There's a 12V AC power transformer once again, relays are 12VDC. This dehum has two thermistors, one is attached to the evaporator coil like on the AC unit, but another is attached to the outlet of the compressor, probably to make sure nothing overheats.
This time around, I'm thinking that I will leave the main board alone, and instead figure out the pinout for the cable connecting the main board to the display board in order to replace the display board with the usual esp32...
Waiting on the delivery of a 4-channel optoisolator...
Fingers crossed that this code is just right. I still need to figure out what kind of hydrometer came with the dehumidifier, I decided to read its values just for the sake of completion, even though whether the dehum runs or not is still based on the canopy humidity sensor. I also noticed the two thermistors had vastly different resistance despite being near the same temperature so I might need to tweak the readTherm function.
Fingers crossed that this code is just right. I still need to figure out what kind of hydrometer came with the dehumidifier, I decided to read its values just for the sake of completion, even though whether the dehum runs or not is still based on the canopy humidity sensor. I also noticed the two thermistors had vastly different resistance despite being near the same temperature so I might need to tweak the readTherm function.
Code: Select all
// espDehum
// control a dehumidifier
// v20220129
//BUGS mqtt payload have detritus from previous payloads?
//this device
#define espName "espDehum"
int interval = 1000; //loop interval ms
unsigned long lastLoop=0;
#define LEDGPIO 25
#include <ArduinoOTA.h>
//#define DEBUG
//#define DIAG
//For display on 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,55);
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];
unsigned char topic[32];
unsigned char payload[64];
//HeatPump stuff
class Dehumidifier {
private:
//thermistor consts
#define A (0.001129148)
#define B (0.000234125)
#define C (0.0000000876741)
#define R2 (103.1*1000)
#define VCC (3.29)
// GPIOs esp32 34-39 input only
//OUTPUTS
#define slowFanGPIO 13
#define mediumFanGPIO 12
#define fastFanGPIO 14
#define compressorGPIO 27
//INPUTS:
#define hydroGPIO 39 //?
#define floaterGPIO 38
#define dischargeGPIO 37
#define tubeGPIO 36
// Internal
double tubeTemp = 0;
double tubeTargetTemp = 0;
double dischargeTemp = 0;
double dischargeMax = 100; //100C seems like a good start //todo: subscribe to setDischargeMax
double airTemp = 0;
double airHumi = 0;
double Humi = 0;
double h=0, airDew=0, dewCalibration = -7.5;
bool compressor = false;
char fan = 0, newfan = 0; // 0 = off, 1 = slow, 2 = med, 3 = fast
unsigned long pumpStopTime = 0;
boolean floater = false;
boolean overheat = false;
unsigned long coolDownTime = 1000*1; //store as millis
//Coil Temperature
#define samples 3
unsigned int ADC;
int ADCIndex=0, i;
double Vout, Rth;
typedef enum { // HP_State_t
none = 'n',
idle_State = 'i', //Idle...
fan_State = 'f', //just the fan
dehum_State = 'd', //pump the cool
pumpCool_State = 'p', //cool the pump
floater_State = 'F', //condensate accumulating
overheat_State = 'O' //todo
} HP_State_t;
typedef enum { // HP_Event_t
noneEvent = 'n',
SetFan = 'S',
StopFan = 's',
StartDehum = 'D',
StopDehum = 'd',
Floater = 'f',
Overheat = 'o'
} HP_Event_t;
HP_State_t myState=none, nextState=idle_State;
HP_Event_t myEvent=noneEvent;
inline void readHydro() {
//todo
Humi = millis() % 100; //some random value for now
}
inline void readTherm(int GPIO, double *temp) {
ADC = 0;
for (i = 0; i < samples; i++) {
ADC += analogRead(GPIO);//not paying attention to overflow, this could be smarter
delay(1);
}
ADC /= samples;
#ifdef DEBUG
Serial.print("ADC: ");Serial.println(ADC);
dtostrf(ADC, 4, 1, tempString);
mqtt.publish("espDehum/ADC", tempString);
#endif
Vout = (ADC * VCC) / 4095;
#ifdef DEBUG
Serial.print("VOUT: ");Serial.println(Vout);
#endif
Rth = (VCC * R2 / Vout) - R2;
#ifdef DEBUG
Serial.print("Rth: ");Serial.println(Rth);
#endif
Rth = log(Rth);
*temp = (1 / (A + (B * Rth) + (C * Rth*Rth*Rth))) - 273.15;
}
inline void gettubeTemp() {
readTherm(tubeGPIO, &tubeTemp);
#ifdef DEBUG
Serial.print("tubeTemp: "); Serial.println(tubeTemp);
#endif
dtostrf(tubeTemp, 4, 1, tempString);
mqtt.publish("espDehum/tubeTemp", tempString);
}
inline void getdischargeTemp() {
readTherm(dischargeGPIO, &dischargeTemp);
#ifdef DEBUG
Serial.print("dischargeTemp: "); Serial.println(dischargeTemp);
#endif
dtostrf(dischargeTemp, 4, 1, tempString);
mqtt.publish("espDehum/dischargeTemp", tempString);
overheat = dischargeTemp > dischargeMax? true : false;
if(overheat) {
mqtt.publish("espDehum/alert", "Overheat!");
myEvent=Overheat;
}
}
inline void getFloater() {
//pull up, LOW on floater is up
floater = !digitalRead(floaterGPIO);
if(floater) {
mqtt.publish("espDehum/alert", "Floater!");
myEvent=Floater;
}
}
inline void setAirHumi(double n) {
airHumi = n;
//Serial.println("setAirHumi");
calcDew();
}
inline void setAirTemp(double n) {
airTemp = n;
//Serial.println("setAirTemp");
calcDew();
}
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("espDehum/airDew", tempString);
//Serial.print("pub:");Serial.println(tempString);
}
inline void calcTargetTemp() {
//set target coil temp to dewpoint+calibration
tubeTargetTemp = airDew + dewCalibration;
//Serial.println(coilTargetTemp);
}
inline void stopFan() {
digitalWrite(slowFanGPIO, LOW);
digitalWrite(mediumFanGPIO, LOW);
digitalWrite(fastFanGPIO, LOW);
}
inline void setFan(int newfan) {
if(fan) stopFan();
fan = newfan;
switch(fan) {
case 0:
break;
case 1:
digitalWrite(slowFanGPIO, HIGH);
break;
case 2:
digitalWrite(mediumFanGPIO, HIGH);
break;
case 3:
digitalWrite(fastFanGPIO, HIGH);
break;
}
}
inline void startPump() {
if(millis() - pumpStopTime > 3000) {
digitalWrite(compressorGPIO, HIGH);
compressor = true;
}
else {//dont start pump if it's only been X seconds since last turned off
#ifdef DEBUG
Serial.println("Delaying start");
#endif
}
}
inline void stopPump() {
digitalWrite(compressorGPIO, LOW);
compressor = false;
pumpStopTime = millis();
}
inline void startDehum() {
nextState = dehum_State;
}
inline void screenPreLoop() {
Heltec.display->clear();
sprintf(line, "Stt: %c Evnt: %c ntrvl: %i", myState, myEvent, interval);
Heltec.display->drawString(0, 0, line);
sprintf(line, "T:%2.f H:%2.f D:%2.f T:%2.f D:%2.f", airTemp, airHumi, airDew, tubeTemp, dischargeTemp);
Heltec.display->drawString(0, 10, line);
Heltec.display->drawString(0, 20, String("Hydro: ") + Humi);
Heltec.display->drawString(0, 30, String("tubeTarg: ") + tubeTargetTemp + String(" dewCa:") + dewCalibration);
Heltec.display->drawString(0, 40, String("DischargeTemp:") + dischargeTemp);
Heltec.display->drawString(0, 50, String("F:") + fan +String(" CO:")+ compressor + String(" FL:") + floater); //String("SF:") + slowFanGPIO + String(" MF:")+mediumFanGPIO + String(" FF:")+fastFanGPIO
}
inline void screenPostLoop() {
Heltec.display->display();
}
public:
void callback(char* topic, byte* payload, unsigned int length) {
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 if (0 == strcmp(topic, "espDehum/setFan")) {
newfan = payload[0]; //no sanity checking
myEvent = SetFan;
}
else if (0 == strcmp(topic, "espDehum/dehum")) {
if(payload[0] == '1')
myEvent = StartDehum;
else if (payload[0]=='0')
myEvent= StopDehum;
else
Serial.println("unhandled payload");}
else if (0 == strcmp(topic, "espDehum/setCoolDown")) {
coolDownTime = 1000*atoi((char *)payload); //no sanity checking here, payload in seconds, stored as millis
#ifdef DEBUG
Serial.print("coolDownTime: "); Serial.println(coolDownTime);
#endif
}
else if (0 == strcmp(topic, "espDehum/setDewCalibration")) {
dewCalibration = atoi((char *)payload); //no sanity checking here, payload in Temp C
#ifdef DEBUG
Serial.print("dewCalibration: "); Serial.println(dewCalibration);
#endif
}
#ifdef DIAG
else if (0 == strcmp(topic, "espDehum/sfan")) { //diagnostic
if(payload[0] == '1')
setFan(1);
else if (payload[0]=='0')
setFan(0);
else
Serial.println("unhandled payload");}
else if (0 == strcmp(topic, "espDehum/mfan")) { //diagnostic
if(payload[0] == '1')
setFan(2);
else if (payload[0]=='0')
setFan(0);
else
Serial.println("unhandled payload");}
else if (0 == strcmp(topic, "espDehum/ffan")) { //diagnostic
if(payload[0] == '1')
setFan(3);
else if (payload[0]=='0')
setFan(0);
else
Serial.println("unhandled payload");}
else if (0 == strcmp(topic, "espDehum/comp")) { //diagnostic
if(payload[0] == '1')
startPump();
else if (payload[0]=='0')
stopPump();
else
Serial.println("unhandled payload");}
else if (0 == strcmp(topic, "espDehum/reboot")) {
stopPump();
setFan(0);
esp_restart();
}
#endif
else {
Serial.print("Unhandled callback topic: ");
Serial.println(topic);
}
memset(payload, 0 , length); //wtf buggy mqtt library not clearing previous payload?
}
Dehumidifier() {
pinMode(slowFanGPIO, OUTPUT);
pinMode(mediumFanGPIO, OUTPUT);
pinMode(fastFanGPIO, OUTPUT);
pinMode(compressorGPIO, OUTPUT);
pinMode(tubeGPIO, INPUT);
pinMode(dischargeGPIO, INPUT);
pinMode(floaterGPIO, INPUT);
digitalWrite(slowFanGPIO, LOW);
digitalWrite(mediumFanGPIO, LOW);
digitalWrite(fastFanGPIO, LOW);
digitalWrite(compressorGPIO, LOW);
//todo NVRAM to resume state after unexpected poweroff?
//Serial.println("setup()'d");
}
void loop() {
readHydro();
gettubeTemp();
getdischargeTemp();
getFloater();
myState = nextState;
screenPreLoop();
//Serial.print("loop: myState: "); Serial.println((char)myState);
switch(myState) {
case none:
break;
case idle_State:
switch(myEvent) {
case Floater:
case Overheat:
//what do we do? stall for now
break;
case SetFan:
setFan(newfan);
nextState = fan_State;
Serial.println("Idle State, Start Fan");
break;
case StartDehum:
Serial.println("Idle State, Start Dehum");
startDehum();
break;
case StopDehum:
//ignore
break;
case noneEvent:
//Serial.println("Idle, no event");
break;
default:
Serial.print("Unhandled event: "); Serial.print((char)myEvent); Serial.print(" in state: "); Serial.println((char)myState);
break;
}
break;
case fan_State:
switch(myEvent) {
case Floater:
case Overheat:
//what do we do?
break;
case noneEvent:
//Serial.println("Fan State, no event");
break;
case StopFan:
setFan(0);
nextState = idle_State;
Serial.println("Fan State, StopFan event");
break;
case StartDehum:
startDehum();
break;
case StopDehum:
//do nothing
break;
default:
Serial.print("Unhandled event: "); Serial.print((char)myEvent); Serial.print(" in state: "); Serial.println((char)myState);
break;
//fan goes brrrr
}
break;
case dehum_State:
switch(myEvent) {
case noneEvent:
//Serial.println("fan mode, no event");
break;
case Floater: //fall thru
case Overheat: //todo: proper handling would just mean stopping the pump until no longer over max, then resuming when under max, for now bomb out as I don't expect to hit this limit
case StopDehum:
Serial.println("dehum_State: going to pumpCool_State");
stopPump();
//coolStartTime = millis();
//Serial.print("Dehum State, event, cooltime: "); Serial.println(cooltime);
nextState = pumpCool_State;
goto stopper; //early exit
case StartDehum:
startDehum();
break;
default:
Serial.print("Unhandled event: "); Serial.print((char)myEvent); Serial.print(" in state: "); Serial.println((char)myState);
break;
}
//no new events to process, pump goes brrrrr?
setFan(1);
calcTargetTemp();
if(tubeTargetTemp < tubeTemp)
startPump();
else stopPump();
break;
case pumpCool_State:
switch(myEvent) {
case noneEvent:
//Serial.println("pump cool state, no event");
break;
case SetFan:
setFan(newfan);
nextState = fan_State;
Serial.println("Pump Cool State, Start Fan");
break;
case StartDehum:
startDehum();
break;
default:
Serial.print("Unhandled event4: "); Serial.print((char)myEvent); Serial.print(" in state: "); Serial.println((char)myState);
break;
}
if (millis() - pumpStopTime > coolDownTime ) {
setFan(0);
Serial.println("pumpCool State: going idle");
nextState = idle_State;
}
else {}
break;
default:
Serial.println("unhandled state");
break;
}
stopper:
myEvent = noneEvent;
screenPostLoop();
}
} *myFirstDehum; //class Dehumidifier
//////////////////////////////////////// SETUP CODE
void led_setup() {
Heltec.begin(true /*DisplayEnable Enable*/, false /*LoRa Enable*/, true /*Serial Enable*/);
Heltec.display->clear();
Heltec.display->drawString(0,0, espName);
Heltec.display->display();
}
void wifi_setup() {
Heltec.display->drawString(0,10,"wifi..");
Heltec.display->display();
char i =0;
WiFi.persistent(false);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
WiFi.config(staticIP, gateway, gateway, subnet);
while ((WiFi.status() != WL_CONNECTED) & (i < 35)) {
i++;
delay(100);
}
sprintf(line, "i: %i", i);
Heltec.display->drawString(40, 10, line);
if(WiFi.status() != WL_CONNECTED)
Heltec.display->drawString(16, 10, "...Ain't no wifi!");
else
Heltec.display->drawString(16,10,"...done");
Heltec.display->display();
}
void ota_setup() {
ArduinoOTA
.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH)
type = "sketch";
else // U_SPIFFS
type = "filesystem";
Serial.println("Start updating " + type);
})
.onEnd([]() {
Serial.println("\nEnd");
})
.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
})
.onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
else if (error == OTA_END_ERROR) Serial.println("End Failed");
});
ArduinoOTA.begin();
}
void mqtt_setup() {
Heltec.display->drawString(60,10,"mqtt..");
Heltec.display->display();
mqtt.setServer(mqtt_server, 1883);
mqtt.setCallback(callback);
mqtt_connect();
mqtt.subscribe("canopy/temperature");
mqtt.subscribe("canopy/humidity");
mqtt.subscribe("espDehum/setInterval");
mqtt.subscribe("espDehum/setCoolDown");
mqtt.subscribe("espDehum/setCoilTemp");
mqtt.subscribe("espDehum/setDewCalibration");
mqtt.subscribe("espDehum/setFan");
mqtt.subscribe("espDehum/dehum");
//diagnostic commands
#ifdef DIAG
mqtt.subscribe("espDehum/sfan");
mqtt.subscribe("espDehum/mfan");
mqtt.subscribe("espDehum/ffan");
mqtt.subscribe("espDehum/comp");
mqtt.subscribe("espDehum/reboot");
#endif
mqtt.publish("espDehum/alert", "Hello world ");
Heltec.display->drawString(90,10,"..done");
Heltec.display->display();
}
void mqtt_connect() {
while (!mqtt.connected())
if (!mqtt.connect(espName)) delay(800);
}
void callback(char* topic, byte* payload, unsigned int length) {
//I want to use strcat(espName, "/fan") but something's fucky
#ifdef DEBUG
Serial.print("callback topic: ");
Serial.print(topic);
Serial.print(" payload: ");
Serial.println((char *) payload);
//Serial.print(sizeof( (char *)payload));
#endif
if (0 == strcmp(topic, "espDehum/setInterval")) {
interval = atoi((char *)payload)/10; //no sanity checking here
//Serial.print("new interval: ");Serial.println(interval);
}
else
myFirstDehum->callback(topic, payload, length);
}
void setup() {
pinMode(LEDGPIO, OUTPUT);
Serial.begin(115200);
Serial.println("\nDehumdifier\n\n");
led_setup();
wifi_setup();
ota_setup();
mqtt_setup();
myFirstDehum = new Dehumidifier;
lastLoop = millis();
}
void loop() {
digitalWrite(LEDGPIO, HIGH); //LED heartbeat
if (WiFi.status() != WL_CONNECTED )
wifi_setup();
ArduinoOTA.handle();
if (!mqtt.connected())
mqtt_connect();
mqtt.loop();
myFirstDehum->loop();
digitalWrite(LEDGPIO, LOW);
while(millis()-lastLoop < interval)
if(!(millis() % 250)) mqtt.loop();
lastLoop = millis();
}