“Умный дом” на скорую руку

С нами продолжают делиться решениями на основе модулей Мастер Кит:
«В качестве эксперимента решил я тут попробовать сделать некий прототип «умного дома» на скорую руку и с минимальными затратами. «Хотелок» оказалось много: и свет, и вентиляция, и окна, и вода, и ИК-управление электроприборами. На первых порах решил ограничиться минимумом задач: вентиляция и освещение в комнате.

d293bab3c8e74a03bd2efc794bd85b98.JPG
За основу системы взял Arduino Uno, с возможностью управления четырьмя пинами независимо друг от друга, и несколько модулей беспроводного управления от Мастер Кит: роли исполнительных устройств взяли на себя одно- и двухканальные реле MP3328 и MP3330, а сигналы на них передаются с помощью восьмиканального передатчика MP3329 на частоте 433 МГц.

2459e34a025d4ed7b82eba1b1de97bd9.jpg3251f5709c59488cbd440baa8fbd6f0d.jpga8bffa657e7848aaa1fd046a3a72302f.jpg

130dc82ac36446b2b3684ae3083a3e8c.JPG

На MP3330 я повесил управление двумя светодиодными лентами над диваном, — уютная подсветка для вечернего чтения, —, а на MP3328 — управление серво-машинкой для открывания / закрывания окна.

Конструкцию привода соорудил из подручных материалов, а именно, из деталей конструктора LEGO.

5ca73a92faab408c9c18cf7f83584240.JPG

Ну и, разумеется, простого управления по радиоканалу мне было недостаточно: необходим был доступ ко всей системе через веб-интерфейс и мониторинг состояния в режиме реального времени.

В качестве ресурса хранения я взял стандартный MySQL, где я создал таблицу с айдишниками контроллеров, пинов и значением состояния каждого из них, в целях безопасности привязав к каждому общий идентификатор системы. Также, я жестко зашил возможность установки фиксированного таймера на изменение состояния, и под это тоже предусмотрено поле.

0cda7bc0c4334f88b1108a9d681cc18a.png

При работе с веб-сокетами Arduino слегка подглючивало, и разбираться в причинах пока времени не было, поэтому, набросал по-простому: ajax-запросами через каждые 100 мс. Это, конечно, губительно для трафика (почти 30Мб в сутки), —, но для квартиры с безлимитным интернетом на первое время хватит.

Простейший веб-интерфейс: 4 кнопки, по одной на каждый пин. Однократное нажатие изменяет состояние, а длительное нажатие устанавливает таймер (у меня пока жестко зашиты 3 минуты) на его изменение.

2925624a3e8a49b1af311f0997925b99.JPG6424494f306c41178c9b57e4698aa83c.JPG02ec67b48735456d9793e36837b50dbe.JPG

В итоге, упрощенная версия логики такова:

Веб-интерфейс обращается к серверу и проверяет состояние контроллеров, после чего, выводит интерфейс работы с системой: 4 кнопки в соответствующем состоянию виде:

Скрипты
$.ajax({
                        url: 'engine/ajax.php',
                        type: 'POST',
                        dataType: 'json',
                        data: {action: 'getStates'},
                })
                .done(function(data) {
                        for (key in data.success){
                                var controller = data.success[key];
                                if($('.btn#btn_' + controller.id).length === 0){
                                        html = '';
                                        html += '
0){ var seconds = 60 - (controller.timer_switch % 60) html += 'seconds="' + seconds + '"'; } html += 'id="btn_' + controller.id + '" class="btn" state="' + controller.state + '">
'; $('#btn_placer').append(html); } else { if (controller.state == 1){ $('.btn#btn_' + controller.id).removeClass('btn_off').addClass('btn_on'); } else if (controller.state == 0){ $('.btn#btn_' + controller.id).removeClass('btn_on').addClass('btn_off'); } if (controller.timer_switch > 0){ var seconds = 60 - controller.timer_switch % 60; var minutes = Math.floor(controller.timer_switch / 60); $('.btn#btn_' + controller.id).addClass('seconds').css('background-position', (-seconds * 100) + 'px 0px').find('.btn_timer').text(minutes + 'M'); } else { $('.btn#btn_' + controller.id).css('background-position', '0px 0px').removeClass('seconds').find('.btn_timer').text(''); } } } setTimeout(function(){ getStates(); }, 1000); }) .fail(function(data) { console.log('error'); });
function getStates($sql){
        $result = $sql->query("SELECT * FROM `controllers` WHERE `home_id` = '1' ORDER BY `order`");
        if (isset($result->rows)){
                $result = $result->rows;
                foreach ($result as $key => $value) {
                        if (strtotime($result[$key]['timer']) > -62169990000){
                                // echo strtotime($result[$key]['timer']);
                                $timer_switch = strtotime($result[$key]['timer']) - strtotime(date("Y-m-d H:i:s"));
                                $result[$key]['timer_switch'] = $timer_switch;
                                if ($timer_switch < 0){
                                        $sql->query("UPDATE `controllers` SET `state` = '".$result[$key]['timer_state']."', `timer` = '0000-00-00 00:00:00', `timer_state` = '' WHERE `id` = '".$result[$key]['id']."'");
                                }
                        }
                }
                $res['success'] = $result;
        } else {
                $res['error'] = 'Неверный пароль';
        }
        return $res;
}



При нажатии соответствующей кнопки происходит отправка POST-запроса к файлу ajax.php:

JavaScript
$(document).on('mousedown', '.btn', function(event){
                        event.preventDefault();
                        var id = parseInt($(this).attr('id').replace('btn_', ''));
                        click_wait = false;
                        mousetimer = setTimeout(function(){
                                click_wait = true;
                                setTimer(id);   
                        }, 2000);
                });

                $(document).on('mouseup', '.btn', function(){
                        clearTimeout(mousetimer);
                        if (!click_wait){
                                var id = parseInt($(this).attr('id').replace('btn_', ''));
                                switchController(id);
                                console.log('click !!!');
                                click_wait = false;
                        }
                });
        function switchController(id){
                var el = $('.btn#btn_' + id);
                var state = parseInt($(el).attr('state'));
                var need_state;
                if (state == 0){
                        need_state = 1;
                } else if (state == 1){
                        need_state = 0;
                }
                $(el).addClass('waiting');
                $.ajax({
                        url: 'engine/ajax.php',
                        type: 'POST',
                        dataType: 'json',
                        data: {action: 'setState', id: id, state: state, need_state: need_state},
                })
                .done(function(data) {
                        if (data.success == 'ok'){
                                $(el).attr('state', need_state);
                                $(el).removeClass('waiting').removeClass('btn_on').removeClass('btn_off');
                                if(need_state == 1){
                                        $(el).addClass('btn_on');
                                } else if(need_state == 0){
                                        $(el).addClass('btn_off');
                                }
                        }
                })
                .fail(function(data) {
                        console.log('error');
                });
        }
        function setTimer(id){
                var el = $('.btn#btn_' + id);
                var state = parseInt($(el).attr('state'));
                var need_state;
                if (state == 0){
                        need_state = 1;
                } else if (state == 1){
                        need_state = 0;
                }
                $(el).attr('seconds', 0);
                $(el).addClass('seconds');
                $.ajax({
                        url: 'engine/ajax.php',
                        type: 'POST',
                        dataType: 'json',
                        data: {action: 'setTimer', id: id, state: state, need_state: need_state},
                })
                .done(function(data) {
                })
                .fail(function(data) {
                        console.log('error');
                });
        }



В переменной id передаём айдишник контроллера, а в переменной state — значение его состояния. В файле ajax.php получаем POST-запрос и кладём новые данные в значения записи соответствующего id.

PHP
function setState($sql){
        $need_state = (int)$_POST['need_state'];
        $state = (int)$_POST['state'];
        $id = (int)$_POST['id'];

        $result = $sql->query("UPDATE `controllers` SET `state` = '".$need_state."', `timer` = '0000-00-00 00:00:00' WHERE `id` = '".$id."'");
        // Проверка выполенения Arduino
        $result = $sql->query("SELECT `state` FROM `controllers` WHERE `id` = '".$id."'");
        if ($need_state == $result->row['state']){
                $res['success'] = 'ok';
        } else {
                $res['success'] = 'err';
        }
        return $res;
}

function setTimer($sql){
        $need_state = (int)$_POST['need_state'];
        $state = (int)$_POST['state'];
        $id = (int)$_POST['id'];

        $result = $sql->query("UPDATE `controllers` SET `timer` = NOW() + INTERVAL 3 MINUTE, `timer_state` = '".$need_state."' WHERE `id` = '".$id."'");
        // Проверка выполенения Arduino
        $result = $sql->query("SELECT `state` FROM `controllers` WHERE `id` = '".$id."'");
        if ($need_state == $result->row['state']){
                $res['success'] = 'ok';
        } else {
                $res['success'] = 'err';
        }
        return $res;
}



Arduino же, в свою очередь, по Ethernet обращается к веб-серверу, проверяет уникальный ключ системы, значения контроллеров, парсит полученную строку и переключает значения пинов.

PHP
if (isset($_GET['key']) && $_GET['key'] !== ''){
                $key = $_GET['key'];
        } else {
                die;
        }
        
$key = $sql->escape($key);
        $result = $sql->query("SELECT * FROM `controllers` WHERE `home_id` = '1' AND `key` = '".$key."' ORDER BY `order`");
        foreach ($result->rows as $key => $value) {
                if ($value['id'] == '1'){
                        if ($value['state'] == '1'){
                                $ar .= 'Q';
                        } else if ($value['state'] == '0') {
                                $ar .= 'q';
                        }
                }
                if ($value['id'] == '2'){
                        if ($value['state'] == '1'){
                                $ar .= 'W';
                        } else if ($value['state'] == '0') {
                                $ar .= 'w';
                        }
                }
                if ($value['id'] == '3'){
                        if ($value['state'] == '1'){
                                $ar .= 'E';
                        } else if ($value['state'] == '0') {
                                $ar .= 'e';
                        }
                }
        }



После чего, строка вида »%qUerTY» передается в скетч, где парсится, в зависимости от жестко зашитых правил: каждая буква соответствует своему номеру пина, а регистр отвечает за конечное значение: прописная — 1, строчная — 0.

Arduino
void readData(){
  if (led_connect){
    digitalWrite(6, HIGH);
  } else {
    digitalWrite(6, LOW);
  }
  digitalWrite(4, LOW);
  previousMillis = currentMillis;
  
  led_connect = !led_connect;
    while (client.available()){      
      switch (char c = client.read()) {
        case 'Q':
          digitalWrite(9, HIGH);
          break;
        case 'q':
          digitalWrite(9, LOW);
          break;
        case 'W':
          digitalWrite(8, HIGH);
          break;
        case 'w':
          digitalWrite(8, LOW);
          break;
        case 'E':
          digitalWrite(7, HIGH);
          myservo.write(0);
          break;
        case 'e':
          digitalWrite(7, LOW);
          myservo.write(180);
          break;
      }
    }
}


На пин, отвечающий за серво-машинку, можно передавать значения от 0 до 180 (градусов поворота), а, допустим, на диммер — до 255. Мне же пока хватает двух значений: 0 или 1.

1416318c6a664a5d90e299894b6ba46f.JPG

059b6cc6ad224f8eab3ece4bdd303d0e.JPG

С подключением модулей проблем не возникло: оба реле сопрягаются с передатчиком один раз, без необходимости повторной привязки, и прекрасно выполняют свою задачу.

2391231e0f7646f9ab352d321dee4703.JPG

61b5d3aea1c24afd91ce04fbb1895af5.JPG

А вот с мощностью серво-машинки я немного просчитался: взял на 6 кг, — чего, в общем, хватает для того, чтобы прикрывать и отпускать створку окна, что она открывалась под собственным весом, —, но лучше брать что-то помощнее, на случай, сильного ветра или сквозняка.

Вообще, простор для творчества безграничен: к примеру, можно подключить датчики, например, CO2 или температуры, и открывать / закрывать окно при достижении определенных показателей.
Ну и всю систему, разумеется, я планирую переписать на веб-сокетах, облегчив запросы, — и сделать более гибкой передачу и парсинг значений параметров.

Скетч для ардуино

Небольшое видео

Работа подсветки

Открывание окна

Дмитрий Кузнецов»

© Geektimes