Умный дом на openHAB+MQTT+Arduino. Часть 2: Датчики, релюшки

от автора

Продолжаем разговор за бюджетный умный дом, в этой статье мы соберем простой модуль на Arduino Nano. Предыдущая статья, посвященная настройке кластера openHAB, находится тут.

Я выбрал Arduino Nano, потому что для него существует вот такой очень удобный шилд:

NANO IO Shield

NANO IO Shield

К такому шилду удобно подключать все что угодно при помощи кабельных наконечников НШВИ 0,5-8. А VCC и GND я подключаю через латунные шины, т.к. к ним надо подключать кучку проводов. Тоже получилось удобно. Собрал все в корпусе распределительной коробки.  Вот так это в итоге выглядит:

Это пример модуля для кухни. К нему подключены два реле, для управления светом, сенсорный выключатель, датчик движения, датчик открытия окна (геркон), два датчика протечки, датчик газа MQ2 (т.к. присутствует и печное отопление, хочу знать CO), датчик температуры. Для связи модуля с MQTT-брокером используется Ethernet модуль enc28j60.

Вместо схемы подключения, предлагаю Вам такую таблицу, по-моему тут все предельно понятно:

Arduino Pin 

Module Pin 

Module 

D2 

In 

Реле 1 

GND 

GND 

VCC 

VCC 

D7 

In 

Реле 2 

GND 

GND 

VCC 

VCC 

D3 

Датчик протечки 1 

GND 

— 

VCC 

D4 

Датчик протечки 2 

 

 

GND 

— 

VCC 

D5 

Желтый (DQ) 

Датчик температуры DS18b20

*нужен резистор на 4.7 кОм между DQ и VCC

GND 

Черный 

VCC 

Красный 

D6 

Out 

Инфракрасный датчик движения 

 

GND 

GND 

VCC 

VCC 

D8 

Датчик открытия 

GND 

D9 

SIG 

Сенсорный 

Выключатель 

GND 

GND 

VCC 

VCC 

A0 

Analog Out 

Датчик газа MQ2 

*цифровой выход не использую

GND 

GND 

VCC 

VCC 

3.3v 

VCC 

Модуль enc28j60 

*для UNO пин CS нужно подключать к D8

GND 

GND 

D10 

CS 

D11 

SI 

D12 

SO 

D13 

SCK 

Примеры, модулей (хотел разместить в таблице справа. но не получилось):

Реле

Реле
Датчик движения

Датчик движения
Датчик протечки

Датчик протечки
DS18b20

DS18b20
Датчик открытия

Датчик открытия
Сенсорный датчик

Сенсорный датчик
MQ2

MQ2
Модуль enc28j60

Модуль enc28j60

Подобных модулей умного дома у меня будет около десятка, все оснащены разными датчиками и исполнительными механизмами, т.к. это ардуино, то все очень быстро переписывается под разные нужды.
Я использую последнюю на данный момент Arduino IDE 2.3.2.

Чтобы собрать скетч, нужно установить некоторые библиотеки. В Arduino IDE идем в Tools->Manage Libraries.

Library manager

Library manager

И тут устанавливаем: 

OneWire 2.3.7; 

UIPEthernet 2.0.12; 

PubSubClient 2.7.0.

Целеком скетч можно посмотреть тут:

