# 基于 ESP8266 的热水器管路自动切换系统

# 简介

我家的房子,开发商送了个阳台壁挂式的太阳能热水器,自己又装了天然气热水器。本着节约能源(抠逼)的原则,自然是应该优先使用太阳能热水器的热水。但这就需手动去开/关两个热水器的阀门,大多数时间都因为懒,一直用的是天然气热水器的热水。

如果能自己切换就好了。阳光充足,太阳能热水器的水温足够高,就用太阳能热水器里的水,否则使用天热气热水器。

结合最近在玩的 ESP8266,又搜到了有电动球阀能符合我的需求,就有了下面这个方案: 拓扑图

# 硬件设置

# 太阳能热水器侧

太阳能热水器侧

ESP8266 上连接了以下设备:

  • DS18B20 温度传感器,用于检测太阳能热水器内的水温,安装在太阳能自带的传感器的位置;
  • 2 路继电器,用来控制电动球阀的打开与闭合;
  • 0.96 英寸 OLED 显示屏,用于显示温度。

# 天然气热水器侧

天然气热水器侧

ESP8266 上只连接了 2 路继电器,用以控制电动球阀的打开与闭合。

# 代码

# 太阳能热水器侧

太阳能热水器侧的功能比较多。首先是检测温度,然后通过 MQTT 协议向 Domoticz 广播温度信息,如果温度达到设置的阀值,就驱动继电器打开/关闭电动球阀,同时通过 http 协议向天然气热水器侧发送控制信息。

为方便以后更新代码,还添加了 OTA 更新功能。访问http://ESP8266IP/ota?action=open即可开启 OTA 更新。

#include <ESP8266WiFi.h>
#include <OneWire.h>
#include <DS18B20.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <OLED.h>
#include <ArduinoOTA.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>
// wifi
const char* ssid     = "wifi";
const char* password = "wifipassword";
// DS18B20
#define ONE_WIRE_BUS 2
DS18B20 ds(ONE_WIRE_BUS);
// relay
#define OPEN_PIN 14
#define CLOSE_PIN 12
bool ISOPEN = false;
// mqtt
const char MqttServer[] = "10.0.2.11";
const char TOPIC[] = "domoticz/in";
WiFiClient espClient;
PubSubClient client(espClient);
// oled
OLED display(4, 5);
// ota
bool ISOTA = false;
// server
ESP8266WebServer server(80);
// client
HTTPClient http;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  pinMode(OPEN_PIN, OUTPUT);
  pinMode(CLOSE_PIN, OUTPUT);
  digitalWrite(OPEN_PIN, LOW);
  digitalWrite(CLOSE_PIN, LOW);
  setWifi();
  display.begin();
  client.setServer( MqttServer, 1883 );
  ota();
  setHttpServer();
}

void loop() {
  // put your main code here, to run repeatedly:
  server.handleClient();
  if(ISOTA){
    display.print("OTA...", 4, 5);
    ArduinoOTA.handle();
  } else{
   if (!client.connected()) {
      reconnect();
    }
    client.loop();
    changRelay();
    delay(2000);
  }
}


void setWifi(){
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

// mqtt reconnect
void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    if (client.connect("ESP8266Client")) {
      Serial.println("connected");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(1000);
    }
  }
}

void changRelay(){
  Serial.println(ds.getTempC());
  float temp = ds.getTempC();
  publishTemp(temp);
  updateOled(temp);
  if(temp >= 42 && !ISOPEN) {
    http.begin("http://10.0.2.200/update?action=close");
    http.GET();
    http.end();
    digitalWrite(OPEN_PIN, HIGH);
    delay(15000);
    ISOPEN = true;
    digitalWrite(OPEN_PIN, LOW);
  } else if (temp < 40 && ISOPEN) {
    http.begin("http://10.0.2.200/update?action=open");
    http.GET();
    http.end();
    digitalWrite(CLOSE_PIN, HIGH);
    delay(15000);
    ISOPEN = false;
    digitalWrite(CLOSE_PIN, LOW);
  }
}

void publishTemp(float temp){
  String payload = "{";
  payload += "\"idx\": 4,";
  payload += "\"command\":\"udevice\",";
  payload += "\"nvalue\": 0,";
  payload += "\"svalue\":";
  payload += "\"";
  payload += temp;
  payload += "\"";
  payload += "}";
  char attributes[100];
  payload.toCharArray( attributes, 100 );
  client.publish(TOPIC, attributes);
}

void updateOled(float temp){
  char c[10];
  dtostrf(temp,2,2,c);
  display.print(c, 4, 5);
}

void ota(){
  ArduinoOTA.onStart([]() {
    display.clear();
    Serial.println("Start");
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {nb
    char c[10];
    float p = progress / (total / 100);
    dtostrf(p,2,0,c);
    display.print(c, 4, 7);
  });
  ArduinoOTA.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 setHttpServer(){
  server.on("/ota", [](){
    if (server.hasArg("action")){
      if(server.arg("action") == "open") {
        ISOTA = true;
        server.send(200, "text/plain", "start ota update");
      } else if(server.arg("action") == "close"){
        ISOTA = false;
        server.send(200, "text/plain", "stop ota update");
      }
    }
  });
  server.begin();
}

# 天然气热水器侧

天然气热水器侧,只接收太阳能热水器侧的控制信息,驱动继电器打开/关闭电动球阀即可。

其实使用任一设备访问http://ESP8266IP/update?action=open即可打开/关闭球阀了。这是个安全漏洞,不过应该不会有人来恶意控制我的热水器玩吧,在我洗澡的时候给我把热水停了?

#include <ESP8266WebServer.h>
#include <ESP8266WiFi.h>

// wifi
const char* ssid     = "wifi";
const char* password = "wifipassword";
// server
ESP8266WebServer server(80);
// relay
#define OPEN_PIN 14
#define CLOSE_PIN 12
bool ISOPEN = false;


void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  pinMode(OPEN_PIN, OUTPUT);
  pinMode(CLOSE_PIN, OUTPUT);
  digitalWrite(OPEN_PIN, LOW);
  digitalWrite(CLOSE_PIN, LOW);
  setWifi();
  setHttpServer();
}

void loop() {
  // put your main code here, to run repeatedly:
  server.handleClient();
}

void setWifi(){
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void setHttpServer(){
  server.on("/update", [](){
    if (server.hasArg("action")){
      if(server.arg("action") == "open" && !ISOPEN) {
        Serial.print("open");
        digitalWrite(OPEN_PIN, HIGH);
        delay(15000);
        ISOPEN = true;
        digitalWrite(OPEN_PIN, LOW);
      } else if(server.arg("action") == "close" && ISOPEN){
        Serial.print("close");
        digitalWrite(CLOSE_PIN, HIGH);
        delay(15000);
        ISOPEN = false;
        digitalWrite(CLOSE_PIN, LOW);
      }
    }
    server.send(200, "text/plain", "this works as well");
  });
  server.begin();
}

# 代码仓库

代码托管于 https://gitee.com/stillyu/esp8266_heater_switch

# 总结

# 投入成本

序号 名称 单价 数量 总价
1 ESP8266 模块 14.60 2 29.20
2 2 路继电器 5.88 2 11.76
3 OLED 显示屏 10.80 1 10.80
4 5V 降压模块 1.35 2 2.70
5 DS18B20 温度传感器 4.70 1 4.70
6 电动球阀 63.00 2 126.00
7 4 分水管活接头 5.60 2 11.20
8 4 分水管对丝 2.80 2 5.60
9 12V 电源适配器 10.00 2 20.00
10 接线端子 1.75 4 7.00
11 亚克力外壳 20 1 20.00
合计 248.96

# 收获

  • 焊工(电烙铁)      EXP+300
  • 管钳工              Level UP
  • ESP8266             Level UP++
  • Arduino             Level UP++

为什么管钳工也是 Level UP 呢?因为第一个电动球阀我安装了三遍,学会了生料带怎么缠,生料带缠不好是真的会漏水的。

# 使用体验

放一张手机端 HomeKit 的图 HomeKit

可以看到热水器的水温,实际也可以控制两个球阀的开关,但没什么意义,自动控制就好了。

有同学注意到猫窝温度吗?这还是个半成品,因为人体猫体传感器的原因,只能检测到活动的人体猫体,不能检测到睡觉的人体猫体,准备用电子秤模块解决。

# 参考资料

最后更新时间: 11/3/2019, 4:45:22 PM