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



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





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

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



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

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



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

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



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

Веб-интерфейс обращается к серверу и проверяет состояние контроллеров, после чего, выводит интерфейс работы с системой: 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 += '<div ';
					if (controller.timer_switch > 0){
						var seconds = 60  - (controller.timer_switch % 60)
						html += 'seconds="' + seconds + '"';
					}
					html += 'id="btn_' + controller.id + '" class="btn" state="' + controller.state + '"><div class="btn_timer"></div></div>';
					$('#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.





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





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

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

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

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

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


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


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

Комментарии (8)


  1. eta4ever
    03.09.2015 14:42
    +1

    В конструкцию вместо сервы просится все тот же универсальный движок с червячным редуктором от дворников или стеклоподъемника (на котором тут по соседству газонокосилка бегает), с рычагом и парой концевиков.


    1. konstantin1970
      04.09.2015 09:15

      Можно и лучше для открывания окон сделать движок постоянного тока с червячным механизмом и на движок поставить измеритель тока. Тогда когда окно плотно закрывается — ток возрастает и движок нужно выключить. На ардуине есть АЦП, это не сложно будет реализовать используя например ACS712


  1. serafims
    03.09.2015 17:39

    Открытие окна надо сделать плавным, чтобы окно не било по серве — отвалится все иначе скоро.


  1. nitso
    03.09.2015 21:13
    +3

    Очень хотелось увидеть какой-то изящный механизм открытия/закрытия окна. А тут даже «серво-сейвера» нет :(

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

    Кстати, у вас на соседнем окне не клапан ли вентиляционный? Он со своей задачей не справляется?

    PHP вообще страшненький :(


    1. dcoder_mm
      03.09.2015 23:13

      1. nitso
        04.09.2015 10:28

        С первого взгляда изяществом не блещут :) Здоровая дура, да еще и с цепью. И, кстати, механизм должен обладать определенной жесткостью в обе стороны. У меня в квартире, например, сквозняком закрывает открытые фрамуги.

        Напрашивается механизм, подобный серве с длинным эластичным элементом, который бы цеплял/толкал створку. Чтобы можно было руками окно открыть и не бояться, что серву сорвёт от порыва ветра/случайного дёрга за ручку. У нас на окнах даже фиксаторы-«гребенки» уже гнутые вхлам, потому что про них забываешь и пытаешься с силой окно закрыть (или открыть).


    1. Sergey_Tokarev
      07.09.2015 19:10

      Конечно, хотелось бы увидеть «изящный механизм открытия/закрытия окна», чтобы не мешал ручному открыванию, чтобы не было его видно, чтобы тихо работал, соплей чтобы не было.

      Но хочется поддержать автора статьи. Думаю по поводу «рука не поднимается сопли по окнам развешивать» — вопрос конкретной реализации. Думаю, у всех, у кого есть руки, в состоянии сопли прибрать хотя бы в кабель канал. А вот сделать подобное открывание-закрывание окон — не каждый может. Думаю, автор без проблем спрячет все сопли. Речь в статье не об этом.

      Согласен с тем, что подобная автоматизация не позволит руками открывать окно на полную, но для ночного режима, когда стало чуть холоднее, можно чуть прикрыть — это хорошее решение.

      Кстати про клапан. Он тут вообще не при чем. Если бы автор хотел что-то аналогичное, то поставил бы скорее рекупиратор, чем автоматизировал бы открывание окон. Рассматривать это стоит скорее как игрушку, нежели как серийное производство.

      И еще. Тут на GT есть несколько статей с достаточно полной и красивой автоматизацией.


  1. jrip
    04.09.2015 12:47
    +3

    PHP страшненький
    JavaScript не очень
    База не оптимальна.
    Стандарты PSR не используются.
    Сопромат и физику забыли — механизм развалится через неделю.
    Везде скрутки.
    Все в целом и выглядит некрасиво.

    А ко всему прочему — изолента то, не рассововерная, не синяя! :)