Hidden text
#include <avr/wdt.h> #include <OneWire.h> #include "PubSubClient.h" #include <UIPEthernet.h> #define DEBUG 1  // Debug output to serial console  const byte Relay1Pin = 2; const byte w1Pin = 3; const byte w2Pin = 4; OneWire ds(5); const byte md1Pin = 6; const byte Relay2Pin = 7; const byte Win1Pin = 8; const byte Button1Pin = 9; const byte MQ2pin = A0;  byte tmp = 0; byte w1Value = 0; byte w2Value = 0; byte md1Value = 0; byte win1Value = 0; byte button1Value = 0; byte l2Status = 0;  unsigned int gasValue = 0; unsigned long mytime = 0;  const char* mqtt_server = "10.20.10.40"; const char* mqttUser = "arduino-kitchen-01"; const char* mqttPassword = "password123";  #define MACADDRESS 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 #define MYIPADDR 10, 20, 10, 50 #define MYIPMASK 255, 255, 255, 0 #define MYDNS 10, 20, 10, 10 #define MYGW 10, 20, 10, 10  char buf[10];                // Buffer to store the sensor value int updateInterval = 10000;  // Interval in milliseconds EthernetClient espClient; PubSubClient client(espClient);  void reconnect() {   // Loop until we're reconnected   while (!client.connected()) {     #ifdef DEBUG     Serial.print(F("MQTT connection..."));     #endif     wdt_disable();     // Attempt to connect     if (client.connect("arduino-KITCHEN-01", mqttUser, mqttPassword)) {       #ifdef DEBUG       Serial.println("connected");       #endif       w1Value = digitalRead(w1Pin);       pubw1();       w2Value = digitalRead(w2Pin);       pubw2();       md1Value = digitalRead(md1Pin);       pubmd1();       win1Value = digitalRead(Win1Pin);       pubwin1();       client.subscribe("oh/kitchen/l1");       client.subscribe("oh/kitchen/l2");       wdt_enable(WDTO_8S);     } else {       #ifdef DEBUG       Serial.print(F("failed, rc="));       Serial.print(client.state());       Serial.println(F(" try again in 5s"));       #endif       // Wait 5 seconds before retrying       delay(5000);     }   } }  void sensors() {   byte data_t[2];   ds.reset();      // Начинаем взаимодействие со сброса всех предыдущих команд и параметров   ds.write(0xCC);  // Даем датчику DS18b20 команду пропустить поиск по адресу. В нашем случае только одно устрйоство   ds.write(0x44);  // Даем датчику DS18b20 команду измерить температуру. Само значение температуры мы еще не получаем - датчик его положит во внутреннюю память   delay(1500);   ds.reset();  // Теперь готовимся получить значение измеренной температуры   ds.write(0xCC);   ds.write(0xBE);  // Просим передать нам значение регистров со значением температуры   // Получаем и считываем ответ   data_t[0] = ds.read();  // Читаем младший байт значения температуры   data_t[1] = ds.read();  // А теперь старший   float temperature = ((data_t[1] << 8) | data_t[0]) * 0.0625;    dtostrf(temperature, 4, 1, buf);   #ifdef DEBUG   Serial.print("Temp: ");   Serial.println(buf);   #endif   client.publish("oh/kitchen/temp", buf);    gasValue = analogRead(MQ2pin);   dtostrf(gasValue, 4, 1, buf);   #ifdef DEBUG   Serial.print("gasValue: ");   Serial.println(buf);   #endif   client.publish("oh/kitchen/gas", buf); }  void callback(char* topic, byte* message, unsigned int length) {   #ifdef DEBUG   Serial.print(F("Msg arrived on topic: "));   Serial.print(topic);   Serial.print(". Msg: ");   #endif   String messageTemp;    for (int i = 0; i < length; i++) {     #ifdef DEBUG     Serial.print((char)message[i]);     #endif     messageTemp += (char)message[i];   }   #ifdef DEBUG   Serial.println();   #endif     if (String(topic) == "oh/kitchen/l2") {     if (String(messageTemp) == "1") {       l2Status = 1;       digitalWrite(Relay1Pin, 0);     }     if (String(messageTemp) == "0") {       l2Status = 0;       digitalWrite(Relay1Pin, 1);     }   }    if (String(topic) == "oh/kitchen/l1") {     if (String(messageTemp) == "1") {        digitalWrite(Relay2Pin, 0);     }     if (String(messageTemp) == "0") {        digitalWrite(Relay2Pin, 1);     }   } }  void setup() {   wdt_disable();   digitalWrite(Relay1Pin, 1);   digitalWrite(Relay2Pin, 1);   pinMode(Relay1Pin, OUTPUT);   pinMode(Relay2Pin, OUTPUT);   pinMode(w1Pin, INPUT_PULLUP);   pinMode(w2Pin, INPUT_PULLUP);   pinMode(md1Pin, INPUT_PULLUP);   pinMode(Win1Pin, INPUT_PULLUP);   pinMode(Button1Pin, INPUT_PULLUP);   #ifdef DEBUG   Serial.begin(9600);   #endif   uint8_t mac[6] = { MACADDRESS };   uint8_t myIP[4] = { MYIPADDR };   uint8_t myMASK[4] = { MYIPMASK };   uint8_t myDNS[4] = { MYDNS };   uint8_t myGW[4] = { MYGW };   Ethernet.begin(mac, myIP, myDNS, myGW, myMASK);   client.setServer(mqtt_server, 1883);   client.setCallback(callback);   #ifdef DEBUG   Serial.println(F("\nStart!"));   #endif }  void pubw1() {   client.publish("oh/kitchen/w1", itoa(w1Value, buf, 2));   #ifdef DEBUG   Serial.print("W1: ");   Serial.println(w1Value);   #endif }  void pubw2() {   client.publish("oh/kitchen/w2", itoa(w2Value, buf, 2));   #ifdef DEBUG   Serial.print("W2: ");   Serial.println(w2Value);   #endif }  void pubmd1() {   client.publish("oh/kitchen/md1", itoa(md1Value, buf, 2));   #ifdef DEBUG   Serial.print("md1: ");   Serial.println(md1Value);   #endif }  void pubwin1() {   client.publish("oh/kitchen/win1", itoa(win1Value, buf, 2));   #ifdef DEBUG   Serial.print("win1: ");   Serial.println(win1Value);   #endif }  void loop() {   wdt_reset();   if (!client.connected()) { reconnect(); }   client.loop();   if (millis() - mytime > updateInterval) {     mytime = millis();     sensors();   }    tmp = digitalRead(w1Pin);   if (tmp != w1Value) {     w1Value = tmp;     pubw1();   }   tmp = digitalRead(w2Pin);   if (tmp != w2Value) {     w2Value = tmp;     pubw2();   }   tmp = digitalRead(md1Pin);   if (tmp != md1Value) {     md1Value = tmp;     pubmd1();   }   tmp = digitalRead(Win1Pin);   if (tmp != win1Value) {     win1Value = tmp;     pubwin1();   }    if (digitalRead(Button1Pin) == 0 && button1Value == 0) {     char* tmp = "0";     if (l2Status == 0) {       tmp = "1";     }     client.publish("oh/kitchen/l2", tmp);     button1Value = 1;     digitalWrite(Relay1Pin, 0);   } else if (digitalRead(Button1Pin) == 1) {     button1Value = 0;   } }

