RV/Trailer/Modbus Thermostat with multiple heat sources

Oh great, another thermostat…
But what is a thermostat? Once upon a time, it was simply a device to open or close a circuit to activate a heating device. Then we started adding more things to out HVAC systems and thus more wires 2 became 4 became 5 became 8. Now were back to 2 or 3 wires with these new communicating systems that don’t simply turn on or off but might be trying to run at a variable state to reduce energy consumption and increase comfort.

My specific living situation has me relying on a propane furnace central air system running MODBUS as the thermostat communication protocol. Because I’d rather not use all of my propane as fast as my furnace alone can I also have regular wall plug space heaters.

How can I control 2 completely separate heating devices using one temperature sensor(I live in a very small space and have no need for multiple sensors or zone control).
Easy I thought, some wifi wall plugs(rated for my space heaters amperage) and some way to control the Modbus signal. Luckily my Modbus system(Dometic) is only communicating from the thermostat to the system so I don’t have to worry about errors coming back to the thermostat or anything like that.

A simple D1 Mini board is able to control the modbus signal that has been recorded from my thermostat, and relay the temperature sensor data to home assistant, home assistant is able to take the temperature sensor data and act as the actual thermostat controlling the space heater plugs via the normal switch’s as well as control the modbus signal as if it where switches also because that’s how the d1 mini is setup.

Originally I wasn’t using Home Assistant because anything I owned that integrated I just had on my iPhone, Ive been aware of Home Assistant and following its progression since 2021 I believe. Because I wasn’t on home assistant I originally tried to just make the switches work with the Arduino(d1 mini(I was testing with allot of microcontrollers)) and have the Arduino be the thermostat. I soon realized that on such a small scale if I had to deal with MQTT or anything like that I might as well use home assistant and have access to my thermostat over the internet.

ESPHOME, YAML, and LAMBDA
These are some amazing tools immediately squashed by an inability to delayMicroseconds
that’s all ok unless your modbus control signal looks like this

  digitalWrite(AC_signal_genPIN, LOW); // sets the digital pin AC_signal_genPIN on
  delayMicroseconds(500); 
  digitalWrite(AC_signal_genPIN, HIGH);  // sets the digital pin AC_signal_genPIN off
  delayMicroseconds(1000);            

repeating with many delayMicroseconds.

Solution
BACK TO MQTT

so well go back to the Arduino.ino setup the d1 mini as an mqtt wifi device, assign our digitalWrite outputs for all the different central hvac system operations as switches(also assigning them as switches in home assistant). Relay our temp/humidity sensor data to the home assistant(it also has a screen but who cares when my phone can tell me).

Ok but now I only have a current temperature and a bunch of switches on my home assistant. Nothing controlling/automating anything.

Solution!
generic_thermostat !?!?!
No. Why? Only offers control over one heating OR cooling device, not both and not in a heating primary/auxiliary configuration like most modern heat pumps or anyone with two controlled heat sources.

Next Solution?
HACS

Home Assistant Dual Smart Thermostat component

This combines all of the HVAC operations into one card with one setpoint even allowing for way more control over zones and active statuses(not my concern). But what about Auxilary heat? This is once again where the water got dirty. Although “Home Assistant Dual Smart Thermostat component” does offer aux heating ability it is very clear that.
“If the timeout ends and the heater was on for the whole time the thermostat switches to the secondary heater. In this case the primary heater (heater) will be turned off. This will be remembered for the day it turned on and in the next heating cycle the secondary heater will turn on automatically. On the next day the primary heater will turn on again the second stage will again only turn on after a timeout. If the third secondary heater_dual_mode is set to true the secondary heater will be turned on together with the primary heater.”
this is not what I need or want. Luckily we have access to

/homeassistant/custom_components/dual_smart_thermostat/hvac_device/heater_aux_heater_device.py

Originally

@property
def _has_aux_heating_ran_today(self) -> bool:
    """Determines if the aux heater has been used today."""
    if self._aux_heater_last_run is None:
        return False

    if self._aux_heater_last_run.date() == datetime.datetime.now().date():
        return True

    return False

is now

@property
def _has_aux_heating_ran_today(self) -> bool:
    """Determines if the aux heater has been used today."""
    if self._aux_heater_last_run is None:
        return False

    if self._aux_heater_last_run.date() == datetime.datetime.now().date():
        return False

    return False

Explanation: I don’t care if the aux ran today and don’t want to switch it to the primary for the day.

the other portion that I adjusted was to account for some inconsistency with the aux and primary running at the same time

