Demo 51: Building a Smart Home system with Home Assistant using Raspberry Pi and ESP32/ESP8266 (A to Y)

1. Introduction
  Today I will show you how to build a Smart Home with Home Assistant (HA) using Raspberry Pi and ESP32/ESP8266.
 Home Assistant will act as a server with GUI and ESP32/ESP8266 as clients. User will access Home Assistant by web browser with url: http://homeassistant.local:8123 or http://IP_address:8123
This tutorial has the steps:
- Install Home Assistant on Raspberry Pi
- Setup Ethernet, Wifi
- Install necessary software Mosquitto MQTT broker, InfluxDB, File Editor, ...
- Read data from sensor on ESP (temperature, humidity, ...)
- Send data to actuator on ESP (control bulb, ...)
- Use InfluxDB to store data
- Auto-detection ESP device from Home Assistant
- Automation script (control the ESP by context/scenario) from Home Assistant 
- Grafana platform for beautiful analytics and monitoring
- Monitor ESP camera
2. Actions
2.1 Install Home Assistant on Raspberry Pi
- Go to: https://www.home-assistant.io/getting-started/ and download the image according to your Raspberry Pi
- Write the downloaded image to SD card by balenaEtcher or Startup Disk Creator on Ubuntu.

Figure: Startup Disk Creator

- Waiting until the image is written to SD card.

- Plug the SD card to Raspberry Pi.

- Power up the Raspberry Pi and connect it to your LAN network by network cable.

2.2 Setup Ethernet, Wifi

If you only have Wifi and have no Wired connection hole, you can configure your laptop as a bridge (like hotspot) like below:

Pi -> network cable -> laptop -> Wifi

Figure: laptop as a bridge
Figure: ifconfig to see the IP of wired connection is up
- Open Terminal and type command below to scan the IP address of Raspberry Pi Home Assistant:
nmap -sn 10.42.0.1/24
 
- Open the Web Browser and type command: http://10.42.0.137:8123/

- For the first time, Home Assistant takes quite long time to startup (about ~15-20 minutes).  

- Create account

- Choose Next and Finish

Setup Wifi

From left Menu choose Supervisor > IP Address Change > WLAN0 > IPv4 > DHCP > Wi-Fi > SCAN FOR ACCESSPOINTS > wpa-psk > Password > Save > REBOOT



2.3 Install necessary software

From left Menu choose Supervisor > Add-on Store

then press Install and press Start button:

Mosquitto broker


Important: Enable HA to connect to this add-on
InfluxDB

File editor

This application supports to modify *.yaml (choose Show in sidebar to add the add-on to the upper left Menu)

 

Install Grafana
2.4 Read data from sensor on ESP (temperature, humidity, ...)
We use MQTT to transfer data from ESP to HA. We will configure the HA to listen on the MQTT topic. We will use File Editor add-on above to modify "confiuration.yaml"

choose File Browser (red circle)

choose "confiuration.yaml" and add the below entry:
sensor:
    - platform: mqtt
      state_topic: 'home/room/temperature'
      name: 'Room Temperature'
      unit_of_measurement: '°C'
The HA will subscribe the topic: home/room/temperature
From File Editor choose Save button:

Restart HA service
Flash the Arduino code (refer Demo 14)
This code also refers this demo to get the IP address of HA via hostname. You can set the the hostname of HA via Supervisor > System > Host > Change (default hostname is: homeassistant)

#include <WiFi.h>
#include <PubSubClient.h>
#include <ESPmDNS.h>


/* change it with your ssid-password */
const char* ssid = "I3.41";
const char* password = "xxx";
/* this is the IP of PC/raspberry where you installed MQTT Server */
const char* mqtt_server = "homeassistant";
#define mqtt_user       "iotsharing"
#define mqtt_password   "xyz"
#define mqtt_clientId   "iotsharing-sensor"

float temperature = 0;

/* create an instance of PubSubClient client */
WiFiClient espClient;
PubSubClient client(espClient);

/* topics */
#define TEMP_TOPIC    "home/room/temperature"

long lastMsg = 0;
char msg[20];

void reconnect() {
  //Loop until we're reconnected
  while (!client.connected()) {
    if (client.connect(mqtt_clientId, mqtt_user, mqtt_password)) {
      Serial.println("connected");
      delay(1000);
      Serial.print("MQTT connected!");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
    }
  }
}

