Умный дом на openHAB+MQTT+Arduino. Часть 2: Датчики, релюшки
Продолжаем разговор за бюджетный умный дом, в этой статье мы соберем простой модуль на 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 | S | Датчик протечки 1 |
GND | — | |
VCC | + | |
D4 | S | Датчик протечки 2 |
GND | — | |
VCC | + | |
D5 | Желтый (DQ) | Датчик температуры DS28b20 *нужен резистор на 4.7 кОм между DQ и VCC |
GND | Черный | |
VCC | Красный | |
D6 | Out | Инфракрасный датчик движения |
GND | GND | |
VCC | VCC | |
D8 | 1 | Датчик открытия |
GND | 2 | |
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 |
Примеры, модулей (хотел разместить в таблице справа., но не получилось):
Реле
Датчик движения
Датчик протечки
DS28b20
Датчик открытия
Сенсорный датчик
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
#include
#include "PubSubClient.h"
#include
#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); //датчик DS28b20
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 "Плешки" ["Building"]
Group GF "Первый этаж" (Home) ["GroundFloor"]
Group OU "За домом" (Home) ["Outdoor"]
Group AT "Второй этаж" (Home) ["Attic"]
Group GF_FamilyRoom "Гостиная" (Home, GF) ["Room"]
Group GF_Kitchen "Кухня" (Home, GF) ["Kitchen"]
Group OU_Toilet "Туалет" (Home, OU) ["Bathroom"]
Group AT_StorageRoom "Кладовка" (Home, AT) ["Room"]
Switch Security "Охрана" (Home,house,GF_FamilyRoom) ["Security", "Switchable"]
Number OU_Temperature "Температура" (OU, garden, gTemperature) ["Temperature"]
Number OU_Humidity "Влажность [%.2f]%unit%" (OU, garden, gHumidity) ["Humidity"]
Number OU_Pressure "Атмосферное давление [%.1f]%unit%" (OU, garden, gPressure) ["Pressure"]
Number OU_windDir_Num "Направление ветра [%s] градусы" (OU, garden, gWeather)
String OU_windDir "Направление ветра [%s]" (OU, garden, gWeather)
Dimmer GF_FamilyRoom_Light "Освещение" (GF_FamilyRoom, gLight) ["Light"]
Rollershutter GF_FamilyRoom_Shutter_1 "Шторы 1" (GF_FamilyRoom, gShutter) ["Rollershutter"]
Rollershutter GF_FamilyRoom_Shutter_2 "Шторы 2" (GF_FamilyRoom, gShutter) ["Rollershutter"]
Rollershutter GF_FamilyRoom_Shutter_3 "Шторы 3" (GF_FamilyRoom, gShutter) ["Rollershutter"]
Rollershutter GF_FamilyRoom_Shutter_4 "Шторы 4" (GF_FamilyRoom, gShutter) ["Rollershutter"]
Number GF_FamilyRoom_Temperature "Температура" (GF_FamilyRoom, gTemperature) ["Temperature"]
Contact GF_FamilyRoom_Motion "Датчик движения в гостиной" (GF_FamilyRoom, gMotion) ["MotionDetector", "Switchable"]
Number GF_FamilyRoom_Gas "Содержание CO [%.1f]%unit%" (GF_FamilyRoom, gGas) ["Gas"]
Number GF_FamilyRoom_Humidity "Влажность" (GF_FamilyRoom, gHumidity) ["Humidity"]
Switch GF_Kitchen_Light_1 "Освещение общее" (GF_Kitchen, gLight) ["Light", "Switchable"]
Switch GF_Kitchen_Light_2 "Освещение рабочей зоны" (GF_Kitchen, gLight) ["Light", "Switchable"]
Rollershutter GF_Kitchen_Shutter_1 "Шторы 1" (GF_Kitchen, gShutter) ["Rollershutter"]
Rollershutter GF_Kitchen_Shutter_2 "Шторы 2" (GF_Kitchen, gShutter) ["Rollershutter"]
Rollershutter GF_Kitchen_Shutter_3 "Шторы 3" (GF_Kitchen, gShutter) ["Rollershutter"]
Number GF_Kitchen_Temperature "Температура" (GF_Kitchen, gTemperature) ["Temperature"]
Number GF_Kitchen_Gas "Содержание CO [%.1f]%unit%" (GF_Kitchen, gGas) ["Gas"]
Contact GF_Kitchen_Water_1 "Датчик протечки - раковина" (GF_Kitchen, gWater) ["Water"]
Contact GF_Kitchen_Water_2 "Датчик протечки - стиралка" (GF_Kitchen, gWater) ["Water"]
Contact GF_Kitchen_Motion "Датчик движения на кухне" (GF_Kitchen, gMotion) ["MotionDetector"]
Switch OU_Toilet_Light "Освещение" (OU_Toilet, gLight) ["Light", "Switchable"]
Contact AT_StorageRoom_Motion "Датчик движения" (AT_StorageRoom, gMotion) ["MotionDetector", "Switchable"]
Contact GF_Kitchen_Window_1 "Окно 1 на кухне" (GF_Kitchen, gWindow) ["Window"]
Contact GF_Kitchen_Door_1 "Дверь 1 на кухне" (GF_Kitchen, gDoor) ["Door"]
Contact GF_Kitchen_Button_1 "Кнопка у плиты" (GF_Kitchen, gButton) ["Button"]
Group:Switch:OR(ON, OFF) gLight "Освещение" (Home) ["Light", "Switchable"]
Group:Rollershutter:OR(UP, DOWN) gShutter "Шторы" (Home) ["Rollershutter"]
Group:Number:AVG gTemperature "Температура" (Home) ["Temperature"]
Group:Number:AVG gGas "Содержание CO" (Home) ["Gas"]
Group:Number:AVG gHumidity "Влажность" (Home) ["Humidity"]
Group:Contact:OR(OPEN, CLOSED) gMotion "Датчик движения" (Home) ["MotionDetector"]
Group:Contact:OR(OPEN, CLOSED) gWater "Датчик протечки" (Home) ["Water"]
Group:Contact:OR(OPEN, CLOSED) gWindow "Окно" (Home) ["Window"]
Group:Contact:OR(OPEN, CLOSED) gDoor "Дверь" (Home) ["Door"]
DateTime Date "Дата [%1$td.%1$tm.%1$tY]" { channel="ntp:ntp:ourhome:dateTime" }
DateTime CurTime "Время [%1$tR]" { 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]" (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
На сегодня все. В следующий раз я напишу, как можно управлять рольшторами при помощи самодельного привода.