if not self._aux_heater_dual_mode:
    await self.heater_device.async_turn_off()
await self.aux_heater_device.async_turn_on()

becomes

if not self._aux_heater_dual_mode:
    await self.heater_device.async_turn_on()
await self.aux_heater_device.async_turn_on()

I know these are cheesy nasty work around to anyone with a degree in this stuff but for me its ok.

And just like that I can add my intigrations to my configuration.yaml and setup the d1 minis c code.

configuration.yaml(this is currently only setup for heating and cooling code will need to be added)

switch:
  - platform: template
    switches:
      spaceheaters:   #Setting up cloud based plugs as switches in home assistant
        friendly_name: "Space Heaters"
        value_template: >
          {{ is_state('light.plugin1', 'on') or is_state('light.plugin2', 'on') }}
        turn_on:
          - service: light.turn_on
            target:
              entity_id:
                - light.plugin1
                - light.plugin2
        turn_off:
          - service: light.turn_off
            target:
              entity_id:
                - light.plugin1
                - light.plugin2
      # Template Switch
  - platform: template
    switches:
      gasheater:   #Setting up the mqtt wemos switches as HA switches(HA wont do this for me automatically or I missed something)
        friendly_name: "Gas Heater"
        value_template: "{{ is_state('switch.heating', 'on') }}"
        turn_on:
          service: switch.turn_on
          target:
            entity_id: switch.heating
        turn_off:
          service: switch.turn_off
          target:
            entity_id: switch.heating

# Existing configuration...
mqtt:
    sensor:
      - name: "Wemos D1 Temperature"
        state_topic: "home/wemosd1r1/temperature"
        unit_of_measurement: "°F"
        device_class: temperature
      - name: "Wemos D1 Humidity"
        state_topic: "home/wemosd1r1/humidity"
        unit_of_measurement: "%"
        device_class: humidity
    
      # MQTT Switches
    switch:
      - name: "Heating"
        state_topic: "home/wemosd1r1/heating"
        command_topic: "home/wemosd1r1/heating"
        payload_on: "ON"
        payload_off: "OFF"
    
      - name: "AC Low"
        state_topic: "home/wemosd1r1/ac_low"
        command_topic: "home/wemosd1r1/ac_low"
        payload_on: "ON"
        payload_off: "OFF"
    
      - name: "AC High"
        state_topic: "home/wemosd1r1/ac_high"
        command_topic: "home/wemosd1r1/ac_high"
        payload_on: "ON"
        payload_off: "OFF"
    
      - name: "Fan"
        state_topic: "home/wemosd1r1/fan"
        command_topic: "home/wemosd1r1/fan"
        payload_on: "ON"
        payload_off: "OFF"
    

# ... existing configuration ...

climate:
  - platform: dual_smart_thermostat
    name: "Home Thermostat"
    unique_id: home_thermostat

    # Primary Heater (Space Heaters)
    heater: switch.spaceheaters

    # Secondary Heater (Wemos Heating Switch)
    secondary_heater: switch.gasheater
    secondary_heater_timeout: '00:15:00'  # Time after which secondary heating activates
    secondary_heater_dual_mode: false     # Secondary heater replaces primary when activated

    # Cooler (Wemos Cooling Switch)
    cooler: switch.ac_high

    # Temperature Sensor (from Wemos D1 Mini)
    target_sensor: sensor.wemos_d1_temperature

    # Temperature Settings
    min_temp: 50
    max_temp: 90
#    target_temp: 72
    cold_tolerance: 0.5
    hot_tolerance: 0.5
    min_cycle_duration:
      seconds: 60
#    initial_hvac_mode: "off"
    away_temp: 62

    # Presets
    away:
      temperature: 62
    home:
      temperature: 72
    sleep:
      temperature: 68

    # Fan Control (if applicable)
    fan: switch.wemos_fan  # Define this switch if you have a fan

    # Openings (Window/Door Sensors)
#    openings:
#      - binary_sensor.window1
#      - binary_sensor.door1
#      - entity_id: binary_sensor.window2
#        timeout: '00:05:00'  # Delay before considering the opening as open

    # Floor Temperature Control (if using floor heating)
#    floor_sensor: sensor.floor_temperature
#    max_floor_temp: 85  # Maximum allowed floor temperature
#    min_floor_temp: 60  # Minimum floor temperature to maintain

    # HVAC Action Reasons (optional)
#    sensor_stale_duration:
#      minutes: 15  # If sensor data is stale, thermostat turns off