Опишу подробнее, упустив однотипные куски кода и отладку.

Указываем пины ардуины для наших датчиков и реле: 

const byte Relay1Pin = 2; //реле 1  const byte w1Pin = 3;     //датчик протечки 1  const byte w2Pin = 4;     //датчик протечки 2  OneWire ds(5);            //датчик DS18b20  const byte md1Pin = 6;    //датчик движения  const byte Relay2Pin = 7; //реле 2  const byte Win1Pin = 8;   //датчик открытия окна  const byte Button1Pin = 9;//сенсорный ввыключатель  const byte MQ2pin = A0;   //датчик газа 

Укажим параметры соединения для MQTT: 

const char* mqtt_server = "10.20.10.40";  const char* mqttUser = "arduino-kitchen-01";  const char* mqttPassword = "password123"; 

Так же укажем сетевые настройки:  

#define MACADDRESS 0x00,0x01,0x02,0x03,0x04,0x05  #define MYIPADDR 10,20,10,50  #define MYIPMASK 255,255,255,0  #define MYDNS 10,20,10,10  #define MYGW 10,20,10,10 

Не забывем менять MACADDRESS и MYIPADD для каждого подобного устройства. Инициализация:

void setup(){ //выключаем watchdog  wdt_disable(); //инициализируем GPIO, для входных линий указываем использовать подтягиваюший резистор (INPUT_PULLUP));  digitalWrite(Relay1Pin, 1);  pinMode(Relay1Pin, OUTPUT);  pinMode(w1Pin, INPUT_PULLUP);   //инициализация Ethernet  uint8_t mac[6] = {MACADDRESS};  uint8_t myIP[4] = {MYIPADDR};  uint8_t myMASK[4] = {MYIPMASK};  uint8_t myDNS[4] = {MYDNS};  uint8_t myGW[4] = {MYGW};  Ethernet.begin(mac,myIP,myDNS,myGW,myMASK);   //инициализация библиотеки MQTT  client.setServer(mqtt_server, 1883);  client.setCallback(callback); }

 Тут “callback” это функция, которая вызывается если в топик, на который мы подписаны, прилетает какое-то значение. Вот эта функция (в сокращении):

void callback(char* topic, byte* message, unsigned int length) {    String messageTemp;    for (int i = 0; i < length; i++) {      messageTemp += (char)message[i];    }  if (String(topic) == "oh/kitchen/l2") {    if (String(messageTemp) == "1") {      l2Status = 1;      digitalWrite(Relay1Pin, 0);    }     if (String(messageTemp) == "0") {      l2Status = 0;      digitalWrite(Relay1Pin, 1);    } } }

т.е. тут мы из канала «oh/kitchen/l2» получаем или 0 или 1, тем самым включаем или выключаем реле. Периодически из основного цикла вызывается функция опроса датчиков:

void sensors() {    byte data_t[2];    ds.reset();    ds.write(0xCC);    ds.write(0x44);   delay (1500);    ds.reset();    ds.write(0xCC);     ds.write(0xBE);    data_t[0] = ds.read();    data_t[1] = ds.read();    float temperature = ((data_t[1] << 8) | data_t[0]) * 0.0625;    dtostrf(temperature,4, 1, buf);    client.publish("oh/kitchen/temp", buf);    gasValue = analogRead(MQ2pin);    dtostrf(gasValue,4, 1, buf);    client.publish("oh/kitchen/gas", buf);  } 

Тут считываем температуру и содержание CO, после чего пишем в соответствующие каналы, предварительно преобразовав значения в строку. Основной цикл, тоже в сокращении:

void loop(){    wdt_reset(); //сбрасываем watchdog таймер    //если соединение c mqtt не установлено, то устанавливаем    if (!client.connected()) { reconnect(); }      client.loop();    //время от времени дергаем опрос датчиков    if (millis()-mytime>updateInterval){      mytime=millis();       sensors();    }     //обработаем сенсорную кнопку    if (digitalRead(Button1Pin) == 0 && button1Value == 0) {      char* tmp = "0";      if (l2Status == 0) {tmp = "1";}      client.publish("oh/kitchen/l2", tmp);      button1Value = 1;      digitalWrite(Relay1Pin, 0);    } else if (digitalRead(Button1Pin) == 1) {      button1Value = 0;    }     //остальные датчики    tmp = digitalRead(w1Pin);    if (tmp != w1Value) {      w1Value = tmp;      pubw1();    }  } 

Прямо тут обрабатываются все простые датчики и пишутся в соответствующие каналы, но через определенную функцию, для примера тут pubw1(), сделано для экономии памяти, т.к. эта функция вызывается еще из reconnect(), для того чтобы при старте передать актуальные значения всех датчиков, вот эта функция:

void reconnect() {    while (!client.connected()) {    //выключаем watchdog, т.к. соединение может устанавливаться более 8 секунд, а либа его не сбрасывает    wdt_disable();    // Attempt to connect    if (client.connect("arduino-KITCHEN-01", mqttUser, mqttPassword)) {      w1Value = digitalRead(w1Pin);      pubw1();      w2Value = digitalRead(w2Pin);      pubw2();      md1Value = digitalRead(md1Pin);      pubmd1();      win1Value = digitalRead(Win1Pin);      pubwin1();    //тут же подписываемся на необходимые топики    client.subscribe("oh/kitchen/l2");    //и включаем watchdog    wdt_enable(WDTO_8S);   }   }  }

Вот и весь скетч. Приведу еще конфиг openHAB с моими item’ами:

[root@srv-oh-01 ~]# cat /etc/openhab/items/pleshki.items

Hidden text
Group           Home                        "Плешки"            <house>                                           ["Building"] Group           GF                          "Первый этаж"       <groundfloor>     (Home)                          ["GroundFloor"] Group           OU                          "За домом"          <garden>          (Home)                          ["Outdoor"] Group           AT                          "Второй этаж"       <attic>           (Home)                          ["Attic"] Group           GF_FamilyRoom               "Гостиная"          <parents_2_4>     (Home, GF)                      ["Room"] Group           GF_Kitchen                  "Кухня"             <kitchen>         (Home, GF)                      ["Kitchen"] Group           OU_Toilet                   "Туалет"            <toilet>          (Home, OU)                      ["Bathroom"] Group           AT_StorageRoom              "Кладовка"          <suitcase>        (Home, AT)                      ["Room"] Switch          Security                    "Охрана"         <security>           (Home,house,GF_FamilyRoom)             ["Security", "Switchable"] Number          OU_Temperature              "Температура"       <temperature>     (OU, garden, gTemperature)      ["Temperature"] Number          OU_Humidity                 "Влажность [%.2f]%unit%"         <Humidity>        (OU, garden, gHumidity)      ["Humidity"] Number          OU_Pressure                 "Атмосферное давление [%.1f]%unit%"         <Pressure>        (OU, garden, gPressure)      ["Pressure"] Number          OU_windDir_Num              "Направление ветра [%s] градусы"                    <wind>            (OU, garden, gWeather) String          OU_windDir                  "Направление ветра [%s]"                    <wind>            (OU, garden, gWeather) Dimmer          GF_FamilyRoom_Light         "Освещение"         <light>           (GF_FamilyRoom, gLight)         ["Light"] Rollershutter   GF_FamilyRoom_Shutter_1     "Шторы 1"             <rollershutter>   (GF_FamilyRoom, gShutter)       ["Rollershutter"] Rollershutter   GF_FamilyRoom_Shutter_2     "Шторы 2"             <rollershutter>   (GF_FamilyRoom, gShutter)       ["Rollershutter"] Rollershutter   GF_FamilyRoom_Shutter_3     "Шторы 3"             <rollershutter>   (GF_FamilyRoom, gShutter)       ["Rollershutter"] Rollershutter   GF_FamilyRoom_Shutter_4     "Шторы 4"             <rollershutter>   (GF_FamilyRoom, gShutter)       ["Rollershutter"] Number          GF_FamilyRoom_Temperature   "Температура"       <temperature>     (GF_FamilyRoom, gTemperature)   ["Temperature"]                     Contact         GF_FamilyRoom_Motion        "Датчик движения в гостиной"   <motion>          (GF_FamilyRoom, gMotion)        ["MotionDetector", "Switchable"]    Number          GF_FamilyRoom_Gas           "Содержание CO [%.1f]%unit%"       <gas>     (GF_FamilyRoom, gGas)      ["Gas"] Number          GF_FamilyRoom_Humidity      "Влажность"       <Humidity>     (GF_FamilyRoom, gHumidity)      ["Humidity"] Switch          GF_Kitchen_Light_1          "Освещение общее"         <light>           (GF_Kitchen, gLight)            ["Light", "Switchable"] Switch          GF_Kitchen_Light_2          "Освещение рабочей зоны"  <light>           (GF_Kitchen, gLight)            ["Light", "Switchable"] Rollershutter   GF_Kitchen_Shutter_1        "Шторы 1"             <rollershutter>   (GF_Kitchen, gShutter)          ["Rollershutter"] Rollershutter   GF_Kitchen_Shutter_2        "Шторы 2"             <rollershutter>   (GF_Kitchen, gShutter)          ["Rollershutter"] Rollershutter   GF_Kitchen_Shutter_3        "Шторы 3"             <rollershutter>   (GF_Kitchen, gShutter)          ["Rollershutter"] Number          GF_Kitchen_Temperature      "Температура"       <temperature>     (GF_Kitchen, gTemperature)      ["Temperature"] Number          GF_Kitchen_Gas              "Содержание CO [%.1f]%unit%"       <gas>     (GF_Kitchen, gGas)      ["Gas"] Contact         GF_Kitchen_Water_1          "Датчик протечки - раковина"        <water>   (GF_Kitchen, gWater)    ["Water"] Contact         GF_Kitchen_Water_2          "Датчик протечки - стиралка"        <water>   (GF_Kitchen, gWater)    ["Water"] Contact         GF_Kitchen_Motion           "Датчик движения на кухне"   <motion>          (GF_Kitchen, gMotion)           ["MotionDetector"] Switch          OU_Toilet_Light             "Освещение"         <light>           (OU_Toilet, gLight)             ["Light", "Switchable"] Contact         AT_StorageRoom_Motion       "Датчик движения"   <motion>          (AT_StorageRoom, gMotion)       ["MotionDetector", "Switchable"] Contact         GF_Kitchen_Window_1         "Окно 1 на кухне"   <window>          (GF_Kitchen, gWindow)           ["Window"] Contact         GF_Kitchen_Door_1           "Дверь 1 на кухне"  <door>          (GF_Kitchen, gDoor)           ["Door"] Contact         GF_Kitchen_Button_1         "Кнопка у плиты"   <switch>          (GF_Kitchen, gButton)           ["Button"] Group:Switch:OR(ON, OFF)           gLight        "Освещение"         <light>           (Home)   ["Light", "Switchable"] Group:Rollershutter:OR(UP, DOWN)   gShutter      "Шторы"             <rollershutter>   (Home)   ["Rollershutter"] Group:Number:AVG                   gTemperature  "Температура"       <temperature>     (Home)   ["Temperature"] Group:Number:AVG                   gGas          "Содержание CO"     <gas>             (Home)   ["Gas"] Group:Number:AVG                   gHumidity     "Влажность"     <humidity>            (Home)   ["Humidity"] Group:Contact:OR(OPEN, CLOSED)          gMotion  "Датчик движения"   <motion>      (Home)   ["MotionDetector"] Group:Contact:OR(OPEN, CLOSED)          gWater   "Датчик протечки"   <water>       (Home)   ["Water"] Group:Contact:OR(OPEN, CLOSED)          gWindow  "Окно"   <window>                (Home)   ["Window"] Group:Contact:OR(OPEN, CLOSED)          gDoor    "Дверь"   <door>                   (Home)   ["Door"] DateTime Date "Дата [%1$td.%1$tm.%1$tY]" <calendar> { channel="ntp:ntp:ourhome:dateTime" } DateTime CurTime "Время [%1$tR]" <clock> { channel="ntp:ntp:ourhome:dateTime" } DateTime         Sunrise_Time       "Sunrise [%1$tH:%1$tM]"                   { channel="astro:sun:local:rise#start" } DateTime         Sunset_Time        "Sunset [%1$tH:%1$tM]"                    { channel="astro:sun:local:set#start" } Number  Day     "Day [%d]"      <day>   (Home) String VoiceCommand

В интерфейсе openHAB это все выглядит так:

Автоматизация. В ночное время суток при активации датчика движения, будет включаться свет на кухне. Вот такое правило отрабатывает:

rule "Light on kitten work zone" when     Item GF_Kitchen_Motion changed then     if (Day.state == 0){         if (GF_Kitchen_Motion.state == OPEN){             if (GF_Kitchen_Light_2.state == OFF){                 logInfo("pleshki.rules", "GF_Kitchen_Motion is: " + GF_Kitchen_Motion.state )                 sendCommand(GF_Kitchen_Light_2, ON)             }         }     }     if (GF_Kitchen_Motion.state == CLOSED){             if (GF_Kitchen_Light_2.state == ON){                 logInfo("pleshki.rules", "GF_Kitchen_Motion is: " + GF_Kitchen_Motion.state )                 sendCommand(GF_Kitchen_Light_2, OFF)     }     } end

На сегодня все. В следующий раз я напишу, как можно управлять рольшторами при помощи самодельного привода.


ссылка на оригинал статьи https://habr.com/ru/articles/806343/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *