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

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

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
Датчик открытия
Сенсорный датчик
MQ2
Модуль enc28j60

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

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

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

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