My wemos d1 mini esp826 sketch
This code is only setup for a heating comm signal at the moment and will need
void sendACHighSignal
You can find the domestic modbus digitalwrite from

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <Wire.h>
#include <LiquidCrystal_PCF8574.h>
#include <ArduinoOTA.h>

// Wi-Fi credentials
const char* ssid = "ssidSetup";
const char* password = "password";

// MQTT server settings
const char* mqtt_server = "10.0.0.0";  // Your MQTT broker IP
const int mqtt_port = 1883;             // Typically 1883
const char* mqtt_user = "mqtt_user";         // Your MQTT username
const char* mqtt_password = "mqtt_password"; // Your MQTT password

// MQTT topics
const char* temperature_topic = "home/wemosd1r1/temperature";
const char* humidity_topic = "home/wemosd1r1/humidity";
const char* setpoint_topic = "home/wemosd1r1/setpoint";
const char* heating_topic = "home/wemosd1r1/heating";
const char* ac_low_topic = "home/wemosd1r1/ac_low";
const char* ac_high_topic = "home/wemosd1r1/ac_high";
const char* fan_topic = "home/wemosd1r1/fan";

// Hardware pins
#define DHTPIN D4
#define DHTTYPE DHT22
#define AC_SIGNAL_PIN D5  // Pin used for communication signal to AC/heat system

// Initialize DHT sensor
DHT dht(DHTPIN, DHTTYPE);

// Initialize LCD (I2C address 0x27)
LiquidCrystal_PCF8574 lcd(0x27);

// Initialize Wi-Fi and MQTT client
WiFiClient espClient;
PubSubClient client(espClient);

// Variables for storing data
float currentTemperature = 0;
float currentHumidity = 0;
float setpointTemperature = 72.0;  // Default setpoint temperature in Fahrenheit

// Variables for modes
bool heatingOn = false;
bool acLowOn = false;
bool acHighOn = false;
bool fanOn = false;

void setup() {
  Serial.begin(115200);
  delay(10);

  // Initialize I2C bus
  Wire.begin(D2, D1);  // SDA = D2, SCL = D1

  // Initialize DHT sensor
  dht.begin();

  // Initialize LCD
  lcd.begin(16, 2);            // For 16x2 LCD
  lcd.setBacklight(255);       // Set backlight brightness (0-255)
  lcd.clear();                 // Clear any previous data

  // Set up pin modes
  pinMode(AC_SIGNAL_PIN, OUTPUT);
  digitalWrite(AC_SIGNAL_PIN, HIGH);  // Assuming HIGH is off

  // Connect to Wi-Fi
  setup_wifi();

  // Initialize MQTT
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);

  // Initialize OTA
  ArduinoOTA.onStart([]() {
    Serial.println("Start updating firmware...");
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd updating firmware");
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
  });
  ArduinoOTA.begin();
}