void setup() {
  Serial.begin(115200);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  if (!MDNS.begin("esp32")) {
      Serial.println("Error setting up MDNS responder!");
      while(1) {
          delay(1000);
      }
  }

  IPAddress serverIp = MDNS.queryHost(mqtt_server);
  Serial.print("IP address of server: ");
  Serial.println(serverIp.toString());
    
  /* configure the MQTT server with IPaddress and port */
  client.setServer(serverIp, 1883);
}
void loop() {
  /* if client was disconnected then try to reconnect again */
  if (!client.connected()) {
    reconnect();
  }
  /* this function will listen for incomming 
  subscribed topic-process-invoke receivedCallback */
  client.loop();
  /* we measure temperature every 3 secs
  we count until 3 secs reached to avoid blocking program if using delay()*/
  long now = millis();
  if (now - lastMsg > 3000) {
    lastMsg = now;
    /* read DHT11/DHT22 sensor and convert to string */
    temperature = random(20,40)/1.0;
    if (!isnan(temperature)) {
      snprintf (msg, 20, "%lf", temperature);
      Serial.println(msg);
      /* publish the message */
      client.publish(TEMP_TOPIC, msg);
    }
  }
}
From the HA choose Edit Dashboard

Choose ADD CARD > By Entity > tick "sensor.room_temperator" > press Continue > Add to LOVELACE UI

Or add a Gauge by choosing ADD CARD > GAUGE > choose Entity "sensor.room_temperator" > press Save
2.5 Send data to actuator on ESP (control bulb, ...)
Use File editor add-on to open "confiuration.yaml" and add the below entry:
switch:
    - platform: mqtt
      name: "Light"
      state_topic: "home/room/switch/set"
      command_topic: "home/room/switch/set"
      payload_on: "on"
      payload_off: "off"
      qos: 0
The HA will send the command topic: "home/room/switch/set" to ESP. After ESP received the command it must send the state topic back to the HA as ACK (acknowledgement) so that the HA can update its state. If you set the state topic same as command topic, you need not to resend ACK. The payload_on and payload_off are valid values of the topics. 
Restart HA service
Flash the Arduino code (refer Demo 14)
#include <WiFi.h>
#include <PubSubClient.h>
#include <ESPmDNS.h>


/* change it with your ssid-password */
const char* ssid = "I3.41";
const char* password = "xxx";
/* this is the IP of PC/raspberry where you installed MQTT Server */
const char* mqtt_server = "homeassistant";
#define mqtt_user       "iotsharing"
#define mqtt_password   "xyz"
#define mqtt_clientId   "iotsharing-sensor"

float temperature = 0;

/* create an instance of PubSubClient client */
WiFiClient espClient;
PubSubClient client(espClient);

/* topics */
#define CTRL_BULB_TOPIC    "home/room/switch/set"

long lastMsg = 0;
char msg[20];

void reconnect() {
  //Loop until we're reconnected
  while (!client.connected()) {
    if (client.connect(mqtt_clientId, mqtt_user, mqtt_password)) {
      Serial.println("connected");
      delay(1000);
      Serial.print("MQTT connected!");
      client.subscribe(CTRL_BULB_TOPIC);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
    }
  }
}

void receivedCallback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message received: ");
  Serial.print("payload: ");
  String cmd = "";
  for (int i = 0; i < length; i++) {
    cmd += (char)payload[i];
  }
  Serial.print("HA control bulb: ");
  Serial.println(cmd);
}

void setup() {
  Serial.begin(115200);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  if (!MDNS.begin("esp32")) {
      Serial.println("Error setting up MDNS responder!");
      while(1) {
          delay(1000);
      }
  }
  IPAddress serverIp = MDNS.queryHost(mqtt_server);
  Serial.print("IP address of server: ");
  Serial.println(serverIp.toString());
    
  /* configure the MQTT server with IPaddress and port */
  client.setServer(serverIp, 1883);
  client.setCallback(receivedCallback);
}
void loop() {
  /* if client was disconnected then try to reconnect again */
  if (!client.connected()) {
    reconnect();
  }
  /* this function will listen for incomming 
  subscribed topic-process-invoke receivedCallback */
  client.loop();
}
From Dashboard choose Edit Dashboard > Add Card > Button > Light > Save