void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.mode(WIFI_STA);  // Station mode
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("Wi-Fi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
  String messageTemp;
  for (unsigned int i = 0; i < length; i++) {
    messageTemp += (char)payload[i];
  }

  Serial.print("Message arrived on topic: ");
  Serial.print(topic);
  Serial.print(". Message: ");
  Serial.println(messageTemp);

  if (String(topic) == setpoint_topic) {
    setpointTemperature = messageTemp.toFloat();
    Serial.print("Setpoint temperature updated to: ");
    Serial.println(setpointTemperature);
  } else if (String(topic) == heating_topic) {
    heatingOn = (messageTemp == "ON");
    Serial.println(heatingOn ? "Heating turned ON" : "Heating turned OFF");
  } else if (String(topic) == ac_low_topic) {
    acLowOn = (messageTemp == "ON");
    Serial.println(acLowOn ? "AC Low turned ON" : "AC Low turned OFF");
  } else if (String(topic) == ac_high_topic) {
    acHighOn = (messageTemp == "ON");
    Serial.println(acHighOn ? "AC High turned ON" : "AC High turned OFF");
  } else if (String(topic) == fan_topic) {
    fanOn = (messageTemp == "ON");
    Serial.println(fanOn ? "Fan turned ON" : "Fan turned OFF");
  }
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect with MQTT authentication
    if (client.connect("WemosD1R1Client", mqtt_user, mqtt_password)) {
      Serial.println("connected");
      // Subscribe to topics
      client.subscribe(setpoint_topic);
      client.subscribe(heating_topic);
      client.subscribe(ac_low_topic);
      client.subscribe(ac_high_topic);
      client.subscribe(fan_topic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(". Trying again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void loop() {
  ArduinoOTA.handle();

  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  unsigned long currentMillis = millis();

  static unsigned long previousSensorMillis = 0;
  const long sensorInterval = 5000;  // Interval to read sensors and update LCD

  if (currentMillis - previousSensorMillis >= sensorInterval) {
    previousSensorMillis = currentMillis;

    // Read DHT sensor in Fahrenheit
    float newTemperature = dht.readTemperature(true);  // Pass 'true' for Fahrenheit
    float newHumidity = dht.readHumidity();

    if (!isnan(newTemperature)) {
      currentTemperature = newTemperature;
      // Publish temperature
      client.publish(temperature_topic, String(currentTemperature).c_str(), true);
    }

    if (!isnan(newHumidity)) {
      currentHumidity = newHumidity;
      // Publish humidity
      client.publish(humidity_topic, String(currentHumidity).c_str(), true);
    }

    // Update LCD
    lcd.setCursor(0, 0);
    lcd.print("Temp: ");
    lcd.print(currentTemperature, 1);
    lcd.print("F   ");  // Update unit to Fahrenheit

    lcd.setCursor(0, 1);
    lcd.print("Setpoint: ");
    lcd.print(setpointTemperature, 1);
    lcd.print("F   ");  // Update unit to Fahrenheit
  }

  // Control AC signal
  controlACSignal();
}

void controlACSignal() {
  if (heatingOn) {
    sendHeatingSignal();
  } else if (acLowOn) {
    sendACLowSignal();
  } else if (acHighOn) {
    sendACHighSignal();
  } else if (fanOn) {
    sendFanSignal();
  } else {
    // No mode is active, ensure pin is off
    digitalWrite(AC_SIGNAL_PIN, LOW);  // Assuming HIGH is off
  }
}

void sendHeatingSignal() {
  // Implement the heating signal as per your protocol
  Serial.println("the heater loop is running");
  digitalWrite(AC_SIGNAL_PIN, LOW);// sets the digital pin 13 on
  delayMicroseconds(1000);// waits for a second
  digitalWrite(AC_SIGNAL_PIN, HIGH);// sets the digital pin 13 AC_SIGNAL_PIN
  delayMicroseconds(500);// waits for half a second
}

void sendFanSignal() {
  // Implement the Fan signal as per your protocol
}

Please give me feedback. Also sorry about my readability.

Very impressive! My one question to you is, you said the thermostat could not control the space heater? It can. Just have an automation with the thermostat heat going on as the trigger to then turn on the space heater (and when it goes off, to turn opff the space heater), no?

1 Like

You’re right that you can technically use an automation triggered by the main thermostat’s heating state to turn the space heaters on and off. If the primary heater turns on, an automation could immediately switch the space heaters on, and when it goes off, switch them off again. This would allow the thermostat entity to indirectly control the space heaters.

However, my goal was to have both heat sources managed by a single climate entity that understands them as a primary and auxiliary heat source rather than relying on a separate automation. Using the Dual Smart Thermostat component (with some tweaks) gives me a single control interface and logic chain: one setpoint, integrated logic for aux heating, and a cleaner overall setup. It also ensures that the control logic is centralized and does not rely on separate automations that could get out of sync.

In addition, the Dual Smart Thermostat component can also handle cooling and fan operations within the same entity. This means I can integrate my air conditioning and fan controls directly into the same interface, maintaining a unified experience for heating, cooling, and air circulation. By incorporating all of these functions into one flexible climate platform, I end up with a truly “all-in-one” solution that simplifies management and enhances convenience.

2 Likes

Wow man, just in time for me to start my HACS project for my camper! I think this is exaclty what i was looking for :slight_smile:

1 Like

all thanks to the github i linked if you have a dometic, its missing the heat pump(if you have the nice rv) and its missing a fan speed i think.

Sloopdog/Ardunio-Code

If we have to tho we can get a tool and decode the signals ourselves.
USB-to-RS485 adapter (e.g., FTDI or ARDD-1066) for Modbus RTU.

Let me know if there’s any other way I can help or better explain myself.

Also RVs are hard with distnace to wifi/2 routers/mini servers so if you want a further setup explanation I can provide that too.

Hi All! I’m needing some help in a similar situatiion. I’m using HA in an entertainer tour coach with 5 Dometic roof AC units. I have successfully used Sloopdog’s code to load to an ESPHome ESP8266 and it’s running. I can’t seem to get it to talk to the dometic though. It’s a 3.3v logic out vs. the CCC2’s 5v out, but it seems that others had had success with it despite that issue. Any thoughts? Thanks in advance!