From Arduino open the Terminal, then try to click the Light button, and you will see from Terminal:
2.6 Use InfluxDB to store data
Open InfluxDB add-on
Create user: open InfluxDB Admin > press Create User > Enable ALL in Permissions > press green button

Open Explore Tab in InfluxDB
Create a database called "myhome" by using query: 
CREATE DATABASE myhome 
Press Submit Query
Add entry below in "configuration.yaml"
influxdb:
    host: localhost
    port: 8086
    database: myhome
    username: iotsharing
    password: xyz
    default_measurement: state
    include:
        entities:
            - sensor.room_temperator
Here: 
username and password: account that created in previous step
entities: entity of ESP sensor in 2.4 Read data from sensor on ESP
In order to view the eties of myhome database, use the query:
SELECT * FROM "myhome"."autogen"."°C" WHERE "entity_id"='room_temperator'
You will see something like this.
You can use Dashboards in InfluxDB to visualize easily
From InfluxDB Menu choose Dashboards > Line style chart > Add Data

choose myhome.autogen > entity_id > room_temperator > value > tick green button

2.7 Auto-detection ESP device from Home Assistant
With this mode, you need not to configure HA via "configuration.yaml".
Auto-detection via MQTT, the ESP need to send its configuration to HA to advertise itself.
The advertising topic need to follow format: 
<discovery_prefix>/<component>/[<node_id>/]<object_id>/config
We will make a demo with a send/receive data to/from HA and ESP. Here are the config topics:
"homeassistant/sensor/iotsharing/iotsharing_temp/config" (temperature)
"homeassistant/switch/iotsharing/iotsharing_light/config" (bulb)
The ESP will publish the temperature to HA via topic:
"homeassistant/sensor/iotsharing/temp_state"
and under json format:
"{{value_json.temperature}}"
The HA will publish command to ESP via topic:
"homeassistant/switch/iotsharing/light_state"
And ESP will send response back to HA after received the command via topic:
"homeassistant/switch/iotsharing/light_state/set"
Here is the Arduino ESP code:
#include <WiFi.h>
#include <PubSubClient.h>
#include "ArduinoJson.h"
#include <ESPmDNS.h>

#define wifi_ssid       "I3.41"
#define wifi_password   "xxx"
#define mqtt_server     "homeassistant"
#define mqtt_user       "iotsharing"
#define mqtt_password   "yyy"
#define mqtt_clientId   "iotsharing-sensor"

//The discovery topic need to follow a specific format:
//   <discovery_prefix>/<component>/[<node_id>/]<object_id>/config
#define TOPIC_TEMP_CONF   "homeassistant/sensor/iotsharing/iotsharing_temp/config"
#define TOPIC_PRESS_CONF  "homeassistant/switch/iotsharing/iotsharing_light/config"
#define TEMP_STATE        "homeassistant/sensor/iotsharing/temp_state"
#define LIGHT_STATE       "homeassistant/switch/iotsharing/light_state"
#define CMD_LIGHT_STATE   "homeassistant/switch/iotsharing/light_state/set"

#define TEMP_NAME         "IOTSHARING TEMP"
#define TEMP_CLASS        "temperature"
#define LIGHT_NAME        "IOTSHARING LIGHT"

//variables
WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
float temp = 0.00;
float pressure = 0.00;

//functions
void publishTempConfig() {
  const size_t bufferSize = JSON_OBJECT_SIZE(8);
  DynamicJsonBuffer jsonBuffer(bufferSize);
  JsonObject& root = jsonBuffer.createObject();
  root["name"] = TEMP_NAME;
  root["dev_cla"] = TEMP_CLASS;
  root["stat_t"] = TEMP_STATE;
  root["unit_of_meas"] = "°C";
  root["val_tpl"] = "{{value_json.temperature}}";
  root.prettyPrintTo(Serial);
  Serial.println("");
  char message[256];
  root.printTo(message, sizeof(message));
  client.publish(TOPIC_TEMP_CONF, message, true);
}

void publishLightConfig() {
  const size_t bufferSize = JSON_OBJECT_SIZE(8);
  DynamicJsonBuffer jsonBuffer(bufferSize);
  JsonObject& root = jsonBuffer.createObject();
  root["name"] = LIGHT_NAME;
  root["stat_t"] = LIGHT_STATE;
  root["cmd_t"] = CMD_LIGHT_STATE;
  root["pl_on"] = "on";
  root["pl_off"] = "off";
  root.prettyPrintTo(Serial);
  Serial.println("");
  char message[256];
  root.printTo(message, sizeof(message));
  client.publish(TOPIC_PRESS_CONF, message, true);
}

void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(wifi_ssid);
  WiFi.begin(wifi_ssid, wifi_password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void reconnect() {
  //Loop until we're reconnected
  while (!client.connected()) {
    if (client.connect(mqtt_clientId, mqtt_user, mqtt_password)) {
      Serial.println("connected");
      delay(1000);
      publishTempConfig();
      delay(1000);
      publishLightConfig();
      delay(500);
      client.subscribe(CMD_LIGHT_STATE);
      Serial.print("MQTT connected!");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
    }
  }
}

void receivedCallback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message received: ");
  Serial.print("payload: ");
  String cmd = "";
  for (int i = 0; i < length; i++) {
    cmd += (char)payload[i];
  }
  Serial.println(cmd);
  
  if(cmd == "on"){
    client.publish(LIGHT_STATE, "on");
  } else {
    client.publish(LIGHT_STATE, "off");
  }
}

void publishData(float p_temperature) {
  const size_t bufferSize = JSON_OBJECT_SIZE(2);
  DynamicJsonBuffer jsonBuffer(bufferSize);
  JsonObject& root = jsonBuffer.createObject();
  root["temperature"] = (String)p_temperature;
  //root.prettyPrintTo(Serial);
  char data[74];
  root.printTo(data, sizeof(data));
  client.publish(TEMP_STATE, data, true);
}

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

  
  if (!MDNS.begin("esp32")) {
    Serial.println("Error setting up MDNS responder!");
    while(1) {
        delay(1000);
    }
  }

  IPAddress serverIp = MDNS.queryHost(mqtt_server);
  Serial.print("IP address of server: ");
  Serial.println(serverIp.toString());
  client.setServer(serverIp, 1883);
  client.setCallback(receivedCallback);
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
  long now = millis();
  if(now - lastMsg > 5000) {
    lastMsg = now;
    if (!client.connected()) {
      reconnect();
    }
    temp = (float)random(10, 50);
    publishData(temp);
  }
}
From HA, choose Edit Dashboards > Add Card > Entities
You can try to press the IOTSHARING LIGHT switch andopen the Terminal of Arduino IDE:
and monitor IOTSHARING TEMP
2.8 Automation script
This feature allows users to specify a sequence of actions to be executed by Home Assistant. We will make a script to turn off the bulb at 9PM and turn on at 6PM. We will re-use the IOTSHARING LIGHT entity in previous step.
You must configure your time zone properly in:
Configuration > General > Time Zone (choose according to your location) > Save
from HA left Menu choose Configuration > Automations > ADD AUTOMATION > START WITH AN EMPTY AUTOMATION > give the name for script.
We will make 2 automations  auto light off and  auto light on following:
The first automation is to turn off light if time is 9PM
Name: auto light off
Mode: Single
switch Enable/Disable automation
Trigger-type: Time Pattern
Hours: 21, Minutes: 00, Seconds: 00
Action type: Call service
Service: switch.turn_off
Name(s) of entities to turn off: switch.iotsharing_light

The secondautomation is to turn onlight if time is 6PM
Name: auto light on
Mode: Single
switch Enable/Disable automation
Trigger-type: Time Pattern
Hours: 18, Minutes: 00, Seconds: 00
Action type: Call service
Service: switch.turn_on
Name(s) of entities to turn on: switch.iotsharing_light
2.9 Grafana platform for beautiful analytics and monitoring
This step will resue 2.6 Use InfluxDB to store data
choose Grafana from left Menu > choose Configure > Data Sources
choose InfluxDB
Fill the HTTP part with:
      URL: http://192.168.1.8:8086 (ip address of HA)
Fill the InfluxDB Details with (resuse previous InfluxDB step):
     Database: myhome
     User and Password as in previous InfluxDB step
     HTTP Method: GET
choose Save & Test
choose Create > Dashboard
choose Add new panel
choose toogle text edit mode (pen button) to modify the Query using text mode (you can use GUI mode by clicking on every Query rows.
Copy and Paste the Query below into Query text editor
SELECT "value" FROM "autogen"."°C" WHERE ("entity_id" = 'room_temperator')
choose Query Inspector > Refresh
choose Apply button
Finally,
You can choose range time at the top of the chart to make it easy to read.
2.10 Monitor ESP camera
We will turn ESP CAM module to a CCTV - IP camera which are capable to stream its video with MJPEG into Home Assistant.
Open "configuration.yaml" and add the entry below and reset HA services.
camera:
  - platform: mjpeg
    name: Livingroom Camera
    mjpeg_url: http://ip_of_esp_cam/video
change ip_of_esp_cam according to yours.
Flash the ESP32-Cam code:
#include "esp_camera.h"
#include <WiFi.h>

#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27

#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

WiFiServer server(80);
bool connected = false;
WiFiClient live_client;


String index_html = "<meta charset=\"utf-8\"/>\n" \
                    "<style>\n" \
                    "#content {\n" \
                    "display: flex;\n" \
                    "flex-direction: column;\n" \
                    "justify-content: center;\n" \
                    "align-items: center;\n" \
                    "text-align: center;\n" \
                    "min-height: 100vh;}\n" \
                    "</style>\n" \
                    "<body bgcolor=\"#000000\"><div id=\"content\"><h2 style=\"color:#ffffff\">HTTP ESP32 Cam live stream </h2><img src=\"video\"></div></body>";

void configCamera(){
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;

  config.frame_size = FRAMESIZE_QVGA;
  config.jpeg_quality = 9;
  config.fb_count = 1;

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
}

//continue sending camera frame
void liveCam(WiFiClient &client){
  //capture a frame
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) {
      Serial.println("Frame buffer could not be acquired");
      return;
  }
  client.print("--frame\n");
  client.print("Content-Type: image/jpeg\n\n");
  client.flush();
  client.write(fb->buf, fb->len);
  client.flush();
  client.print("\n");
  //return the frame buffer back to be reused
  esp_camera_fb_return(fb);
}

void setup() {
  Serial.begin(115200);
  WiFi.begin("I3.41", "xxx");
  Serial.println("");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  String IP = WiFi.localIP().toString();
  Serial.println("IP address: " + IP);
  index_html.replace("server_ip", IP);
  server.begin();
  configCamera();
}
    
void http_resp(){
  WiFiClient client = server.available();                    
    /* check client is connected */           
  if (client.connected()) {     
      /* client send request? */     
      /* request end with '\r' -> this is HTTP protocol format */
      String req = "";
      while(client.available()){
        req += (char)client.read();
      }
      Serial.println("request " + req);
      /* First line of HTTP request is "GET / HTTP/1.1"  
        here "GET /" is a request to get the first page at root "/"
        "HTTP/1.1" is HTTP version 1.1
      */
      /* now we parse the request to see which page the client want */
      int addr_start = req.indexOf("GET") + strlen("GET");
      int addr_end = req.indexOf("HTTP", addr_start);
      if (addr_start == -1 || addr_end == -1) {
          Serial.println("Invalid request " + req);
          return;
      }
      req = req.substring(addr_start, addr_end);
      req.trim();
      Serial.println("Request: " + req);
      client.flush();
  
      String s;
      /* if request is "/" then client request the first page at root "/" -> we process this by return "Hello world"*/
      if (req == "/")
      {
          s = "HTTP/1.1 200 OK\n";
          s += "Content-Type: text/html\n\n";
          s += index_html;
          s += "\n";
          client.print(s);
          client.stop();
      }
      else if (req == "/video")
      {
          live_client = client;
          live_client.print("HTTP/1.1 200 OK\n");
          live_client.print("Content-Type: multipart/x-mixed-replace; boundary=frame\n\n");
          live_client.flush();
          connected = true;
      }
      else
      {
          /* if we can not find the page that client request then we return 404 File not found */
          s = "HTTP/1.1 404 Not Found\n\n";
          client.print(s);
          client.stop();
      }
    }       
}

void loop() {
  http_resp();
  if(connected == true){
    liveCam(live_client);
  }
}
choose Edit Dashboard > Add Card > By Entity > choose Lingroom Cam > Continue > Add to LoveLace UI


Thanks all.

Post a Comment

2 Comments

Appreciating the time and effort you put into your website and the in-depth information you offer. I would also like to recommend you go with top solar companies in India
Samuel Bryant said…
Interesting thoughts.